pax_global_header00006660000000000000000000000064132045447240014517gustar00rootroot0000000000000052 comment=ffa1d18da70b7e95e87036ca6ca39f5b85ef8fb6 mapproxy-1.11.0/000077500000000000000000000000001320454472400134565ustar00rootroot00000000000000mapproxy-1.11.0/.github/000077500000000000000000000000001320454472400150165ustar00rootroot00000000000000mapproxy-1.11.0/.github/ISSUE_TEMPLATE.md000066400000000000000000000022701320454472400175240ustar00rootroot00000000000000 ## Context ## Expected Behavior ## Actual Behavior ## Possible Fix ## Steps to Reproduce 1. 2. 3. 4. ## Context ## Your Environment * Version used: * Environment name and version (e.g. Python 2.7.5 with mod_wsgi 4.5.9): * Server type and version: * Operating System and version: mapproxy-1.11.0/.gitignore000066400000000000000000000003011320454472400154400ustar00rootroot00000000000000*.egg-info *.pyc *.patch build/ dist/ doc/_build examples .ropeproject/ *.sublime-project *.sublime-workspace nosetests*.xml .DS_Store .coverage .project .settings .pydevproject .tox/ .idea/ mapproxy-1.11.0/.travis.yml000066400000000000000000000025241320454472400155720ustar00rootroot00000000000000language: python python: - "2.7" - "3.3" - "3.4" - "3.5" - "3.6" services: - couchdb - riak - redis-server addons: apt: packages: - libproj0 - libgeos-dev - libgdal-dev - libgdal1h - libxslt1-dev - libxml2-dev - build-essential - python-dev - libjpeg-dev - zlib1g-dev - libfreetype6-dev - protobuf-compiler - libprotoc-dev env: global: - MAPPROXY_TEST_COUCHDB=http://127.0.0.1:5984 - MAPPROXY_TEST_REDIS=127.0.0.1:6379 - MAPPROXY_TEST_RIAK_HTTP=http://localhost:8098 - MAPPROXY_TEST_RIAK_PBC=pbc://localhost:8087 # do not load /etc/boto.cfg with Python 3 incompatible plugin # https://github.com/travis-ci/travis-ci/issues/5246#issuecomment-166460882 - BOTO_CONFIG=/doesnotexist matrix: # Test 2.7 and 3.6 also with latest Pillow version include: - python: "2.7" env: USE_LATEST_PILLOW=1 - python: "3.6" env: USE_LATEST_PILLOW=1 cache: directories: - $HOME/.cache/pip before_install: - echo -n "ulimit -n 4096" | sudo tee /etc/default/riak && sudo service riak restart # default open file limit is too low for riak install: - "pip install -r requirements-tests.txt" - "if [[ $USE_LATEST_PILLOW = '1' ]]; then pip install -U Pillow; fi" - "pip freeze" script: nosetests mapproxy mapproxy-1.11.0/AUTHORS.txt000066400000000000000000000010631320454472400153440ustar00rootroot00000000000000MapProxy is written and maintained by Omniscale and various contributors: Development Lead ---------------- - Oliver Tonnhofer Contributors ------------ - Dominik Helle - Marcel Radischat Patches and Suggestions ----------------------- - Bruno Binet - Denis Rykov - Frederic Junod - Iván Sánchez Ortega - Josh Doe - Kai Culemann - Keith Moss - Matt Walker - Miloslav Kmeť - Paul Norman - Ramūnas Dronga - Richard Duivenvoorde - Stephan Holl - Steven D. Lander - Tom Payne - Joseph Svrcek mapproxy-1.11.0/CHANGES.txt000066400000000000000000000626531320454472400153030ustar00rootroot000000000000001.11.0 2017-11-xx ~~~~~~~~~~~~~~~~~ Improvements: - Improve reprojection performance and accuracy. - ArcGIS compact cache: Support for version 2. - ArcGIS compact cache: Improve performance for version 1. - ArcGIS compact cache: Add ``mapproxy-util defrag`` to reduce bundle size after tiles were removed/updated. - ArcGIS REST source: Support opts.map and seed_only. - Use systems CA certs by default and fix ssl_no_cert_checks for Python >=2.7.9 and >=3.4 - WMS: Improve Bounding Boxes in Capabilities. - Mapserver: Find mapserv binary in PATH environment. Fixes: - Seed: Always show last log line (100%). - Fix saving transparent PNGs for some versions of Pillow (workaround for Pillow bug #2633) - SQLite: Fix possible errors on first request after start. - Demo: Fix demo client with `use_grid_names`. - serve-develop: Fix header encoding for Python 3. - Seed: Fix --interactive for Python 3. - Support tagged layers for sources with colons in name. - Support # character in Basis Authentication password. - Fix import error with shapely>=1.6 - Fix duplicate level caches when using WMTS KVP with MBtile/SQLite/CouchDB. Other: - Remove support for Python 2.6 1.10.4 2017-08-17 ~~~~~~~~~~~~~~~~~ Fixes: - Fix Cross Site Scripting (XSS) issue in demo service (#322). A targeted attack could be used for information disclosure. For example: Session cookies of a third party application running on the same domain. 1.10.3 2017-07-07 ~~~~~~~~~~~~~~~~~ Fixes: - Fix crash during clipping - Fix bilinear/bicubic resampling from cropped source - Fix loading empty coverages 1.10.2 2017-06-21 ~~~~~~~~~~~~~~~~~ Fixes: - Fix coverage clipping for caches with a single source 1.10.1 2017-06-06 ~~~~~~~~~~~~~~~~~ Fixes: - Fix mapproxy-util serve-develop for Python 3.6 on Windows - Fix OGR coverages on Windows with Python 3 and official OGR Python bindings - Fix --repeat option of mapproxy-util scales 1.10.0 2017-05-18 ~~~~~~~~~~~~~~~~~ Improvements: - Support for S3 cache. - Support for the ArcGIS Compact Cache format version 1. - Support for GeoPackage files. - Support for Redis cache. - Support meta_tiles for tiles sources with bulk_meta_tiles option. - mbtiles/sqlite cache: Store multiple tiles in one transaction. - mbtiles/sqlite cache: Make timeout and WAL configurable. - ArcGIS REST source: Improve handling for ImageServer endpoints. - ArcGIS REST source: Support FeatureInfo requests. - ArcGIS REST source: Support min_res and max_res. - Support merging of RGB images with fixed transparency. - Coverages: Clip source requests at coverage boundaries. - Coverages: Build the difference, union or intersection of multiple coverages. - Coverages: Create coverages from webmercator tile coordinates like 05/182/123 with expire tiles files. - Coverages: Add native support for GeoJSON (no OGR/GDAL required). - mapproxy-seed: Add --duration, -reseed-file and -reseed-interval options. Fixes: - Fix level selection for grids with small res_factor. - mapproxy-util scales: Fix for Python 3. - WMS: Fix FeatureInfo precision for transformed requests. - Auth-API: Fix FeatureInfo for layers with limitto. - Fixes subpixel transformation deviations with Pillow 3.4 or higher. - mapproxy-seed: Reduce log output, especially in --quiet mode. - mapproxy-seed: Improve tile counter for tile grids with custom resolutions. - mapproxy-seed: Improve saving of the seed progress for --continue. - Fix band-merging when not all sources return an image. Other: - Python 2.6 is no longer supported. 1.9.1 2017-01-18 ~~~~~~~~~~~~~~~~ Fixes: - serve-develop: fixed reloader for Windows installations made with recent pip version (#279) 1.9.0 2016-07-22 ~~~~~~~~~~~~~~~~ Improvements: - New band merge feature. Allows to create false-color or grayscale images on the fly. - Support for ArcGIS REST sources. - Support multiple tilesets for each WMTS layer with the new tile_sources option. - Allow to build WMS images from SQLite cache with more then 330 tiles. - New `arcgis` cache layout. Compatible to ArcGIS exploded caches. - New `mp` cache layout. Reduces number of nested directories. - Prevent unneeded quantizing/re-encoding of images. - Demo client: Support custom tile_size. Fixes: - Fix quantization error for some image modes (e.g. grayscale image with transparency) - Support custom Proj4/EPSG files in mapproxy-util grid. - Convert paletted images to RGB(A) to avoid NEAREST resampling. - Fix quantizing with FASTOCTREE for paletted images with alpha channel. - Keep configured layer order in WMTS capabilities. - Fix coverage loading with Python 3. Other: - Make the output of various utils more clear. - wms.md.title is no longer required, default to "MapProxy WMS". 1.8.2 2016-01-22 ~~~~~~~~~~~~~~~~ Fixes: - serve-develop: fixed reloader for Windows installations made with recent pip version 1.8.1 2015-09-22 ~~~~~~~~~~~~~~~~ Improvements: - WMS 1.3.0: support for metadata required by INSPIRE View Services - WMS: OnlineResource defaults to service URL Fixes: - mapproxy-seed: fix race-condition which prevented termination at the end of the seeding process - autoconfig: parse capabilities without ContactInformation - SQLite cache: close files after seeding - sqlite/mbtiles: fix tile lock location - WMS 1.0.0: fix image format for source requests - WMS: allow floats for X/Y in GetFeatureInfo requests - CouchDB: fix for Python 3 Other: - mapproxy-seed: seeding a cache with disable_storage: true returns an error - all changes are now tested against Python 2.7, 3.3, 3.4 and 3.5 1.8.0 2015-05-18 ~~~~~~~~~~~~~~~~ Features: - Support for Python 3.3 or newer Improvements: - WMS is now available at /service, /ows and /wms - WMTS KVP is now available at /service and /ows, RESTful service at /wmts - allow tiled access to layers with multiple map:false sources - add Access-control-allow-origin header to HTTP responses - list KVP and RESTful capabilities on demo page - disable verbose seed output if stdout is not a tty - add globals.cache.link_single_color_images option - support scale_factor for Mapnik sources Fixes: - handle EPSG axis order in WMTS capabilities - pass through legends/featureinfo for recursive caches - accept PNG/JPEG style image_format for WMS 1.0.0 - fix TMS capabilities in demo for TMS with use_grid_names - fix ctrl+c behaviour in mapproxy-seed - fix BBOX parsing in autoconf for WMS 1.3.0 services Other: - 1.8.0 is expected to work with Python 2.6, but it is no longer officially supported - MapProxy will now issue warnings about configurations that will change with 2.0. doc/mapproxy_2.rst lists some of the planed incompatible changes 1.7.1 2014-07-08 ~~~~~~~~~~~~~~~~ Fixes: - fix startup of mapproxy-util when libgdal/geos is missing 1.7.0 2014-07-07 ~~~~~~~~~~~~~~~~ Features: - new `mapproxy-util autoconf` tool - new versions option to limit supported WMS versions - set different max extents for each SRS with bbox_srs Improvements: - display list of MultiMapProxy projects sorted by name - check included files (base) for changes in reloader and serve-develop - improve combining of multiple cascaded sources - respect order of --seed/--cleanup tasks - catch and log sqlite3.OperationalError when storing tiles - do not open cascaded responses when image format matches - mapproxy-seed: retry longer if source fails (100 instead of 10) - mapproxy-seed: give more details if source request fails - mapproxy-seed: do not hang nor print traceback if seed ends after permanent source errors - mapproxy-seed: skip seeds/cleanups with empty coverages - keep order of image_formats in WMS capabilities Fixes: - handle errors when loading to many tiles from mbtile/sqlite in one batch - reduce memory when handling large images - allow remove_all for mbtiles cleanups - use extent from layer metadata in WMTS capabilities - handle threshold_res higher than first resolution - fix exception handling in Mapnik source - only init libproj when requested Other: - 1.7.x is the last release with support for Python 2.5 - depend on Pillow if PIL is not installed 1.6.0 2013-09-12 ~~~~~~~~~~~~~~~~ Improvements: - Riak cache supports multiple nodes Fixes: - handle SSL verification when using HTTP proxy - ignore errors during single color symlinking Other: - --debug option for serve-multiapp-develop - Riak cache requires Riak-Client >=2.0 1.6.0rc1 2013-08-15 ~~~~~~~~~~~~~~~~~~~ Features: - new `sqlite` cache with timestamps and one DB for each zoom level - new `riak` cache - first dimension support for WMTS (cascaded only) - support HTTP Digest Authentication for source requests - remove_all option for seed cleanups - use real alpha composite for merging layers with transparent backgrounds - new tile_lock_dir option to write tile locks outside of the cache dir - new decorate image API - new GLOBAL_WEBMERCATOR grid with origin:nw and EPSG:3857 Improvements: - speed up configuration loading with tagged sources - speed up seeding with sparse coverages and limited levels (e.g. only level 17-20) - add required params to WMS URL in mapproxy-util wms-capabilities - support for `@` and `:` in HTTP username and password - try to load pyproj before using libproj.dll on Windows - support for GDAL python module (osgeo.ogr) besides using gdal.so/dll directly - files are now written atomical to support concurrent access to the same tile cache from different servers (e.g. via NFS) - support for WMS 1.3.0 in mapproxy-util wms-capabilities - support layer merge for 8bit PNGs - support for OGR/GDAL 1.10 - show TMS root resource at /tms Fixes: - support requests>=1.0 for CouchDB cache - HTTP_X_FORWARDED_HOST can be a list of hosts - fixed KML for caches with origin: nw - fixed 'I/O operation on closed file' errors - fixed memory leak when reloading large configurations - improve handling of mixed grids/formats when using caches as cache sources - threading related crashes in coverage handling - close OGR sources - catch IOErrors when PIL/Pillow can't identify image file Other: - update example configuration (base-config) - update deployment documentation - update OpenLayers version in demo service - use restful_template URL in WMTS demo - update MANIFEST.in to prevent unnecessary warnings during installation - accept Pillow as depencendy instead of PIL when already installed - deprecate use_mapnik2 option 1.5.0 2012-12-05 ~~~~~~~~~~~~~~~~ Features: - read remove_before/refresh_before timestamp from file - add --concurrency option to mapproxy-utils export Fixes: - fixed where option for coverages (renamed from ogr_where) - only write seed progess with --continue or --progress-file option Other: - add EPSG:3857 to WMS default SRSs and remove UTM/GK - remove import error warning for shapely - create metadata table in MBTiles caches 1.5.0rc1 2012-11-19 ~~~~~~~~~~~~~~~~~~~ Features: - clipping of tile request to polygon geometries in security API - WMTS support in security API - mixed_image mode that automatically chooses between PNG/JPEG - use caches as source for other caches - `mapproxy-util grids` tool to analyze grid configurations - `mapproxy-util wms-capabilities` tool - `mapproxy-util export` tool - use_grid_names option to access Tiles/TMS/KML layers by grid name instead of EPSGXXXX - origin option for TMS to change default origin of the /tiles service - continue stopped/interrupted seed processes - support min_res/max_res for tile sources Improvements: - do not show layers with incompatible grids in WMTS/TMS demo - make 0/0/0.kml optional for the initial KML file - use BBOX of coverage for capabilities in seed_only layers - ignore debug layer when loading tile layers - simplified coverage configuration - add reloader option to make_wsgi_app() - add MetadataURL to WMS 1.1.1 capabilities - improved WMTS services with custom grids (origin) - use in_image exceptions in WMS demo client - larger map in demo client - always request with transparent=true in WMS demo client - use in_image exceptions in WMS demo client Fixes: - fixed reloading of multiapps in threaded servers - fixed BBOX check for single tile requests - fixed TMS for caches with watermarks - fixed limited_to clipping for single layer requests with service-wide clipping geometries - fixed WMTS RESTful template Other: - deprecated `origin` option for tile sources was removed - empty tiles are now returned as PNG even if requested as .jpeg 1.4.0 2012-05-15 ~~~~~~~~~~~~~~~~~ Fixes: - fix TypeError exception when auth callback returns {authorized:'full'} - use MAPPROXY_LIB_PATH on platforms other that win32 and darwin - raise config error for mapnik sources when mapnik could not be imported 1.4.0rc1 2012-05-02 ~~~~~~~~~~~~~~~~~~~ Features: - support clipping of requests to polygon geometries in security API - support for WMS 1.3.0 extended layer capabilities - on_error handling for tile sources. fallback to empty/transparent tiles when the source returns HTTP codes like 404 or 204 - add HTTP Cache-Control header to WMS-C responses Improvements: - WMS source requests and requests to cached tiles are now clipped to the extent. this should prevent projection errors when requesting large bbox (e.g. over 180/90 in EPSG:4326) - improved lock timeouts in mapproxy-seed - the debug source does not overwrite the layer extent anymore. makes it more usable in demo/wms clients - support for multiple files and recursion in base option - mapproxy-seed ETA output is now more responsive to changes in seed speed - improved demo service - choose different SRS for WMS layers - support for WMTS Fixes: - support loading of WKT polygon files with UTF8 encoding and BOM header - upgraded dictspec module with fix for some nested configuration specs. a bug prevented checking of the layers configuration Other: - the documentation now contains a tutorial - old layer configuration syntax is now deprecated - EPSG:4326/900913/3857 are now always initialized with the +over proj4 option to prevent distortions at the dateline see: http://fwarmerdam.blogspot.de/2010/02/world-mapping.html 1.3.0 2012-01-13 ~~~~~~~~~~~~~~~~ No changes since 1.3.0b1 1.3.0b1 2012-01-03 ~~~~~~~~~~~~~~~~~~ Features: - support for RESTful WMTS requests with custom URL templates - support for CouchDB as tile backend - support for Mapnik 2 sources - limit maximum WMS response size with max_output_pixels - new color option for watermarks - new ``mapproxy-util serve-multiapp-develop`` command - new wms.bbox_srs option for bounding boxes in multiple SRS in WMS capabilities Improvements: - log exceptions when returning internal errors (500) Fixes: - fix BBOX in WMS-C capabilities - prevent exception for WMS requests with unsupported image formats with mime-type options (like 'image/png; mode=24bit') - fixed blank image results for servers that call .close() on the response (like gunicorn) Other: - origin option for tile sources is deprecated. use a custom grid with the appropriate origin. 1.2.1 2011-09-01 ~~~~~~~~~~~~~~~~ Fixes: - fixed configuration of watermarks - support for unicode title in old-style layer configuration 1.2.0 2011-08-31 ~~~~~~~~~~~~~~~~ Fixes: - fixed links in demo service when running as MultiMapProxy 1.2.0b1 2011-08-17 ~~~~~~~~~~~~~~~~~~ Features: - support for MBTiles cache - support for (tagged-) layers for Mapnik sources - configurable cache layout (tilecache/TMS) - new `mapproxy-util scales` tool - use MultiMapProxy with server scripts (mapproxy.multiapp.make_wsgi_app) Fixes: - prevent black borders for some custom grid configurations - all fixes from 1.1.x 1.1.2 2011-07-06 ~~~~~~~~~~~~~~~~ Fixes: - compatibility with older PyYAML versions - do not try to transform tiled=true requests - escape Windows path in wsgi-app template 1.1.1 2011-06-26 ~~~~~~~~~~~~~~~~ Fixes: - add back transparent option for mapnik/tile sources (in addition to image.transparent) - keep alpha channel when handling image.transparent_color - fixed combining of multiple WMS layers with transparent_color - fixed header parsing for MapServer CGI source 1.1.0 2011-06-01 ~~~~~~~~~~~~~~~~ Other: - Changed license to Apache Software License 2.0 Fixes: - fixed image quantization for non-png images with globals.image.paletted=True 1.1.0rc1 2011-05-26 ~~~~~~~~~~~~~~~~~~~ Improvements: - add template to build MapProxy .deb package - font dir is now configurable with globals.image.font_dir Fixes: - fixed errors in config spec 1.1.0b2 2011-05-19 ~~~~~~~~~~~~~~~~~~ Improvements: - unified logging - verify mapproxy/seed configurations 1.1.0b1 2011-05-12 ~~~~~~~~~~~~~~~~~~ Features: - support for tagged WMS source names: wms:lyr1,lyr2 - new Mapserver source type - new Mapnik source type - new mapproxy-util command - include development server (``mapproxy-util serve-develop``) - first WMTS implementation (KVP) - configurable image formats - support for ArcGIS tile sources (/L09/R00000005/C0000000d) - support for bbox parameter for tile sources Improvements: - tweaked watermarks on transparent images - [mapproxy-seed] initialize MapProxy logging before seeding - authentication callbacks get environ and qusery_extent - authentication callbacks can force HTTP 401 returns - hide error tracebacks from YAML parser - support for multipolygons in coverages - add support for HTTP_X_SCRIPT_NAME - support for integer images (e.g. 16bit grayscale PNG) Fixes: - fixes demo on Windows (loaded static content from wrong path) - fixed one-off error with grid.max_res: last resolution is now < max_res e.g. min_res: 1000 max_res: 300 -> now [1000, 500], before [1000, 500, 250] - add workaround for Python bug #4606 (segfaults during projection on 64bit systems) - do not add attribution to WMS-C responses Other: - removed Paste dependencies - removed deprecated mapproxy-cleanup tool, feature included in mapproxy-seed 1.0.0 2011-03-03 ~~~~~~~~~~~~~~~~ - no changes since 1.0.0rc1 1.0.0rc1 2011-02-25 ~~~~~~~~~~~~~~~~~~~ Improvements: - handle epsg:102100 and 102113 as equivalents to 900913/3857 Fixes: - fixed attribution placement and padding 1.0.0b2 2011-02-18 ~~~~~~~~~~~~~~~~~~ Improvements: - [mapproxy-seed] support for configuration includes in mapproxy.yaml (base) - [mapproxy-seed] updated config templates - KML: reduce number of required KML requests - KML: improve superoverlays with res_factor != 2 Fixes: - [mapproxy-seed] apply globals from mapproxy.yaml during seed - fix tile_lock cleanup - merging of cache sources with only tile sources failed 1.0.0b1 2011-02-09 ~~~~~~~~~~~~~~~~~~ Features: - [mapproxy-seed] separated seed and cleanup tasks; call tasks independently - XSL transformation of WMS FeatureInfo responses - content aware merging of multiple XML/HTML FeatureInfo repsonses - FeatureInfo types are configurable with wms.featureinfo_types - request cascaded sources in parallel (with threading or eventlet) with new wms.concurrent_layer_renderer option - disable GetMap requests for WMS sources (for FeatureInfo only sources) - new cache.disable_storage option - authorization framework - new image.transparent_color option: replaces color with full transparency - new image.opacity option: blend between opaque layers - new watermark.spacing option: place watermark on every other tile - new wms.on_source_errors option: capture errors and display notice in response image when some sources did not respond - support for custom http headers for requests to sources - add support for http options for tile source (user/password, https ssl options, headers, timeout) Improvements: - [mapproxy-seed] enhanced CLI (summary and interactive mode) - combine requests to the same WMS URL - support for local SLD files (sld: file://sld.xml) - changed watermark color to gray: improves readability on full transparent images - support for transparent/overlayed tile sources - renamed thread_pool_size to concurrent_tile_creators - tweaked KML level of detail parameters to fix render issues in Google Earth with tilted views Fixes: - rounding errors in meta-tile size calculation for meta_buffer=0 - work with upcomming PIL 1.2 release 0.9.1 2011-01-10 ~~~~~~~~~~~~~~~~ Fixes: - fixed regression in mapproxy_seed - resolve direct WMS request issues with equal but not same SRS (e.g. 900913/3857) 0.9.1rc2 2010-12-20 ~~~~~~~~~~~~~~~~~~~ Improvements: - Allow nested layer configurations (layer groups/trees) - Support custom path to libproj/libgdal with MAPPROXY_LIB_PATH environ - Look for xxx if libxxx returned no results. - Limit lat/lon bbox in WMS capabilities to +-89.999999 north/south values Fixes: - bug fix for threshold_res that overlap with the stretch_factor 0.9.1rc1 2010-12-07 ~~~~~~~~~~~~~~~~~~~ Features: - WMS 1.1.0 support - Coverage support (limit sources to areas via WKT/OGC polygons) - new base option to reuse configurations - ScaleHint support (min/max_res, min/max_scale) - Support for multiple MapProxy configurations in one process with distinct global/cache/source/etc. configurations - New MultiMapProxy: dynamically load multiple configurations (experimental) - threshold_res option for grids: switch cache levels at fixed resolutions - seed_only option for sources: allows offline usage - GetLegendGraphic support - SLD support for WMS sources Improvements: - concurrent_requests limit is now per unique hostname and not per URL - concurrent_requests can be set with globals.http.concurrent_requests - font_size of watermark is now configurable - improved configuration loading time and memory consumption - make use of PyYAML's C extension if available - cache projection attributes in SRS objects for better performance - try system wide projection definitions first, then fallback to defaults (e.g. for EPSG:900913) - trailing slash is now optional for /tms/1.0.0 - support for http.ssl_ca_cert for each WMS source - support for http.client_timeout for each WMS source (Python >=2.6) Fixes: - removed start up error on systems where proj4 misses EPSG:3857 - fixed color error for transparent PNG8 files - fixed links in demo service when URL is not /demo/ - removed memory leak proj4 wrapper - fixed mapproxy-seed -f option - tests work without Shapely 0.9.0 2010-10-18 ~~~~~~~~~~~~~~~~ - minor bug fixes 0.9.0rc1 2010-10-13 ~~~~~~~~~~~~~~~~~~~ - new OpenLayers-based '/demo' service that shows all configured WMS/TMS layers - display welcome message at '/' instead of 'not found' error - less rigid feature info request parser (no error with missing style or format parameters). Use wms.strict to enable OCG compliant mode. - updated tempita to 0.5 0.9.0b2 2010-09-20 ~~~~~~~~~~~~~~~~~~ - new minimize_meta_requests option - moved python implementation dependent code to mapproxy.platform module 0.9.0b1 2010-08-30 ~~~~~~~~~~~~~~~~~~ - Improved support for EPSG:3857 - Source requests now never go beyond the grid BBOX even with meta_buffers/meta_tiles - removed install_requires - flup: not required for all deployment options - tempita: now embeded - now Python 2.7 compatible - [mapproxy-seed] fixed libgdal loading on some Linux systems - [mapproxy-seed] check for intersections on all levels - add origin options to /tiles service to support Google Maps clients - Improved PNG performance with PIL fastpng branch. - New concurrent_requests option to limit requests for each source WMS server. - minor bug fixes 0.9.0a1 2010-07-27 ~~~~~~~~~~~~~~~~~~ - new configuration format (merged proxy.yaml and service.yaml) - refactoring of the caching (sources and layers) - large refactoring of the package layout - pyproj dependency is not required when libproj is available - removed jinja dependency - more options to define tile grids (min_res, max_res, etc.) 0.8.4 2010-08-01 ~~~~~~~~~~~~~~~~ - Extra newline at the end of all templates. Some deployment setups removed the last characters. - Improved PNG performance with PIL fastpng branch. - New concurrent_requests option to limit requests for each source WMS server. 0.8.3 2010-06-01 ~~~~~~~~~~~~~~~~ - Some bug fixes regarding feature info - The configured resolutions are sorted 0.8.3rc2 2010-05-25 ~~~~~~~~~~~~~~~~~~~ - HTTPS support with certificate verification and HTTP Basic- Authentication. - New `use_direct_from_level` and `use_direct_from_res` options to disable caching for high resolutions. - New `cache_tiles` source for more flexible tile-based sources Supports url templates like '/tiles?x=%(x)s&y=%(y)s&level=%(z)s' and Quadkeys as used by Bing-Maps. (as suggested by Pascal) - You can limit the SRS of a source WMS with the `supported_srs` option. MapProxy will reproject between cached/requested SRS and the supported. This also works with direct layers, i.e. you can reproject WMS on-the-fly. 0.8.3rc1 2010-04-30 ~~~~~~~~~~~~~~~~~~~ - new improved seed tool - seed polygon areas instead BBOX (from shapefiles, etc) - advanced seeding strategy - multiprocessing - new link_single_color_images layer option. multiple "empty" tiles will be linked to the same image. (Unix only) - fixed transparency for image parts without tiles - log HTTP requests to servers regardless of the success - made proj4 data dir configurable - use same ordering of layers in service.yaml for capabilities documents (use list of dicts in configuration, see docs) - performance improvements for requests with multiple layers and for layers with smaler BBOXs 0.8.2 2010-04-13 ~~~~~~~~~~~~~~~~ - no changes since 0.8.2rc1 0.8.2rc1 2010-04-01 ~~~~~~~~~~~~~~~~~~~ - add fallback if PIL is missing TrueType support - use zc.lockfile for locking - improved logging: - log to stdout when using develop.ini - add %(here)s support in log.ini (changed from {{conf_base_dir}}) - do not enable ConcurrentLogHandler by default 0.8.1 2010-03-25 ~~~~~~~~~~~~~~~~ - improved performance for simple image transformation (same srs and same resolution) #4 0.8.0 2010-03-22 ~~~~~~~~~~~~~~~~ - initial release mapproxy-1.11.0/COPYING.txt000066400000000000000000000035571320454472400153410ustar00rootroot00000000000000Copyright (C) 2010, 2011 Omniscale Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. For the following parts the copyright is held by third parties and the license terms differ. mapproxy/image/fonts/*.ttf -------------------------- Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) See mapproxy/image/fonts/LICENSE mapproxy/util/ext/odict.py -------------------------- (c) 2008 by Armin Ronacher and PEP 273 authors. Modified "3-clause" BSD license. mapproxy/util/ext/tempita/*.py ------------------------------ (c) 2009 by Ian Bicking. MIT license. mapproxy/util/ext/lockfile.py ----------------------------- Copyright (c) 2001, 2002 Zope Corporation and Contributors. Zope Public License (ZPL) Version 2.1 See file header. mapproxy/util/ext/(local|serving).py ----------------------------- Copyright (c) 2010 by the Werkzeug Team Modified "3-clause" BSD license. mapproxy/util/ext/dictspec/*.py ------------------------------ (c) 2011 by Oliver Tonnhofer, Omniscale. MIT license. mapproxy/util/ext/itertools.py ------------------------------ # Copyright © 2001-2012 Python Software Foundation; All Rights Reserved # http://docs.python.org/library/itertools.html#itertools.izip_longest PSF license. mapproxy/test/schemas/* ----------------------- Copyright (c) 1994 - 2010 Open Geospatial Consortium, Inc mapproxy-1.11.0/LICENSE.txt000066400000000000000000000261361320454472400153110ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. mapproxy-1.11.0/MANIFEST.in000066400000000000000000000015341320454472400152170ustar00rootroot00000000000000include setup.py include setup.cfg include README.rst include LICENSE.txt include CHANGES.txt include COPYING.txt include requirements-tests.txt include doc/GM.txt include doc/*.yaml include doc/yaml/*.yaml include doc/Makefile include doc/*.rst include doc/conf.py include doc/imgs/*.png include doc/_static/*.png include doc/_static/*.css include doc/_templates/*.html recursive-include mapproxy/config_template *.ini *.wsgi *.yaml recursive-include mapproxy/image/fonts *.ttf LICENSE recursive-include mapproxy/service/templates *.css *.cfg *.gif *.png *.xml *.html *.js recursive-include mapproxy/test/schemas *.xml *.xsd *.dtd *.wsdl *.txt recursive-include mapproxy/test/system/fixture *.yaml *.mbtiles *.geojson *.xml *.jpeg *.png *.py recursive-include mapproxy/test/unit epsg *.dbf *.shp *.shx recursive-include mapproxy/util/ext/wmsparse/test *.xmlmapproxy-1.11.0/Makefile000066400000000000000000000020251320454472400151150ustar00rootroot00000000000000# Makefile for building MapProxy-Debian-packages # # (c) 2011 Stephan Holl, # PYTHON=`which python` DESTDIR=/ BUILDIR=$(CURDIR)/debian/mapproxy PROJECT=MapProxy VERSION=1.1.0 all: @echo "make source - Create source package" @echo "make install - Install on local system" @echo "make buildrpm - Generate a rpm package" @echo "make builddeb - Generate a deb package" @echo "make clean - Get rid of scratch and byte files" source: $(PYTHON) setup.py sdist $(COMPILE) install: $(PYTHON) setup.py install --root $(DESTDIR) $(COMPILE) buildrpm: $(PYTHON) setup.py bdist_rpm builddeb: # build the source package in the parent directory # then rename it to project_version.orig.tar.gz $(PYTHON) setup.py sdist $(COMPILE) --dist-dir=../ rename -f 's/$(PROJECT)-(.*)\.tar\.gz/$(PROJECT)_$$1\.orig\.tar\.gz/' ../* # build the package dpkg-buildpackage -i -I -rfakeroot clean: $(PYTHON) setup.py clean fakeroot $(MAKE) -f $(CURDIR)/debian/rules clean rm -rf build/ MANIFEST find . -name '*.pyc' -delete mapproxy-1.11.0/README.rst000066400000000000000000000012221320454472400151420ustar00rootroot00000000000000MapProxy is an open source proxy for geospatial data. It caches, accelerates and transforms data from existing map services and serves any desktop or web GIS client. .. image:: https://mapproxy.org/mapproxy.png MapProxy is a tile cache, but also offers many new and innovative features like full support for WMS clients. MapProxy is actively developed and supported by `Omniscale `_, it is released under the Apache Software License 2.0, runs on Unix/Linux and Windows and is easy to install and to configure. Go to https://mapproxy.org/ for more information. The documentation is available at: https://mapproxy.org/docs/latest/ mapproxy-1.11.0/debian/000077500000000000000000000000001320454472400147005ustar00rootroot00000000000000mapproxy-1.11.0/debian/changelog000066400000000000000000000011261320454472400165520ustar00rootroot00000000000000mapproxy (1.8.0) unstable; urgency=low * New upstream, release. -- Oliver Tonnhofer Fri, 17 Jul 2015 09:38:03 +0000 mapproxy (1.5.0) unstable; urgency=low * New upstream release. -- Oliver Tonnhofer Wed, 05 Dec 2012 10:15:19 +0100 mapproxy (1.1.2) unstable; urgency=low * First debian release. -- Bjoern Schilberg Fri, 24 Jun 2011 09:49:44 +0200 mapproxy (1.1.0) unstable; urgency=low * Initial release of the debian-files -- Stephan Holl Mon, 23 May 2011 09:50:26 +0200 mapproxy-1.11.0/debian/compat000066400000000000000000000000021320454472400160760ustar00rootroot000000000000007 mapproxy-1.11.0/debian/control000066400000000000000000000016461320454472400163120ustar00rootroot00000000000000Source: mapproxy Section: python Priority: optional Maintainer: Stephan Holl Build-Depends: debhelper (>=7.0.50~), python-support (>= 0.6), cdbs (>= 0.4.49), python-setuptools, quilt Standards-Version: 3.8.4 Package: mapproxy Architecture: all Homepage: http://www.mapproxy.org Depends: ${misc:Depends}, ${python:Depends}, python-pkg-resources, python-setuptools, python-imaging, python-yaml, libproj0, ttf-dejavu, ttf-dejavu-extra Suggests: python-lxml, python-shapely, python-pastedeploy, libgdal1 Description: This programm MapProxy is an open source proxy for geospatial data. It caches, accelerates and transforms data from existing map services and serves any desktop or web GIS client. MapProxy is a tile cache solution, but also offers many new and innovative features like full support for WMS clients. It is released under the Apache Software License 2.0, runs on Unix/Linux and Windows. mapproxy-1.11.0/debian/copyright000066400000000000000000000032231320454472400166330ustar00rootroot00000000000000Copyright (C) 2010, 2011 Omniscale Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. For the following parts the copyright is held by third parties and the license terms differ. mapproxy/image/fonts/*.ttf -------------------------- Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) See mapproxy/image/fonts/LICENSE mapproxy/util/ext/odict.py -------------------------- (c) 2008 by Armin Ronacher and PEP 273 authors. Modified "3-clause" BSD license. mapproxy/util/ext/tempita/*.py ------------------------------ (c) 2009 by Ian Bicking. MIT license. mapproxy/util/ext/lockfile.py ----------------------------- Copyright (c) 2001, 2002 Zope Corporation and Contributors. Zope Public License (ZPL) Version 2.1 See file header. mapproxy/util/ext/(local|serving).py ----------------------------- Copyright (c) 2010 by the Werkzeug Team Modified "3-clause" BSD license. mapproxy/util/ext/dictspec/*.py ------------------------------ (c) 2011 by Oliver Tonnhofer, Omniscale. MIT license. mapproxy/test/schemas/* ----------------------- Copyright (c) 1994 - 2010 Open Geospatial Consortium, Inc mapproxy-1.11.0/debian/patches/000077500000000000000000000000001320454472400163275ustar00rootroot00000000000000mapproxy-1.11.0/debian/patches/font_dir.patch000066400000000000000000000146201320454472400211570ustar00rootroot00000000000000Index: mapproxy/MANIFEST.in =================================================================== --- mapproxy.orig/MANIFEST.in 2015-07-17 09:16:34.000000000 +0000 +++ mapproxy/MANIFEST.in 2015-07-17 09:17:08.000000000 +0000 @@ -18,9 +18,8 @@ include doc/_templates/*.html recursive-include mapproxy/config_template *.ini *.wsgi *.yaml -recursive-include mapproxy/image/fonts *.ttf LICENSE recursive-include mapproxy/service/templates *.css *.cfg *.gif *.png *.xml *.html *.js recursive-include mapproxy/test/schemas *.xml *.xsd *.dtd *.wsdl *.txt recursive-include mapproxy/test/system/fixture *.yaml *.mbtiles *.geojson *.xml *.jpeg *.png *.py recursive-include mapproxy/test/unit epsg *.dbf *.shp *.shx -recursive-include mapproxy/util/ext/wmsparse/test *.xml \ No newline at end of file +recursive-include mapproxy/util/ext/wmsparse/test *.xml Index: mapproxy/mapproxy/config/defaults.py =================================================================== --- mapproxy.orig/mapproxy/config/defaults.py 2015-07-17 09:16:34.000000000 +0000 +++ mapproxy/mapproxy/config/defaults.py 2015-07-17 09:16:42.000000000 +0000 @@ -43,7 +43,7 @@ max_shrink_factor = 4.0, paletted = True, transparent_color_tolerance = 5, - font_dir = None, + font_dir = '/usr/share/fonts/truetype/ttf-dejavu', ) # number of concurrent requests to a tile source Index: mapproxy/mapproxy/image/fonts/LICENSE =================================================================== --- mapproxy.orig/mapproxy/image/fonts/LICENSE 2015-07-17 09:16:34.000000000 +0000 +++ /dev/null 1970-01-01 00:00:00.000000000 +0000 @@ -1,99 +0,0 @@ -Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. -Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) - -Bitstream Vera Fonts Copyright ------------------------------- - -Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is -a trademark of Bitstream, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of the fonts accompanying this license ("Fonts") and associated -documentation files (the "Font Software"), to reproduce and distribute the -Font Software, including without limitation the rights to use, copy, merge, -publish, distribute, and/or sell copies of the Font Software, and to permit -persons to whom the Font Software is furnished to do so, subject to the -following conditions: - -The above copyright and trademark notices and this permission notice shall -be included in all copies of one or more of the Font Software typefaces. - -The Font Software may be modified, altered, or added to, and in particular -the designs of glyphs or characters in the Fonts may be modified and -additional glyphs or characters may be added to the Fonts, only if the fonts -are renamed to names not containing either the words "Bitstream" or the word -"Vera". - -This License becomes null and void to the extent applicable to Fonts or Font -Software that has been modified and is distributed under the "Bitstream -Vera" names. - -The Font Software may be sold as part of a larger software package but no -copy of one or more of the Font Software typefaces may be sold by itself. - -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, -TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME -FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING -ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF -THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE -FONT SOFTWARE. - -Except as contained in this notice, the names of Gnome, the Gnome -Foundation, and Bitstream Inc., shall not be used in advertising or -otherwise to promote the sale, use or other dealings in this Font Software -without prior written authorization from the Gnome Foundation or Bitstream -Inc., respectively. For further information, contact: fonts at gnome dot -org. - -Arev Fonts Copyright ------------------------------- - -Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. - -Permission is hereby granted, free of charge, to any person obtaining -a copy of the fonts accompanying this license ("Fonts") and -associated documentation files (the "Font Software"), to reproduce -and distribute the modifications to the Bitstream Vera Font Software, -including without limitation the rights to use, copy, merge, publish, -distribute, and/or sell copies of the Font Software, and to permit -persons to whom the Font Software is furnished to do so, subject to -the following conditions: - -The above copyright and trademark notices and this permission notice -shall be included in all copies of one or more of the Font Software -typefaces. - -The Font Software may be modified, altered, or added to, and in -particular the designs of glyphs or characters in the Fonts may be -modified and additional glyphs or characters may be added to the -Fonts, only if the fonts are renamed to names not containing either -the words "Tavmjong Bah" or the word "Arev". - -This License becomes null and void to the extent applicable to Fonts -or Font Software that has been modified and is distributed under the -"Tavmjong Bah Arev" names. - -The Font Software may be sold as part of a larger software package but -no copy of one or more of the Font Software typefaces may be sold by -itself. - -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL -TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. - -Except as contained in this notice, the name of Tavmjong Bah shall not -be used in advertising or otherwise to promote the sale, use or other -dealings in this Font Software without prior written authorization -from Tavmjong Bah. For further information, contact: tavmjong @ free -. fr. - -$Id: LICENSE 2133 2007-11-28 02:46:28Z lechimp $ mapproxy-1.11.0/debian/patches/package_data.patch000066400000000000000000000007701320454472400217400ustar00rootroot00000000000000diff -r ca4999c5c60e setup.py --- a/setup.py Tue May 24 13:00:18 2011 +0200 +++ b/setup.py Wed May 25 11:42:31 2011 +0200 @@ -38,7 +38,7 @@ 'lighttpd_root_fix = mapproxy.util.wsgi:lighttpd_root_fix_filter_factory', ], }, - package_data = {'': ['*.xml', '*.yaml', '*.ttf', '*.wsgi', '*.ini']}, + package_data = {'': ['*.xml', '*.yaml', '*.wsgi', '*.ini']}, install_requires=install_requires, classifiers=[ "Development Status :: 5 - Production/Stable", mapproxy-1.11.0/debian/patches/series000066400000000000000000000000421320454472400175400ustar00rootroot00000000000000font_dir.patch package_data.patch mapproxy-1.11.0/debian/pyversions000066400000000000000000000000101320454472400170330ustar00rootroot000000000000002.5-2.7 mapproxy-1.11.0/debian/rules000077500000000000000000000005041320454472400157570ustar00rootroot00000000000000#!/usr/bin/make -f # -*- makefile -*- DEB_PYTHON_SYSTEM := pysupport include /usr/share/cdbs/1/rules/debhelper.mk include /usr/share/cdbs/1/rules/patchsys-quilt.mk include /usr/share/cdbs/1/class/python-distutils.mk DEB_COMPRESS_EXCLUDE := .py clean:: rm -rf build build-stamp configure-stamp build/ MANIFEST dh_clean mapproxy-1.11.0/debian/watch000066400000000000000000000001151320454472400157260ustar00rootroot00000000000000version=3 http://mapproxy.org/static/rel/MapProxy-([0-9]\.[0-9].+)\.tar\.gz mapproxy-1.11.0/doc/000077500000000000000000000000001320454472400142235ustar00rootroot00000000000000mapproxy-1.11.0/doc/GM.txt000066400000000000000000003075551320454472400153060ustar00rootroot00000000000000POLYGON ((966096.776201051310636 6055988.988947953097522,965758.253629547310993 6056675.523899752646685,965507.116858318913728 6057382.789786672219634,965170.820676631177776 6058329.981786497868598,965201.767495072213933 6058926.868748364970088,965727.529450088739395 6059156.668010426685214,968355.893947208998725 6059340.975084476172924,969036.167355445679277 6059248.821054951287806,969407.08389877108857 6058834.719144906848669,969561.706671482417732 6058237.838562032207847,969621.151279565994628 6056561.897019822150469,966096.776201051310636 6055988.988947953097522)) POLYGON ((844722.129762893309817 6038003.416996375657618,844886.771289776079357 6039359.6524519296363,844883.988302506506443 6040886.14655645377934,844451.066802810528316 6041940.504500974901021,842379.299759656656533 6046620.29672465287149,841698.915031928685494 6047446.026775361970067,839379.796080231317319 6049649.681963190436363,838112.089719078503549 6050613.049036857672036,837338.975855518481694 6051393.539518415927887,837060.677128536743112 6051852.577586862258613,836380.403720297268592 6054930.433022130280733,836102.10499331553001 6057640.833879398182034,837864.737810535007156 6071666.89070952590555,841265.993532232707366 6084056.269137475639582,844265.608531149220653 6090787.100793438032269,848501.87175328633748 6101726.022982135415077,848718.2768433886813 6103019.994755994528532,848749.223661829833873 6104543.845848207361996,848532.707252236199565 6105838.242433298379183,848089.544359389226884 6106854.83488246332854,847543.299618065007962 6107870.383070412091911,844234.550393216894008 6111384.801155200228095,843183.249122166424058 6112586.170934516936541,842874.003576743649319 6113048.485942538827658,842598.821795502211899 6114307.367653651162982,842317.517442265641876 6116842.174815818667412,842564.869350809138268 6121053.132977366447449,842657.598486641189083 6121701.553738721646369,843456.427152574178763 6125699.982457838021219,843677.952939250622876 6126795.137632604688406,843894.358029352966696 6127304.078945056535304,846888.518373221741058 6134212.96345304697752,856417.912063088151626 6152958.738670173101127,862293.13214817433618 6162023.983153228648007,862818.782783699571155 6163047.270284013822675,862922.755188100854866 6164511.666676648892462,861952.939784310408868 6168817.717120355926454,861241.719557632575743 6171890.391949612647295,861241.719557632575743 6173426.557654925622046,862214.317948692594655 6177844.806597924791276,868539.268776585930027 6203192.545180082321167,877197.476131503703073 6218067.649002770893276,886319.551804558723234 6228185.899019806645811,900822.032426127232611 6241602.566658327355981,902120.685605721198954 6243810.510120262391865,902398.984332702937536 6244749.508355570957065,902831.905832399032079 6247896.26603374350816,903079.257740942528471 6248835.717206894420087,904099.723513043136336 6251985.347177073359489,905676.675419621635228 6256357.958717191591859,906047.814601926715113 6257157.639515234157443,911768.300594810163602 6266710.887503619305789,913499.98659359139856 6267887.916027321480215,915722.814185751369223 6268825.858716582879424,907872.118417045916431 6270855.711297339759767,896338.194656464271247 6275755.989449074491858,894977.647839988116175 6276746.399416313506663,883722.134125879500061 6283159.720492022112012,883134.478533983696252 6283159.720492022112012,870672.929456621990539 6282735.965771642513573,861334.448693464626558 6282121.686561178416014,853696.707110647461377 6282028.453174117952585,851826.650984809733927 6282489.705262280069292,849398.661571119446307 6285944.802159114740789,848903.846434541163035 6286604.556872135959566,833442.793677754234523 6302762.29488065559417,827382.115321006742306 6304748.388803269714117,826392.596367344376631 6304984.912437978200614,825279.290139920543879 6304938.08397757448256,821939.816735613741912 6304323.548402481712401,821012.191418833564967 6304087.213593147695065,820641.052236528485082 6303897.537566742859781,820314.997447994537652 6303547.993197460658848,820115.401601003250107 6302147.917899492196739,819496.910510154790245 6299405.472418908961117,819311.452238493482582 6299027.696545575745404,811704.546154626063071 6293924.445899857208133,811271.624654930084944 6293734.998314503580332,796119.928763057221659 6295436.097519584931433,789316.972041699569672 6296522.381704264320433,781400.931731900665909 6306734.950830462388694,771907.828196031856351 6312225.679078039713204,771505.85351477691438 6312415.542669532820582,770392.658606844255701 6312367.479847732000053,762971.433433617814444 6311940.891714941710234,762260.213206940097734 6311467.630839933641255,761765.398070364724845 6310805.928217408247292,761579.939798703417182 6310379.419154182076454,761456.263844433124177 6309906.245991516858339,761548.881660771090537 6309433.099402734078467,762507.565115483594127 6307256.149657763540745,763095.109387891017832 6306120.454536017030478,763620.760023416252807 6305457.809325370937586,763775.382796127698384 6305079.762153135612607,763775.382796127698384 6304606.716197683475912,763589.813204975100234 6304180.69105052575469,761889.07402463501785 6301770.019136881455779,761301.529752230388112 6301202.521913141943514,760466.633571279468015 6300871.300935101695359,759415.332300228998065 6301012.910778428427875,749736.770492698997259 6302951.9453643579036,749272.902174561866559 6303093.419912996701896,748901.874311748077162 6303329.897113116458058,748468.952812052099034 6304039.02798633556813,747510.269357342389412 6305978.758865231648088,747417.651541001629084 6306451.711522771976888,747479.433858392527327 6306972.892832051962614,747665.003449545125477 6307397.696473916061223,744387.201043135253713 6316534.62503161560744,734121.094963197712786 6336689.753037153743207,728709.743196245864965 6346333.76921774726361,728091.252105400082655 6347426.643065142445266,727163.626788620022126 6348283.277670696377754,724504.315473058610223 6350327.230531558394432,720051.535841327975504 6353515.009843508712947,714238.209393121185713 6355988.873172503896058,713712.44743810466025 6355941.75983668025583,708233.5247402403038 6353172.645689119584858,708301.206990643986501 6358273.930527319200337,707600.784754573367536 6364565.165742667391896,714763.860028646420687 6388603.87518335133791,719804.072613293305039 6392952.220967181026936,720577.186476853210479 6393381.569818319752812,721875.950975938467309 6393812.145389894023538,724226.239385056542233 6395622.973798915743828,726328.730607669102028 6412668.380007426254451,726297.783789227949455 6413196.030585668981075,726050.431880687363446 6413626.420818764716387,721566.705430515576154 6413626.420818764716387,715753.378982308786362 6413243.467299037612975,711949.925940373679623 6414776.767015567980707,703662.857767760870047 6420050.238133339211345,703087.002041887608357 6421256.722164375707507,696334.361730366479605 6425615.537431493401527,695777.764276400092058 6426239.028323297388852,693984.184640739578754 6428543.360317423008382,687428.802466905093752 6437620.701326671056449,687181.339238870423287 6438004.775765297934413,683563.45578809059225 6445217.59071833640337,682450.26088015793357 6448680.825750201009214,680533.116609715507366 6455950.580181440338492,680471.334292324492708 6456479.764425474219024,681089.714063681778498 6462020.534525237977505,682171.96215317340102 6466456.296481508761644,682419.425381208071485 6467420.717127968557179,682879.842795128352009 6468446.025074811652303,683470.726652258541435 6473065.559952608309686,686160.873466769815423 6484703.943305199034512,686346.443057922297157 6485091.590577069669962,687212.286057311459444 6486638.184156791307032,699179.131317589082755 6495537.219426227733493,706847.819718847400509 6500620.198686923831701,712244.811271486803889 6502433.140743269585073,709599.860170237952843 6522924.165743295103312,709507.019714917521924 6523409.569633388891816,709228.832307424163446 6524283.088707705959678,706260.275446439976804 6530550.063007588498294,705085.186901625129394 6531521.969930436462164,703260.771767014754005 6532688.759074470028281,702364.093268675613217 6532981.18210254330188,697385.663001419743523 6532931.656708871014416,695499.354229927179404 6548941.010718418285251,698034.989591215271503 6553617.610378972254694,698158.776864976854995 6554104.99633458070457,698065.936409656424075 6554592.235618531703949,697818.584501112927683 6554981.767011773772538,686532.012649074778892 6560736.164906280115247,674008.569934830884449 6572548.388279860839248,669230.292112019611523 6578469.565867424942553,668813.623257981380448 6586382.312027862295508,669401.167530388804153 6592794.846682395786047,669462.949847776908427 6593332.858471809886396,670823.607983744353987 6594410.221807206049562,673049.997799609671347 6595145.998728960752487,673668.377570966957137 6595145.998728960752487,674194.139525983482599 6594508.427381386049092,674688.843343067565002 6594361.736543165519834,675121.764842763543129 6594508.427381386049092,675461.957206627586856 6594753.153016392141581,675894.767386832274497 6595537.452529109083116,677286.372341237962246 6598427.734061955474317,677348.154658628976904 6598966.114496985450387,676977.015476323897019 6606025.652477322146297,676853.339522053604014 6606419.037251188419759,671070.95989228785038 6617166.538544823415577,670761.714346864959225 6617461.975834362208843,669865.035848523140885 6617804.455944258719683,662227.294265705859289 6618639.587373713031411,661763.425947571638972 6618639.587373713031411,660402.879131095483899 6618148.541728066280484,657681.67417865479365 6616675.580997887998819,656815.83117926272098 6616331.556941458024085,655857.259044044301845 6616478.640488278120756,653383.517319635488093 6624634.586756318807602,652795.973047228064388 6629011.455181543715298,652857.755364619079046 6629453.041968816891313,653105.218592650955543 6629797.616577193140984,657063.183087806217372 6632453.947612538002431,657557.998224381590262 6632602.737515078857541,657929.02608719537966 6632355.287225363776088,668257.025804015109316 6636194.175751654431224,678770.595111984410323 6644517.349501308053732,677966.645749477436766 6651221.892127034254372,677224.478704358567484 6651714.922815941274166,676760.610386221436784 6652454.613303555175662,676451.364840798662044 6653392.195125944912434,676234.848431205027737 6654970.700426151044667,675863.820568391238339 6658820.027227970771492,675894.767386832274497 6659412.501455130986869,675956.661023714463226 6659954.899998827837408,676420.418022357509471 6663905.790672333911061,676698.716749342158437 6664300.5401631873101,683841.754515072447248 6673691.236981774680316,692654.473323213285767 6685518.967087414115667,692716.366960095474496 6686063.100847483612597,692654.473323213285767 6704110.657425539568067,692623.637824263423681 6704657.283376707695425,691015.739099246566184 6712006.607999907806516,690428.194826839142479 6713248.359249284490943,685573.329194362391718 6720655.932496306486428,683130.534288394614123 6722595.828769395127892,682543.101335478480905 6723193.129806969314814,678399.455929682240821 6728916.545924234203994,678275.779975409037434 6729414.292051946744323,671132.853529167245142 6747259.51691164355725,662579.509134577703662 6754603.384045966900885,665783.284079606295563 6756895.984118143096566,666339.881533572566696 6757544.913390408270061,666432.610669404617511 6758093.88185746781528,666216.205579302273691 6760542.567708348855376,665845.066396997193806 6761392.431272137910128,663989.815763436956331 6765042.475906951352954,663742.463854893459938 6765443.416698913089931,668257.025804015109316 6769444.36770489718765,681677.147016598028131 6779909.436419078148901,688201.693691482651047 6781211.214384685270488,707002.331172067555599 6772697.979166365228593,719433.044750479515642 6773698.881650594994426,726637.864833603496663 6777454.89918074849993,734121.094963197712786 6781362.744317370466888,743181.165679882047698 6784570.07083138730377,744232.578270423808135 6784619.699097042903304,744912.962998151662759 6784068.932611936703324,745129.256768762832507 6783666.889495360665023,746056.993405034183525 6782715.635300729423761,748128.760448188171722 6781662.745861535891891,748561.681947884149849 6781712.175924683921039,751004.476853851811029 6783165.807122839614749,754405.955214532092214 6785923.677891809493303,755766.502031005336903 6787127.314730651676655,760188.334844297729433 6793899.38985523302108,760404.739934397279285 6794952.606078728102148,760312.122118059312925 6796056.922751138918102,759879.200618363334797 6798066.077498474158347,757034.31971164944116 6801430.485762349329889,756354.046303412760608 6801983.881148537620902,749025.550266018370166 6806505.417759709991515,749891.281945919152349 6813998.452031110413373,752456.083013794384897 6820866.479953240603209,761703.615752973710187 6821448.763039967976511,762816.810660906368867 6821549.742302386090159,763713.489159248303622 6821902.363806513138115,764610.167657587327994 6822858.23583247512579,764981.306839892407879 6823714.664112947881222,765290.552385315182619 6825326.583949914202094,765445.175158026628196 6827039.87337779533118,765599.797930738073774 6827543.680441894568503,768228.162427858333103 6831173.943298545666039,768568.243472233880311 6831475.797690022736788,775309.306556709227152 6834603.368340478278697,783936.567093188758008 6842122.70097856875509,785146.832597091794014 6843230.573374897241592,782730.531729935668409 6848889.730106042698026,782359.503867119085044 6850355.309895443730056,782359.503867119085044 6850961.922468082979321,782452.344322442309931 6852175.466523764654994,782823.372185256099328 6854348.619862906634808,783472.698775054537691 6856877.968839676119387,785705.656440874328837 6861406.672263115644455,786997.853090002201498 6864164.956917324103415,787152.475862716441043 6865278.779280195012689,786533.984771867981181 6870190.657328464090824,786132.01009061303921 6871610.133388453163207,785791.817726749111898 6872521.743362235836685,778215.969780814019032 6883678.85549449827522,777844.830598509055562 6883932.440025738440454,777442.967236745287664 6883730.375623905099928,776329.661009321454912 6881649.553733525797725,775432.98251098243054 6880635.186092871241271,774134.21801189717371 6879417.529398015700281,773763.190149083384313 6879164.087136859074235,773237.53951355535537 6879062.383805339224637,753014.461579614784569 6883577.095073698088527,751777.590717411832884 6884237.738637372851372,746458.968086289125495 6888451.348726056516171,746118.775722425198182 6889314.775330664590001,744603.71745272888802 6900036.995631285011768,744758.22890594904311 6900545.388899485580623,748345.276857781806029 6901918.595977255143225,752086.724943343433551 6902631.1083012111485,752457.864125648513436 6902885.291445909999311,752612.486898359842598 6903342.218692814931273,749118.279401850420982 6912300.861791249364614,748747.140219545457512 6913218.565124199725688,747947.087039214675315 6914259.428196498192847,748159.707266629207879 6914746.347592758946121,752148.618580225622281 6918364.231043037958443,753416.436260869726539 6918925.716031574644148,754931.605850057327189 6919281.343515948392451,767516.942201180616394 6919078.20452397223562,768351.838382131536491 6918873.786195711232722,768722.866244945325889 6918619.096319805830717,769557.762425893335603 6917600.050719144754112,769990.683925589430146 6916784.026854607276618,770361.823107894393615 6916478.773067839443684,782019.422822746331804 6915307.579476804472506,782668.749412544770166 6915307.579476804472506,783225.235547019867226 6915357.83548819553107,784060.243047459167428 6915765.214261484332383,784585.893682987196371 6916478.773067839443684,784987.868364239227958 6917345.21780888363719,785389.843045494169928 6918873.786195711232722,785791.817726749111898 6921066.508683867752552,786317.579681765520945 6931626.505381292663515,786503.037953426828608 6937296.198354857042432,786441.255636038724333 6940310.832676274701953,786533.984771867981181 6943479.739347828552127,786688.607544579426758 6945934.755589783191681,786997.853090002201498 6948850.052315478213131,787801.802452512085438 6950589.865589384920895,789373.967620987328701 6952886.740292699076235,794048.050400414853357 6962471.04226852580905,799242.997077264357358 6970829.684263568371534,800758.166666451958008 6975089.064843827858567,801252.981803027330898 6976525.750653848983347,801531.169210520689376 6977552.50908828061074,802644.47543794172816 6983356.405981163494289,802798.986891164793633 6985926.763937522657216,802304.283074077800848 7015639.987778211012483,802431.409932565060444 7028037.170917252078652,806509.710797267849557 7040804.894076263532043,807066.308251234120689 7041529.896038537845016,807777.528477911953814 7042098.615122922696173,809509.103157201898284 7042253.473267873749137,814858.672606762847863 7041943.573647095821798,815940.809376763179898 7041735.986788203939795,816744.870058764237911 7041271.265293912030756,817363.361149612697773 7040597.336934177204967,817796.282649305881932 7039717.608172768726945,818414.662420663167723 7039096.528331928886473,818785.690283477073535 7038837.790421664714813,819806.1560555776814 7038528.023848665878177,819156.829465782037005 7040287.50183436833322,818321.821965342736803 7041943.573647095821798,817270.520694289472885 7042823.735032731667161,816250.166241680039093 7043133.668953320942819,804499.726071504876018 7045464.617403412237763,799706.754075907403603 7046292.403648908250034,793305.994674787274562 7046137.280385302379727,789533.48845129320398 7045774.654274379834533,787399.827771257143468 7045411.858273643068969,785699.088590919855051 7045619.54111161455512,784864.192409968934953 7046086.194780667312443,784554.946864546160214 7046447.529939083382487,784060.243047459167428 7047225.630902728997171,781926.582367425900884 7050800.130723227746785,781400.931731900665909 7052146.603565408848226,781060.739368033828214 7053702.691632498055696,780998.957050645723939 7054272.282230850309134,780844.334277934278361 7058265.975242764689028,780968.010232204571366 7060081.190065468661487,781679.230458882404491 7065271.033392799086869,782730.531729935668409 7072178.272797952406108,782947.048139526508749 7073270.010297969914973,789038.67331471783109 7091530.79309658985585,789935.351813056971878 7093250.666907691396773,790584.78972234367393 7093875.997597998008132,799799.594531230744906 7102686.137634829618037,804499.726071504876018 7106963.556489115580916,805643.867797875776887 7107799.287541255354881,812168.414472760399804 7110774.477065208368003,817208.627057410078123 7111401.192933864891529,829391.988727278425358 7111191.780851698480546,831278.297498771105893 7111088.580705529078841,831834.894952737377025 7110983.87810714635998,833164.49495077249594 7110357.195277690887451,835823.80626633099746 7108372.776341564953327,836967.947992704692297 7108216.434515702538192,843987.198484676307999 7110044.624276876449585,856974.509517054422759 7113594.525239326991141,873115.835682078148238 7116207.350540086627007,877475.88617797952611 7116834.496813284233212,880258.76212831994053 7117200.386450259946287,884897.111351203173399 7116834.496813284233212,888515.106121476972476 7116415.390554669313133,891483.551662969985045 7115423.769712859764695,892813.151661004987545 7114796.735235529951751,893493.425069241668098 7114221.464461803436279,893740.77697778516449 7113803.812864502891898,893864.564251546747983 7113281.638339615426958,893215.126342257135548 7111976.446735521778464,893060.50356954568997 7111454.39275648444891,892967.774433716433123 7110879.176893725059927,893029.556751107447781 7109052.297840813174844,893307.855478089186363 7108060.283682477660477,895688.8680666659493 7101590.154444455169141,897018.4680647009518 7099713.984987495467067,904347.075421586632729 7091895.702960564754903,908057.688008198398165 7088146.707605058327317,908305.039916741894558 7087729.116622253321111,908645.120961114531383 7086845.463162605650723,909016.371462910901755 7085335.889893116429448,909109.100598740042187 7084137.954972284846008,909294.670189892640337 7081121.43697783537209,909263.723371451487765 7080599.913906796835363,908737.961416435078718 7079872.835018787533045,905614.89310223062057 7078155.860175435431302,904316.128603145480156 7077531.767976427450776,902027.845150397974066 7076804.970225897617638,899956.189426735276356 7076387.975976714864373,897606.012337108375505 7076545.001310390420258,897389.607247006031685 7076128.020723730325699,897420.554065447067842 7075557.047399014234543,897915.369202022440732 7072282.464017132297158,898317.232563786092214 7070828.212971966713667,898997.505972022772767 7069009.231169515289366,899492.209789109765552 7068179.074728609994054,901440.300877990550362 7065634.737307622097433,913345.363820877159014 7058109.311021456494927,913778.396640064427629 7057902.608605723828077,915386.295365081168711 7057488.099642520770431,916654.113045728066936 7057435.44734931923449,917736.249815728398971 7057590.604575986042619,918200.229453353909776 7057747.445285513065755,921849.059722577570938 7060495.834362633526325,922745.626901425421238 7061533.287040217779577,923147.601582680363208 7062415.62071280926466,925033.910354172927327 7066932.320414069108665,925405.049536478007212 7067866.666712540201843,925621.454626580351032 7069685.386689404025674,926147.105262105586007 7075816.796803230419755,926239.945717426133342 7077740.292421886697412,926147.105262105586007 7078884.279418115504086,925590.619127630488947 7080237.021495475433767,924910.345719391014427 7080808.519020344130695,924322.801446983590722 7080912.823634508065879,923209.606539050932042 7080757.022634019143879,920921.434405797510408 7079924.138425786048174,919900.96863369399216 7079717.054444113746285,916994.194090100820176 7079508.478032264858484,916499.490273013827391 7079664.254510826431215,916375.702999252243899 7080184.218101002275944,916406.761137184570543 7082057.268294009380043,916932.411772709805518 7086845.463162605650723,917612.685180946486071 7092155.988093191757798,919622.558587221079506 7095334.293916501104832,920921.434405797510408 7096636.932894899509847,922096.411631118273363 7097367.052222860977054,924631.935672918101773 7097941.099730957299471,926518.244444410665892 7097941.099730957299471,928559.175988614675589 7097315.448158889077604,945597.180651977425441 7086323.750923176296055,948256.380648047546856 7078572.947358086705208,947143.297059606062248 7077064.760488103143871,946648.370603539515287 7076285.228066160343587,945813.474422588595189 7074517.573468481190503,945226.041469672345556 7073114.5463876593858,944916.907243740744889 7072126.832395181059837,944576.714879876817577 7070464.458069658838212,944298.416152892285027 7067400.227594263851643,944452.927606115234084 7060755.091434504836798,944576.714879876817577 7058472.500379336066544,944947.631423199432902 7053909.098187626339495,945195.205970722483471 7052303.336855340749025,945968.097195300040767 7049919.08594308141619,946555.641467707464471 7048779.2637355979532,946957.727468453580514 7050333.200269604101777,947112.350241165026091 7052199.407411879859865,946864.998332621529698 7062000.689365667290986,946772.157877301098779 7063765.558625495992601,946060.937650623265654 7066776.980579756200314,945782.750243129907176 7069009.231169515289366,945999.155333232367411 7070775.472153359092772,946401.018694996018894 7072386.656604175455868,946926.780650012427941 7073789.554991121403873,947483.489423470105976 7074569.029370416887105,952991.355188938905485 7078914.233838524669409,953513.109642288065515 7080028.431411072611809,953822.466507202130742 7081068.440429079346359,953822.466507202130742 7082265.91174142062664,953729.626051881583408 7082786.045919827185571,952647.377962387283333 7085491.593504733406007,947521.894647792680189 7097992.707870285958052,945627.904831436113454 7102632.996695402078331,944916.907243740744889 7104459.897587233223021,944607.550378826679662 7106024.779485984705389,944329.362971333321184 7108269.612939316779375,944236.633835501270369 7109991.621879470534623,944360.198470283183269 7111715.32144579757005,944855.124926349846646 7113908.552774738520384,951565.018553404486738 7135824.783748600631952,954502.739915438811295 7142060.860401595942676,955151.95518574595917 7143422.933597770519555,957471.185456934501417 7146989.367644968442619,957811.377820798545144 7147356.655891079455614,958677.220820187707432 7147775.89523209258914,963191.671449818066321 7149613.257017200812697,964645.058721614652313 7150084.565403321757913,966314.962403004872613 7150243.059795869514346,966685.878946327487938 7149927.396513876505196,969190.678808668628335 7146675.345021565444767,969747.276262635015883 7145888.738239218480885,969932.845853787497617 7144838.666670678183436,970118.415444939979352 7144419.583237870596349,971602.526896195253357 7143266.085989373736084,974756.653348331921734 7141169.60999807715416,976890.314028365304694 7140016.587235193699598,979302.273435383103788 7139125.75607584323734,985517.463245353545062 7137973.031555734574795,991578.252921595121734 7137657.858303172513843,1004163.589272718410939 7141169.60999807715416,1013594.68785221374128 7144314.43831682484597,1016223.05234933400061 7144838.666670678183436,1025592.591250423691235 7144629.877279696054757,1026241.806520730839111 7144576.64253150112927,1032611.841742392396554 7143476.349318630062044,1033044.763242088374682 7143266.085989373736084,1033415.902424393454567 7143003.925280393101275,1034621.937787646544166 7141640.41469375230372,1037528.378372768871486 7138077.9034807048738,1037992.358010394382291 7137292.340976368635893,1040991.639050837256946 7132001.777792084030807,1044114.930004024063237 7124989.101297973655164,1047392.621090942760929 7119290.514422331005335,1048165.734954502666369 7118140.67815310228616,1062853.673847729805857 7096741.44597904663533,1064461.683892237721011 7094448.479153910651803,1065667.607936002546921 7093094.6232705200091,1066749.856025493936613 7092155.988093191757798,1076768.610196887981147 7086428.128103362396359,1079335.192376617342234 7085440.254069899208844,1087006.552445653825998 7084213.827558961696923,1091178.250363133382052 7082525.880916220135987,1092198.716135233873501 7082213.094838201068342,1093342.746542116394266 7082108.773123124614358,1093868.397177644539624 7082213.094838201068342,1094270.594497879035771 7082473.062281691469252,1094548.781905372394249 7082837.555482001975179,1094703.293358592549339 7083306.214213330298662,1094703.293358592549339 7083930.760939756408334,1094301.318677337840199 7084762.567210190929472,1093682.82758649205789 7085440.254069899208844,1093250.128725778544322 7085700.327330800704658,1089137.430138420546427 7087521.828729171305895,1088055.182048928923905 7087782.157704432494938,1084372.399335012305528 7088199.564046370796859,1082489.096189774340019 7088302.653864267282188,1079737.167057872284204 7088823.184199671261013,1077789.075968991499394 7089499.718525432981551,1075748.144424787489697 7090749.837431290186942,1065914.959844543132931 7097315.448158889077604,1064337.896618476137519 7098827.492764323949814,1062729.886573968222365 7101068.784804313443601,1050608.529860470443964 7120963.165060877799988,1049804.580497960560024 7122687.938866849988699,1048320.246407722821459 7126926.083674090914428,1047268.833817181177437 7130903.234233390539885,1045784.722365923109464 7136401.628332003951073,1045630.099593211663887 7136872.148966987617314,1039476.469461649656296 7142322.989810078404844,1038363.274553716997616 7143160.956423830240965,1033755.983468766091391 7146413.071114580146968,1032859.193650935892947 7146832.260357725434005,1027973.603839 7148667.891552069224417,1026458.434249812853523 7149140.465692413039505,1024726.636931543238461 7149454.775268171913922,1008894.66763143078424 7150871.401418307796121,1001102.303275902173482 7149559.988660784438252,1000019.943866919376887 7149402.830214342102408,998226.586870241211727 7150295.010378590784967,996030.921233834582381 7152604.228509044274688,993804.754056948819198 7154966.103760926052928,992289.361828781664371 7156646.324924129992723,990712.409922203165479 7158957.402476713061333,988857.159288642811589 7162793.485208897851408,985177.27088148961775 7171839.754538405686617,983754.941747622564435 7175523.104603540152311,983631.377112843445502 7176049.410942767746747,984126.080929927527905 7176839.031136461533606,984806.35433816711884 7177471.011740268208086,985641.250519115128554 7177945.314820511266589,986970.85051715024747 7178525.436537722125649,988609.807380099315196 7178735.125026864930987,989691.944150102557614 7178525.436537722125649,993711.913601628388278 7177207.711922005750239,995814.627463223412633 7175943.843619584105909,997484.308505631168373 7174365.051177844405174,998659.285730954841711 7173523.574670993722975,999525.24004983517807 7173681.214410616084933,1001937.199456853093579 7175102.3880481319502,1002215.386864346452057 7175470.98759526014328,1003112.176682176650502 7178471.782965579070151,1003297.746273329248652 7179630.440107084810734,1003792.450090413331054 7186270.550336133688688,998659.285730954841711 7195130.659151518717408,986135.95433620212134 7193653.819904461503029,984899.194793487549759 7195025.025738888420165,984589.837928573484533 7195394.558890007436275,982920.045566674438305 7198244.821928518824279,982394.39493114920333 7199618.158659620210528,981033.848114673048258 7203368.160962206311524,981435.711476436699741 7206221.504865012131631,982208.825339996721596 7209499.752867390401661,984218.921385250869207 7217750.802082504145801,984373.432838471024297 7218279.940165244042873,984651.620245964382775 7218650.758195608854294,985517.463245353545062 7219075.333932409994304,988918.941606033826247 7226066.027831118553877,982951.10370460676495 7225905.809201662428677,967984.75476490391884 7221509.464594282209873,967830.131992192473263 7221033.862608561292291,967490.050947819836438 7220715.539887227118015,966252.957446634303778 7220715.539887227118015,965077.980221310746856 7220873.937186271883547,963531.863813681993634 7221351.054482883773744,962202.263815646991134 7222038.855851247906685,960872.663817611872219 7223204.137934166938066,958677.220820187707432 7227442.70757070183754,958151.570184662472457 7228821.155107823200524,957687.590547034051269 7230304.615860507823527,957378.456321102450602 7231894.867254277691245,957254.669047340983525 7233008.922166247852147,957285.615865782019682 7233645.43230783380568,959790.415728120366111 7242188.90248944144696,961120.015726155368611 7244897.11620903853327,961676.501860630465671 7245639.758673563599586,962480.673862122814171 7246118.192264212295413,963006.324497648049146 7246224.706031632609665,976117.200164808076806 7248349.355470561422408,982208.825339996721596 7249252.634919972158968,982734.47597552195657 7249093.663966599851847,983105.61515782692004 7248775.349144220352173,983816.724065013462678 7247552.520578687079251,984126.080929927527905 7247234.459549023769796,984713.625202335068025 7247127.932069378904998,985795.873291826574132 7247339.45842678565532,989290.080788338789716 7248987.11123238876462,1001287.65022807510104 7256217.615993781946599,1002431.903273937175982 7257813.129163457080722,1003545.098181869834661 7260047.431678821332753,1003699.720954581280239 7260579.429380930960178,1004039.913318445207551 7264092.631960514001548,1003761.503271972294897 7265104.498034475371242,1002493.68559132819064 7267074.743714639917016,998288.369187629432417 7273308.907211754471064,997453.584326172596775 7273789.224814109504223,995969.138916443567723 7274322.15661040134728,995227.083190815988928 7274907.713343700394034,993155.204828170826659 7276827.825220025144517,992536.936376304831356 7277467.90317939966917,985270.111336810165085 7288621.415203700773418,981188.359567893203348 7297759.418607238680124,980167.893795792595483 7301076.365233298391104,974540.248258229577914 7306105.490519311279058,972313.858442364260554 7307925.726875032298267,968139.26621812407393 7312906.611797288060188,963531.863813681993634 7323306.254845689050853,962758.972589104552753 7325130.505420353263617,962418.780225240625441 7326042.887228777632117,962047.641042935545556 7328243.628665839321911,961862.071451783063821 7330498.341622419655323,954391.309105155989528 7332861.213045955635607,954818.108032856835052 7334956.341912056319416,955182.456726224976592 7336266.034941651858389,951292.953717906260863 7336793.572025833651423,933982.772899551317096 7336084.587456043809652,924230.072311154333875 7334740.885496271774173,924013.444582069525495 7334258.354342968203127,923735.145855087786913 7332485.139554671943188,923333.171173832844943 7328028.550799731165171,923085.81926528934855 7321108.256480649113655,923240.442038000794128 7319981.40200088173151,923951.66226468142122 7318105.47383538633585,924013.444582069525495 7316871.571559986099601,923797.150811458355747 7311620.69412637501955,923766.09267352882307 7311352.853393594734371,922652.89776559616439 7311085.021844476461411,921972.62435735668987 7312371.044175134040415,921849.059722577570938 7312906.611797288060188,921787.054766204208136 7314032.252794926986098,921787.054766204208136 7318318.92744945269078,921879.783902036258951 7326633.877022141590714,922034.406674747704528 7329961.565755023621023,922962.254630510346033 7341460.550352157093585,923456.958447594428435 7343718.894512126222253,924353.636945933452807 7347591.83683767169714,925157.69762793451082 7350175.772392796352506,932115.165802515111864 7368338.058034271933138,933197.413892006617971 7370657.641546722501516,934341.444298889255151 7372169.822742181830108,935640.208797974395566 7372871.215474430471659,936320.482206211076118 7372817.768250113353133,940340.451657736906782 7372169.822742181830108,941700.998474213061854 7371521.931184414774179,942102.973155468003824 7371197.22838369756937,939350.821384586161003 7366556.87738436087966,937495.45943153463304 7364400.040565061382949,936227.641750890645199 7363698.05190394166857,935207.287298278417438 7363428.416558965109289,934805.312617023475468 7363158.790551952086389,931960.543029800872318 7360733.546821037307382,929950.558304037898779 7356638.005225617438555,929764.988712885417044 7355452.870856042951345,929826.882349767605774 7352436.643732541240752,936934.965795389376581 7347658.284554575569928,937139.125741504249163 7345947.678451950661838,937454.493858922272921 7345107.718666777014732,938029.6816678509349 7344462.849014785140753,938493.5499859880656 7344140.046917153522372,941406.447101573576219 7342590.318255020305514,942389.732163752312772 7342364.390295166522264,945154.129078621626832 7342590.318255020305514,957161.828592017642222 7343624.404592018574476,957621.912047466961667 7345107.718666777014732,957658.981437903246842 7345914.558655360713601,964532.737355404882692 7345017.666096045635641,966933.342174362158403 7342267.012910942547023,968417.676264599896967 7341083.868388279341161,970644.066080465214327 7340760.625508707948029,974571.083757179439999 7341943.527390172705054,975498.709073959616944 7342267.012910942547023,978034.233115756534971 7343664.291590719483793,982827.31643084238749 7345171.240245669148862,984209.125270059099421 7345132.89484038297087,992969.635237018344924 7344041.098261154256761,995319.923646136419848 7343556.442263395525515,1020675.943300555925816 7335708.938807819969952,1026829.350793135468848 7333505.897395013831556,1027880.763383679906838 7331948.226972453296185,1027262.272292831446975 7329746.247247450985014,1027880.763383679906838 7327867.777686550281942,1028932.175974221667275 7326311.039195445366204,1030107.264519033720717 7324862.394013115204871,1040033.178235107217915 7324809.275050834752619,1041826.757870770641603 7325559.34828888066113,1042908.894640770973638 7327170.424006732180715,1042939.952778703300282 7327546.434624436311424,1044022.20086819480639 7328888.124796369113028,1045568.317275823559612 7329961.565755023621023,1048041.947680738288909 7329585.438737388700247,1051452.442920171655715 7328053.478658375330269,1051072.398178604664281 7326955.182506436482072,1050577.471722538117319 7324648.568246448412538,1050763.041313690599054 7323574.505929716862738,1066255.152208409970626 7328083.044361399486661,1075408.063380411826074 7325506.417759177275002,1089322.999729573028162 7320464.595506404526532,1100918.705807545688003 7321000.718546087853611,1101660.761533173266798 7321536.878407536074519,1102712.17412371491082 7321750.425862652249634,1103918.209486968116835 7321429.334568263962865,1106051.870167004177347 7319660.383457952179015,1108958.422071617795154 7317086.535190312191844,1110164.346115379594266 7315639.40769452508539,1110597.267615075688809 7314781.291281790472567,1117307.38388111279346 7298302.840912542305887,1117400.22433643322438 7297180.810660258866847,1117647.576244976604357 7290976.075036756694317,1116812.680064025800675 7276028.089293680153787,1114400.720657008001581 7270963.96801414899528,1113906.016839923802763 7270164.830726452171803,1113230.864128261106089 7269425.806811788119376,1109916.994206836214289 7266862.684137949720025,1101289.844989850651473 7261963.712189627811313,1098847.050083882873878 7261111.463199438527226,1096496.761674762004986 7261431.620509636588395,1095971.111039236653596 7261324.899952916428447,1095754.705949134426191 7260845.537568158470094,1095785.764087066752836 7260260.839454960078001,1096713.389403846813366 7258610.816020862199366,1098011.931263949489221 7257281.319188251160085,1098754.209628559648991 7256695.355018301866949,1099650.888126898556948 7256376.922925828024745,1101320.680488797603175 7256590.230786599218845,1113936.963658364955336 7260487.471848980523646,1124481.257145792944357 7263294.195305222645402,1125532.669736334588379 7263507.691520811058581,1127326.249371998012066 7263453.646147929131985,1154197.438615605002269 7253080.580053403973579,1181068.850498191546649 7238738.034973569214344,1183666.268176870886236 7236722.371081725694239,1184315.483447178034112 7236085.611413025297225,1184779.463084803428501 7235236.169635098427534,1185119.655448667472228 7234281.994059813208878,1185799.928856906946748 7233061.421799932606518,1188799.209897349821404 7229562.08716104272753,1189541.488261959748343 7228979.906747962348163,1190067.02757799369283 7228767.348188762553036,1191860.495894165942445 7228502.325871454551816,1198354.207070097792894 7228450.047507165931165,1200023.99943 7228662.406676786951721,1200889.842431388795376 7229032.188639608211815,1202497.741156405769289 7230039.732493661344051,1204600.455018 7231683.565900018438697,1205311.675244678510353 7232319.968597581610084,1206239.411880949744955 7233327.934445875696838,1207043.249923968454823 7234547.211533790454268,1211743.492783733876422 7239003.212006230838597,1217464.090096108615398 7243144.039933386258781,1218484.555868209106848 7243409.557066842913628,1219133.882458007428795 7243356.988166468217969,1222226.00395377073437 7242560.660197351127863,1219566.803957703523338 7242985.001576201058924,1219597.63945665047504 7242506.759016290307045,1219937.720501026138663 7241869.529856295324862,1220370.642000719206408 7241658.150929920375347,1223061.011454212944955 7240914.547535751014948,1226400.484858519630507 7240542.67671681381762,1229492.828993265517056 7240490.127020155079663,1232739.684581234352663 7241764.604162098839879,1234996.909896049415693 7210080.682127042673528,1235027.856714490568265 7208865.195704294368625,1234873.122622287832201 7207648.565089578740299,1231688.271990692475811 7204266.472205441445112,1230698.753037030110136 7203316.049505141563714,1211990.844692274462432 7187851.730732556432486,1210877.649784341687337 7187007.484003847464919,1210011.695465461350977 7186744.055035966448486,1206641.275242713512853 7187218.916797946207225,1205033.376517696771771 7187534.747989940457046,1202405.012020576512441 7187113.19969129934907,1198508.829842812148854 7181737.164014083333313,1197581.204526031855494 7179999.441153919324279,1197241.012162165250629 7178998.286289248615503,1197210.065343726892024 7177734.130786491557956,1197395.634934879373759 7176681.328221855685115,1198168.637478948105127 7174365.051177844405174,1198941.751342505216599 7172575.529080390930176,1200456.920931692933664 7170260.268712848424911,1201137.194339932408184 7169628.86597903072834,1202373.953882644185796 7168999.028689204715192,1204724.24229176226072 7169051.102925105020404,1209084.070148681290448 7168999.028689204715192,1210104.535920784575865 7168630.542287633754313,1211155.948511326219887 7167841.933216452598572,1211588.870011019520462 7166999.827466639690101,1211681.710466342745349 7166474.124936113134027,1212238.196600817842409 7162162.6800138046965,1212330.925736649893224 7160849.389651210978627,1209331.422057224670425 7154441.202508762478828,1204313.807329207658768 7149361.085527870804071,1213258.662372921360657 7157119.566087580285966,1213876.04026886029169 7162081.893670342862606,1220587.269729804247618 7166420.553329393267632,1225967.563358823768795 7169261.865072852931917,1230680.496640538796782 7171549.366031790152192,1235058.692213437519968 7172734.476010415703058,1244211.603385442402214 7173155.066185119561851,1244799.036338358651847 7173049.537622364237905,1245603.208339850883931 7172523.431558800861239,1245912.342565782368183 7172154.780390333384275,1246407.046382869360968 7168999.028689204715192,1251694.722195548238233 7159273.427151574753225,1252220.484150564763695 7158537.56880714930594,1252591.623332869727165 7158223.086418267339468,1253828.382875584298745 7157487.326021943241358,1254787.066330296685919 7157224.88170554023236,1270464.524177183397114 7154966.103760926052928,1278040.48344261245802 7162636.252199453301728,1279308.301123256562278 7168840.157054787501693,1279431.865758035564795 7169419.419612685218453,1282122.235211529303342 7176208.427946347743273,1282895.126436106860638 7176734.780058096162975,1283390.052892173407599 7176681.328221855685115,1285152.574389901710674 7175154.31311078555882,1286265.769297834252939 7175259.869858453050256,1288399.429977870313451 7176312.672061745077372,1289048.645248177461326 7176998.064074473455548,1294150.862789195030928 7184478.322435260750353,1294583.78428889112547 7185426.473090276122093,1294676.62474421155639 7186586.153711558319628,1294367.267879297491163 7187534.747989940457046,1293686.883151569403708 7188748.272563060745597,1293377.748925635125488 7189696.937497153878212,1293501.313560416921973 7190277.925429258495569,1294274.427423976827413 7191544.096020502969623,1295696.979196823667735 7193442.2142344256863,1299778.508326758164912 7198614.696375775150955,1300520.786691368091851 7199142.548345421440899,1301417.465189707232639 7199512.274898992851377,1302468.877780251670629 7199670.435727526433766,1307725.60677448939532 7199829.930455600842834,1334287.773111656075343 7203051.883962181396782,1350274.365184477064759 7206433.454503217712045,1372229.129117198754102 7224634.094384512864053,1374084.491070250282064 7226754.242160625755787,1375475.873385676182806 7228821.155107823200524,1384474.161784966941923 7244577.824505384080112,1389329.027417443692684 7254727.716779392212629,1394492.915956363780424 7260472.720428659580648,1396008.085545551264659 7257547.123913083225489,1406923.740854266798124 7255643.60175824444741,1409354.067977264523506 7255101.026542757637799,1411617.638503054622561 7254877.42143162433058,1425260.51001671468839 7254409.364598935469985,1435217.259231738047674 7254568.634789508767426,1436979.780729469144717 7254196.116068713366985,1438216.874230654677376 7252760.763638325966895,1438649.795730347745121 7251963.480407816357911,1438711.578047738643363 7251432.067072619684041,1438587.790773977059871 7250846.731227099895477,1438340.438865433679894 7250421.773014653474092,1437969.522322111064568 7250103.595606505870819,1433856.712415261892602 7248562.445058114826679,1420251.578208982711658 7248773.244942585006356,1417012.514985369052738 7250747.425557676702738,1415193.665825297590345 7251954.677444892004132,1413167.317134388489649 7251954.677444892004132,1411093.101062438683584 7250199.257931883446872,1409896.527855900814757 7247950.927905458025634,1407437.035026313271374 7248934.504940495826304,1387659.235055544646457 7245480.669715638272464,1387288.095873239682987 7245215.273682760074735,1383237.40224225516431 7241020.99068021401763,1376650.85061 7228608.600845373235643,1376465.503658826928586 7228078.766654878854752,1376403.498702456476167 7227390.245488774962723,1376990.9316553727258 7220504.539058900438249,1377300.288520286791027 7220133.824135776609182,1378753.675792083377019 7218915.257800880819559,1380763.549198355060071 7217592.467723579145968,1381784.014970458578318 7217221.699624046683311,1385371.062922291224822 7217063.375924291089177,1386267.630101138958707 7217433.946025569923222,1386576.875646561849862 7217804.724138679914176,1386205.847783748060465 7218069.195269756019115,1384412.379467578837648 7218386.076940513215959,1383391.9136954753194 7218756.899979280307889,1382371.559242866002023 7219709.191205854527652,1381938.526423678733408 7220556.955176550894976,1381845.797287849476561 7221669.593584612011909,1382093.149196390062571 7222834.630235471762717,1382649.857969847740605 7224369.402189965359867,1383391.9136954753194 7225694.859657066874206,1388988.835053579648957 7232902.587620206177235,1389576.379325987072662 7233645.43230783380568,1397028.551317651756108 7238738.034973569214344,1400615.599269484635442 7240224.899739692918956,1405377.624446638161317 7241073.352828699164093,1411840.277484643273056 7241710.517330437898636,1420962.241838207002729 7241020.99068021401763,1449812.580308627104387 7246224.706031632609665,1459831.33448002114892 7223310.150575171224773,1460264.255979717243463 7222463.614707323722541,1461501.015522428788245 7221669.593584612011909,1470808.772106127580628 7218015.46194199565798,1478570.078323723981157 7215105.645912212319672,1479219.516233013477176 7214418.260597705841064,1489361.94635867793113 7203262.226720285601914,1497618.179032343672588 7188748.272563060745597,1497896.366439837031066 7188378.861425830051303,1500555.67775539541617 7186955.48075735103339,1502194.634618344716728 7186480.44509894400835,1502782.178890752140433 7186480.44509894400835,1500586.735893327742815 7192335.081258613616228,1500648.518210718873888 7192862.512219509109855,1502318.199253126513213 7193706.057368128560483,1525818.968273985665292 7202628.172588893212378,1526375.677047443343326 7202681.991035751067102,1527117.732773070922121 7202364.220307854004204,1534971.879445990547538 7190963.014647900126874,1536177.914809243753552 7186744.055035966448486,1536023.29203653219156 7185478.655997902154922,1534662.745220056036487 7179630.440107084810734,1539517.499533041613176 7167577.629552058875561,1543661.033619349589571 7152184.732686701230705,1543289.894437044393271 7151868.991752908565104,1538558.927397820400074 7146780.521382455714047,1537043.757808635709807 7143843.474205957725644,1536889.135035921353847 7143266.085989373736084,1537043.757808635709807 7142793.674527396447957,1537662.248899481492117 7140907.707548822276294,1553927.251018778188154 7127657.495613758452237,1583395.969260555692017 7113699.262379672378302,1586024.333757672924548 7112967.635678436607122,1588003.260345506481826 7113125.38755396194756,1589155.528394709108397 7113367.756216426379979,1592950.855113812489435 7086428.128103362396359,1604476.875190550461411 7045943.939881308935583,1604422.996557005913928 7039511.383423914201558,1602075.491135157411918 7009709.191664970479906,1601052.576334258075804 7004555.263777083717287,1600557.649878191528842 7002236.442947717383504,1597712.880290968809277 6992765.469048694707453,1596568.849884086288512 6991325.673744099214673,1591835.545135554857552 6986963.059607216157019,1590508.06020784471184 6985977.650316850282252,1583952.566714521963149 6981198.891291120089591,1583179.45285096205771 6980685.168733023107052,1580767.604763435432687 6980171.664120332337916,1577273.397266926011071 6978065.845362557098269,1575108.789768448797986 6976115.453849482350051,1574737.87322512618266 6975139.882227128371596,1574675.979588243877515 6974472.999075345695019,1575077.954269498819485 6957655.790566356852651,1580334.683263739338145 6949873.079349939711392,1582344.556670011021197 6949157.830819537863135,1597310.905609713867307 6937193.756036232225597,1598671.563745678402483 6935967.492577820084989,1601145.305470087332651 6933362.581356229260564,1608380.961052161175758 6923157.56759122107178,1611411.411550024757162 6919281.343515948392451,1612122.520457211416215 6918670.84101480524987,1614781.943092263769358 6916937.94227206800133,1616173.325407689902931 6916274.604389509186149,1617224.737998231546953 6915969.36998001113534,1619420.180995655944571 6914848.503546574153006,1620997.24422172550112 6913829.936624718829989,1623842.013808945426717 6911537.075243853963912,1625747.024254891555756 6909664.58398752193898,1629531.664302881807089 6904715.904021199792624,1629748.069392984267324 6904309.268684247508645,1629871.856666745850816 6903852.286422338336706,1629407.877029120223597 6890532.468625165522099,1629315.370532270753756 6889314.775330664590001,1627861.983260474167764 6886877.017545705661178,1623903.79612633632496 6880381.705474451184273,1623316.363173420075327 6879721.195876054465771,1621615.623993080109358 6878960.681759269908071,1620718.94549474096857 6878707.071304792538285,1619543.968269417295232 6878048.159688966348767,1619265.558222944382578 6877693.074526689015329,1619018.206314403796569 6877287.803667929023504,1618276.150588773190975 6874043.269169731065631,1617966.793723859125748 6872370.135668253526092,1617966.793723859125748 6871813.353086967952549,1622821.548036844702438 6852832.082926006987691,1623223.522718099644408 6852024.059242190793157,1624831.421443119412288 6849900.107516416348517,1625449.912533964961767 6849295.030145579017699,1631479.755391765385866 6847324.386296740733087,1635561.284521699883044 6846262.868285267613828,1636829.324841326335445 6845404.226609332486987,1637571.380566954147071 6844344.415681592188776,1637880.737431868212298 6843485.796385199762881,1643160.843519174261019 6812767.270493966527283,1638460.600659408839419 6790203.28558855317533,1638239.965428659226745 6789488.355823687277734,1635561.507160682231188 6781863.715223235078156,1634974.074207766214386 6781161.787376498803496,1630613.912392373429611 6776102.705505971796811,1630304.778166441945359 6775752.073431774973869,1629748.069392984267324 6775251.480030929669738,1627057.922578473109752 6773748.443409530445933,1625821.163035761332139 6771947.293259502388537,1625635.593444608850405 6771446.934640874154866,1625326.347899183165282 6769694.571471702307463,1625171.613806980429217 6767894.321103473193944,1625202.56062542158179 6767392.95179845765233,1625790.104897829005495 6765642.724229828454554,1632314.651572713628411 6751202.091095076873899,1632716.626253968570381 6750402.712621480226517,1633984.555254103848711 6748606.913911815732718,1636179.998251528013498 6746460.526186366565526,1638158.924839361570776 6745013.453616759739816,1639550.529793767025694 6743866.862542822025716,1642550.033473192481324 6740675.396369541063905,1642673.598107974277809 6740226.318300979211926,1642797.385381735861301 6738780.354915716685355,1643199.2487434996292 6731207.290806321427226,1643230.306881429161876 6730161.057962676510215,1643075.46146973525174 6728916.545924234203994,1642457.193017872050405 6727572.407093834131956,1638066.084384038113058 6720010.477313368581235,1640416.37279315921478 6715237.129572517238557,1641158.428518786793575 6714689.793603885918856,1651702.94464519713074 6709969.451130202040076,1661010.367270422168076 6706990.371132547035813,1664350.063313711434603 6705103.152863139286637,1664690.144358084071428 6704805.963397518731654,1664875.60262974537909 6704409.251106985844672,1667071.15694666095078 6699644.314919441007078,1667770.020709862699732 6686657.553156084381044,1666638.235446967650205 6685023.893922097049654,1666638.235446967650205 6684429.560527901165187,1667627.754400627221912 6681360.536583973094821,1668246.24549147579819 6679677.696827493607998,1669297.435443037888035 6678291.418456421233714,1670874.609988596057519 6676757.202597983181477,1673131.835303411120549 6673592.078366609290242,1673843.055530088953674 6671961.029484928585589,1674121.242937582312152 6669488.643230488523841,1673379.187211954733357 6663708.956490409560502,1669173.870808255858719 6642300.804866869002581,1667442.184809477534145 6635161.283417917788029,1666050.802494051633403 6630781.132678112015128,1665865.232902899151668 6630289.359884395264089,1662123.562178354943171 6621735.071217895485461,1658320.220455911010504 6614122.615373594686389,1650620.473916723160073 6601319.031949603930116,1650218.499235468218103 6600437.314745385199785,1650311.339690788649023 6599457.426228715106845,1650682.478873093845323 6597594.654527955688536,1650311.339690788649023 6595928.924962686374784,1647992.109419602900743 6589612.095082506537437,1647713.922012109542266 6589319.066784811206162,1644899.987923839595169 6589220.923139488324523,1643817.739834345178679 6589220.923139488324523,1640818.236154919955879 6590199.592749860137701,1638375.441248952178285 6591130.088807290419936,1631016.109712610719725 6594313.251564943231642,1627676.413669321686029 6595782.385126316919923,1627398.003622848540545 6596125.373557104729116,1627274.438988066744059 6596565.371950143016875,1628758.661758813075721 6600289.283045599237084,1628418.469394949264824 6607791.793946257792413,1624955.20871688099578 6618198.405304266139865,1621832.140402673743665 6622276.219841150566936,1613204.768546705599874 6627780.838949244469404,1592950.855113812489435 6630781.132678112015128,1592177.741250255377963 6630387.995419438928366,1590260.708299304125831 6627929.367063040845096,1588034.207163947634399 6622963.365494169294834,1586921.012256014859304 6620064.440270774997771,1586859.229938623961061 6619573.30938395857811,1586982.794573405757546 6619132.077129542827606,1594064.05002174526453 6611913.031034580431879,1594404.242385611869395 6611619.030075713992119,1599475.401788697112352 6609803.665925649926066,1593136.42470496497117 6600485.659405926242471,1575789.063176688272506 6593577.549131480045617,1557885.104194442275912 6588683.180174053646624,1556988.537015591515228 6588388.775501389987767,1547835.62584358965978 6584132.97693841997534,1547650.056252437178046 6583692.410314185544848,1546351.180433860747144 6580513.176232004538178,1528880.254270802019164 6573817.544727659784257,1521675.434187677921727 6572938.768917518667877,1517810.310147845884785 6572304.146016950719059,1511966.036881197942421 6570057.421656171791255,1507389.581295196898282 6566591.223796062171459,1504018.938433466712013 6559711.478454791009426,1504544.589068991830572 6559077.872960864566267,1505008.568706617457792 6558346.539600173011422,1505070.351024008356035 6557859.078555491752923,1504853.834614414721727 6557468.182641371153295,1499751.839712379733101 6551765.350180339068174,1498793.156257667345926 6550888.921137005090714,1498205.723304751096293 6550938.378933507017791,1495762.817079292144626 6551765.350180339068174,1493505.480444985674694 6552691.516024117358029,1493165.288081121863797 6552984.648117745295167,1489269.10590335750021 6556005.866091744974256,1475106.817645655712113 6549086.719835380092263,1474766.736601282842457 6548795.128835988231003,1472107.313966230256483 6544655.928052576258779,1471890.908876128029078 6543585.04855293314904,1471890.908876128029078 6543049.04761622287333,1472076.478467280510813 6542125.36430627387017,1472076.478467280510813 6541541.282535792328417,1471952.691193518927321 6541054.812837922014296,1470406.574785890290514 6537503.243432618677616,1469788.30633402406238 6536286.228971413336694,1468427.759517550934106 6533953.199889834970236,1467561.805198670364916 6533660.742473053745925,1455996.934619647683576 6532981.18210254330188,1449255.982854660833254 6523214.845314455218613,1448451.922172659775242 6522196.025917547754943,1446256.479175235610455 6519525.563210516236722,1445545.704226519912481 6519040.38711741939187,1441649.076770793413743 6517779.62135499343276,1440350.53491068794392 6517390.442175720818341,1437938.464184178737924 6518410.067575070075691,1435990.373095297953114 6520108.091640782542527,1435681.238869363674894 6520400.070178261958063,1431104.783283362630755 6523214.845314455218613,1428383.578330921940506 6524865.952221196144819,1426992.08469600463286 6525351.470071250572801,1422230.059518851106986 6523846.929471051320434,1421364.216519459150732 6523554.829269106499851,1418890.586114544421434 6521903.983998507261276,1417622.768433897523209 6520787.992969233542681,1410572.571123487548903 6517924.95687254704535,1405223.001673926599324 6517924.95687254704535,1404697.239718910306692 6517924.95687254704535,1393565.290639583487064 6514770.945906778797507,1392853.959093414479867 6514286.048427115194499,1389947.518508292268962 6511134.819568190723658,1380207.063063879963011 6499991.105116979219019,1372074.395024996018037 6487266.265578266233206,1371796.207617502659559 6482143.803335079923272,1367467.103940045228228 6483640.790058706887066,1366786.830531805753708 6484753.007174490019679,1357633.919359800871462 6499409.989891317673028,1355902.233361022546887 6501249.165677722543478,1355562.040997158735991 6501540.480033307336271,1346264.97108457586728 6502321.378541161306202,1349284.846230814699084 6500039.039355491288006,1349563.256277290405706 6499699.845212746411562,1359056.359813159331679 6475671.912164300680161,1359179.924447938334197 6474706.707127678208053,1359118.142130550229922 6474127.359917213208973,1357695.701677192002535 6471231.2297751782462,1357417.402950210031122 6470218.777935646474361,1357262.78017749870196 6469108.331862445920706,1358781.511990391649306 6464689.726424768567085,1365395.225577400065958 6457732.636544094420969,1370064.521618724334985 6454504.858141225762665,1383639.265604018699378 6445217.59071833640337,1388277.50350741064176 6443822.554438131861389,1390380.440007988130674 6442235.09109375718981,1390689.574233919847757 6441947.060384312644601,1396410.282865785760805 6432096.140559693798423,1396564.794319005915895 6431663.413442983292043,1396750.363910158630461 6430223.283725295215845,1396719.417091720271856 6428543.360317423008382,1388092.156555240741 6409076.446954391896725,1387875.640145647106692 6408645.084327545017004,1386546.040147612104192 6393716.28791883494705,1388834.212280868319795 6392903.522669564932585,1392483.042550089070573 6391470.061279798857868,1392946.799548734910786 6391279.777468802407384,1393225.098275719676167 6390944.984815110452473,1394214.617229379015043 6388555.202191896736622,1397585.260091109434143 6380151.059403548017144,1400244.34876768826507 6370516.679080831818283,1402378.009447721531615 6364845.053461906500161,1410912.652167860418558 6347284.249939216300845,1423343.254426783882082 6333461.53475003875792,1424394.667017325526103 6332702.93808635417372,1425291.34551566443406 6332370.475625089369714,1433640.418644650839269 6330708.872888644225895,1434166.069280178751796 6330614.929625954478979,1434660.773097262950614 6330661.901126119308174,1435031.68964058556594 6330852.181087268516421,1435248.317369670374319 6331231.387386608868837,1434382.474370281212032 6332702.93808635417372,1434258.687096516834572 6333129.042291361838579,1434258.687096516834572 6333651.878077989444137,1434475.203506110468879 6333936.034170652739704,1435000.965461126994342 6333936.034170652739704,1441092.590636318316683 6332654.588412055745721,1442391.355135403573513 6332133.352158262394369,1443442.54508696263656 6331373.508747449144721,1450616.640990625368431 6323832.266538787633181,1466417.663472296670079 6303283.077846315689385,1482126.068137624301016 6292413.06508072745055,1483084.751592336688191 6291562.097241677343845,1491155.414674849947914 6283396.828602934256196,1491340.98426600266248 6283018.40994163416326,1491964.150775463087484 6275911.263667738996446,1503895.373798684682697 6270195.828563833609223,1510029.523019357351586 6270394.415456793271005,1511780.467290045460686 6269913.812665764242411,1512584.305333063937724 6269159.555393221788108,1512955.444515369134024 6268028.97379,1513202.796423912514001 6267558.168775944039226,1513914.127970081521198 6266710.887503619305789,1514532.396421944722533 6266522.218776003457606,1516882.684831065824255 6266946.347911407239735,1517006.249465844826773 6266475.603161985985935,1516727.839419371914119 6265957.085346825420856,1516789.844375742366537 6264544.925367821007967,1518521.307735541136935 6260309.012326624244452,1519418.208872862625867 6258944.368561895564198,1520129.31778004928492 6258098.118811561726034,1520840.538006729912013 6257298.349161185324192,1521366.299961746204644 6257016.762886860407889,1522603.059504457982257 6255229.086349239572883,1524087.504914186894894 6254241.023326002061367,1525942.755547747248784 6254570.421279144473374,1527241.520046832505614 6254241.023326002061367,1527798.006181307602674 6254429.587698009796441,1528632.902362258406356 6255134.794734570197761,1529158.664317274931818 6255182.532907337881625,1529622.532635409152135 6254805.37482338771224,1529807.879587579285726 6253583.789137552492321,1530179.018769884249195 6252455.286569516174495,1531013.914950835285708 6250997.662562035955489,1531385.054133140249178 6249869.504535327665508,1531910.81608815677464 6248929.938710411079228,1532096.163040326908231 6248365.978597317822278,1532374.461767311440781 6247896.26603374350816,1532807.494586495915428 6247520.785344040952623,1533920.689494428457692 6247051.119638216681778,1534631.68708212650381 6246204.70469803083688,1536517.995853616390377 6242823.778032816946507,1537755.089354801923037 6241040.445997013710439,1537940.436306974617764 6240477.010679271072149,1537971.383125415770337 6239771.670838021673262,1537631.302081040339544 6238504.606099782511592,1537569.519763649208471 6237754.324046231806278,1537940.436306974617764 6237331.65440771728754,1539826.745078467298299 6236768.465494444593787,1539950.532352228881791 6236533.842638072557747,1537847.818490633973852 6230530.617693575099111,1539177.418488668976352 6214510.229867246933281,1539208.365307110128924 6213808.392244072631001,1539022.795715957414359 6211515.946879042312503,1538744.496988972881809 6210159.130742960609496,1536765.347762159770355 6204594.358207736164331,1536487.271674157818779 6204080.787595919333398,1527970.88535051047802 6193062.811912507750094,1523190.603776865405962 6195391.47222859878093,1522262.978460085345432 6195998.154595790430903,1521397.135460696183145 6196698.355773873627186,1520376.669688592664897 6198707.319831451401114,1519696.618919335771352 6199547.667518802918494,1514254.209014454390854 6201884.495688748545945,1505812.40674963593483 6204735.545078444294631,1504513.64225055091083 6204781.822572304867208,1503400.66998160071671 6204408.244173904880881,1497308.822167429607362 6202024.459992506541312,1496319.414533258415759 6201463.607293738052249,1495577.247488139662892 6200715.412667649798095,1495608.082987089408562 6200014.712708934210241,1496535.708303869701922 6198940.732077550143003,1496968.629803565563634 6197913.127123503014445,1498112.882849430665374 6193430.68275030143559,1498051.100532039767131 6192683.361982377246022,1496133.844942105934024 6179576.505806717090309,1495670.087943462887779 6177619.520303159952164,1494278.482989054406062 6172588.782440930604935,1494092.913397901924327 6171983.764419369399548,1493814.837309899739921 6171470.98279697354883,1491124.467856409028172 6167979.053882445208728,1484445.298408812843263 6161698.318572333082557,1482002.503502845298499 6159558.785098511725664,1479652.326413218397647 6158164.358974670059979,1478168.103642471833155 6157421.293634124100208,1475879.931509215617552 6156722.971548175439239,1464346.007748634088784 6155608.418496932834387,1449132.195580899249762 6150125.114482661709189,1433238.221324416110292 6141026.542142974212766,1432403.32514346530661 6140330.894064240157604,1421271.598703121067956 6128833.586211016401649,1420529.320338510908186 6128045.866700580343604,1420251.132931017782539 6127489.853392218239605,1419879.993748712586239 6126332.080671465955675,1419694.424157560104504 6125034.587324028834701,1419818.211431324481964 6124433.091920059174299,1420900.459520816104487 6121053.132977366447449,1421364.216519459150732 6120034.856433538720012,1424085.310152411460876 6116749.402419441379607,1425507.861925258068368 6115130.627369059249759,1429063.96305864979513 6111847.051419837400317,1430826.484556378098205 6109812.637555180117488,1431382.970690855989233 6108887.216276194900274,1431924.206055091461167 6106854.83488246332854,1432163.320321314968169 6105956.181611167266965,1438124.033775331219658 6099832.30711757671088,1443442.54508696263656 6091387.571011839434505,1448142.787946728058159 6082627.122784348204732,1444184.600812590448186 6075855.924741555936635,1443566.332360724220052 6074935.146897738799453,1441432.671680690953508 6072448.13006972335279,1440443.264046519994736 6071114.515238320454955,1437412.702229162212461 6062559.0424261726439,1437289.137594383209944 6061915.068043232895434,1437227.132638012524694 6061133.624812577851117,1437350.919911774108186 6060536.419581540860236,1437969.522322111064568 6059662.777401387691498,1438649.795730347745121 6059525.286098636686802,1445483.476631166879088 6059111.170909988693893,1446163.75003940355964 6059156.668010426685214,1446658.453856490319595 6059433.130099025554955,1447524.296855879481882 6060030.093440468423069,1448730.332219132687896 6061086.793425534851849,1449286.818353610578924 6061271.141769378446043,1449874.362626015208662 6061225.633508309721947,1450987.557533947983757 6060766.096276036463678,1453646.868849509162828 6058100.204556143842638,1455502.23080256069079 6055481.772138500586152,1456058.939576018368825 6054516.703778555616736,1458285.10675290110521 6047996.887825408950448,1458316.164890833431855 6047308.564831959083676,1453275.729667204199359 6024072.119370986707509,1453059.435896593146026 6023476.318228266201913,1452750.301670661428943 6023065.245952074415982,1449008.630946117220446 6019175.771922996267676,1448451.922172659775242 6018992.319815597496927,1447709.866447032196447 6019038.758850434795022,1443040.681725199101493 6020045.163826836273074,1441886.409925162792206 6020526.421902596950531,1425971.618923903908581 6032680.224834294058383,1425291.34551566443406 6033505.612978994846344,1423714.393609088845551 6037171.163234572857618,1423343.254426783882082 6038226.388144602999091,1423528.824017936363816 6038821.886303264647722,1424920.428972342051566 6042720.211189256049693,1425878.778468580683693 6043270.770768674090505,1426497.380878920434043 6043362.75430063623935,1427981.492330175824463 6043270.770768674090505,1428321.68469403963536 6043684.126361171714962,1428136.226422378327698 6044233.426138117909431,1427517.73533152975142 6045105.507185718044639,1418735.740702850511298 6053965.426344594918191,1417715.386250238167122 6054471.231151284649968,1416292.945796882733703 6054516.703778555616736,1409954.080032641999424 6054608.807252679020166,1403398.475219825049862 6052862.811834366060793,1402934.71822117920965 6052587.882829224690795,1402285.280311892274767 6051715.21073561348021,1399997.330817618407309 6047859.417108722031116,1398914.860089144436643 6047491.297173419967294,1393503.508322192588821 6045885.499748370610178,1392915.964049785165116 6046069.523516119457781,1392483.042550089070573 6046390.996350195258856,1386484.257830223767087 6051760.503535140305758,1358221.463632208295166 6050062.02097649499774,1358860.994106814963743 6047094.280567661859095,1358963.519357835873961 6045060.084123249165714,1358808.896585124544799 6043637.554745960049331,1358623.215674480888993 6043035.284180908463895,1358028.435635174391791 6042259.181701392866671,1357624.123244612012058 6041906.490945130586624,1351573.129683562321588 6042399.040123600512743,1348202.709460814250633 6042994.99118659645319,1342512.947647389490157 6043958.771881455555558,1336544.998426471138373 6044601.407170771621168,1326928.107616840861738 6042904.16750372108072,1300551.622190318070352 6038042.531430001370609,1289481.678067361935973 6028558.752152914181352,1288832.128838583827019 6027688.339456328190863,1288461.212295261444524 6027367.726555364206433,1286234.711159904720262 6026634.564959973096848,1280761.68839505314827 6025582.072457782924175,1275442.954444442177191 6026085.064171101897955,1257508.271282737376168 6012362.328927347436547,1237037.730120762251318 6007107.508181246928871,1235151.421349269570783 6006970.850355018861592,1227699.249357604887336 6006832.714670409448445,1225442.024042789824307 6006924.311539120040834,1222689.872271907981485 6007289.393059633672237,1222102.439318991731852 6007472.597420475445688,1221576.67736397520639 6007701.032700366340578,1221298.267317502293736 6008157.756857266649604,1214433.639598242240027 6027046.960709646344185,1190097.974396434845403 6032955.675344381481409,1176585.012728528585285 6030711.09183444827795,1167091.909192659892142 6037721.369213320314884,1167122.967330592218786 6038455.471824086271226,1166937.397739439737052 6039005.759605477564037,1166411.635784423211589 6039235.029016827233136,1165793.367332557216287 6039143.089436389505863,1162577.235924049746245 6037905.21909317933023,1161526.04597248788923 6037492.304998569190502,1161062.066334862262011 6037217.865020699799061,1160721.985290489625186 6036759.439329176209867,1161773.397881031269208 6028878.255158752202988,1164216.19279 6022606.416799211874604,1165236.658559099538252 6021370.911691213957965,1165422.228150252019987 6020868.358687356114388,1165484.01046764315106 6020182.192832938395441,1165978.714284727117047 6014145.257231585681438,1165885.873829406686127 6013459.788619183003902,1161031.342155403690413 6006970.850355018861592,1151043.312163468217477 5993641.693775478750467,1150734.177937533939257 5993230.721030218526721,1149868.33493814454414 5992546.904208737425506,1143745.762944515096024 5989355.924241134896874,1139107.525041120126843 5987350.343755191192031,1138581.76308610662818 5987167.563924365676939,1137901.489677867153659 5987122.443822233006358,1132490.137910915073007 5987031.055824706330895,1136757.459270984632894 5994873.250849040225148,1138952.902268408797681 6002677.175694292411208,1139107.525041120126843 6003270.396881964057684,1138952.902268408797681 6004549.683644127100706,1117987.768608840648085 6023293.928238715976477,1108185.530847037443891 6030619.242894043214619,1092446.068043777486309 6039509.533083806745708,1091858.523771370062605 6039693.421024010516703,1088982.807365709217265 6039648.026419921778142,1087993.399731538025662 6039143.089436389505863,1086818.199867232004181 6038042.531430001370609,1085303.030278044287115 6035751.087411268614233,1084591.921370857860893 6034100.950838732533157,1083849.643006247701123 6031810.716501901857555,1083049.589825916802511 6029890.092570433393121,1078967.615418020403013 6028936.61766411177814,1074400.733308225870132 6028936.61766411177814,1069833.962517922511324 6028936.61766411177814,1069453.472498390590772 6028936.61766411177814,1066208.509341767290607 6030239.819821005687118,1064962.844239790691063 6030739.949422918260098,1055121.978614682564512 6036579.409109153784811,1030940.379588130512275 6050945.930667268112302,1011553.867627500905655 6054608.807252679020166,1010595.406811770866625 6054746.220188598148525,1006606.495498177362606 6055252.236154717393219,1004565.563953973352909 6055068.01644816249609,1002431.903273937175982 6054194.928086219355464,1001504.277957157115452 6053643.671215767040849,1000267.29577546287328 6052587.882829224690795,998906.63763949542772 6051715.21073561348021,996061.979371764115058 6050062.02097649499774,994546.809782576514408 6049328.087735320441425,992722.283328477642499 6048914.62705113273114,991299.954194610589184 6048869.18393955938518,989939.296058643143624 6049051.94932599645108,988884.321244396152906 6049509.044986145570874,985826.820110267726704 6056905.09776827134192,983538.647977014188655 6060075.59539058059454,980600.815295488690026 6063018.675260256975889,980075.164659960544668 6063248.58363129850477,979611.407661317498423 6062926.481905543245375,977137.665936908684671 6060857.108186458237469,976519.174846060224809 6059984.757190841250122,976581.179802433587611 6059295.477011421695352,976797.473573044757359 6058789.389180728234351,979626.324473083368503 6054067.610083510167897,975313.362121789599769 6055802.272802433930337,973519.782486126176082 6056262.86865904647857,970730.672644302132539 6056531.629850168712437,971726.314169956836849 6057732.970724358223379,971896.855629852274433 6058028.245343515649438,972282.80030443193391 6058697.241046601906419,972375.640759755275212 6059386.307948785834014,972592.157169345999137 6066099.195480740629137,972592.157169345999137 6066696.786335341632366,972375.640759755275212 6067248.878562669269741,966871.55985697126016 6071435.597808036953211,964181.301722971606068 6073323.069737927056849,958986.355046119308099 6074243.509433472529054,958306.081637882627547 6074335.823993384838104,953575.11459865863435 6074796.085681305266917,952863.783052489627153 6074705.092132150195539,943927.276970587205142 6069089.883261362090707,943556.249107773415744 6068675.473833568394184,943246.892242859350517 6068260.918722632341087,935825.7783891268773 6057732.970724358223379,935887.560706517891958 6057042.712699921801686,936567.834114754572511 6053506.110506500117481,937835.651795398676768 6052036.72863816190511,943494.466790385311469 6048318.267214233987033,945689.909787809476256 6048363.707425068132579,948256.380648047546856 6048777.141715809702873,949091.388148486847058 6049420.135835316032171,949338.740057030343451 6049971.122907866723835,950328.147691201418638 6051210.72453102748841,951317.666644863784313 6051715.21073561348021,951936.157735709450208 6051852.577586862258613,955183.013323678285815 6051899.358854937367141,958489.090880748117343 6051754.222037793137133,959666.628454358782619 6050659.658303293399513,960347.013182086637244 6049832.463880216702819,960625.311909071286209 6049373.533263418823481,960779.934681782731786 6048777.141715809702873,960594.365090630133636 6048180.956612694077194,956759.96523025399074 6040839.590027997270226,956389.048686931375414 6040473.089950850233436,954722.707229246501811 6039140.613518672063947,953944.58398860367015 6040323.687849867157638,953513.109642288065515 6042949.579225749708712,953142.19309896545019 6044096.180495639331639,952709.160279778181575 6044417.414694011211395,949709.656600352958776 6045977.511141882278025,948441.950239200028591 6046206.965729371644557,947452.431285537662916 6045701.645089991390705,943710.871880484861322 6042582.989596341736615,942319.266926079173572 6040976.783922153525054,939072.633977092802525 6036209.294600993394852,934063.256891395896673 6037263.081831538118422,923116.877403221675195 6041527.23236068058759,922622.062266646302305 6042491.014370169490576,922096.411631118273363 6042720.211189256049693,917365.333272405900061 6043774.793098554946482,913438.204276200383902 6044142.754584637470543,912696.148550572805107 6044142.754584637470543,912077.657459724228829 6044004.189150207675993,911180.756322402739897 6043408.168434468097985,902893.688149789930321 6037446.921959261409938,902584.553923855535686 6036988.649134557694197,902120.685605721198954 6035933.740371827036142,901966.062833006959409 6035338.276244388893247,901656.928607075358741 6034880.115236963145435,900822.032426127232611 6034238.202734721824527,900203.541335278772749 6034055.750571250915527,894266.538932801573537 6033322.021629512310028,882979.967080760747194 6032680.224834294058383,881526.579808964044787 6032772.095675515010953,880939.035536556621082 6032955.675344381481409,880135.086174049647525 6033642.855420893989503,864921.49664529459551 6032863.802551919594407,856850.944882272509858 6031214.217077135108411,852274.377976780291647 6032405.772697052918375,848378.19579901592806 6034146.316308383829892,853449.355202103964984 6036301.202965809963644,853820.383064917754382 6036667.526090055704117,854036.899474511388689 6037171.163234572857618,854593.496928477659822 6041160.703102989122272,854315.198201493127272 6041619.197322756052017,850542.691978001967072 6041206.10565793607384,844722.129762893309817 6038003.416996375657618)) POLYGON ((1582839.260487098013982 7145388.983421822078526,1580272.900946348439902 7146464.99650246091187,1579747.250310823088512 7146570.170061586424708,1567656.61777678411454 7146202.729581869207323,1563513.083690476138145 7145992.393651931546628,1562678.18750952812843 7145573.248989854007959,1561719.504054815741256 7144576.64253150112927,1559369.549604171188548 7142531.718224225565791,1557637.752285898663104 7141745.524022626690567,1552813.944791354238987 7140592.417199974879622,1546567.808162942761555 7139806.415980514138937,1544836.122164164436981 7140068.470586787909269,1539208.14266812754795 7141954.237177175469697,1538817.299935953458771 7142505.862777774222195,1538991.848897516494617 7143371.21695429366082,1539919.474214296555147 7145100.888291492126882,1540166.826122840167955 7145521.329465435817838,1540537.965305145131424 7145835.495004257187247,1544217.742392807034776 7147094.548188736662269,1550587.554975488921627 7150767.681931395083666,1551752.624766129534692 7154049.014241728931665,1553749.362472489709035 7159011.679901720024645,1556476.578677434241399 7163067.089470122940838,1559885.626763487234712 7160583.806943288072944,1561882.364469847176224 7159342.647890636697412,1563879.102176204323769 7159259.810028493404388,1564707.096548727015033 7167290.048451251350343,1563781.697621760191396 7171183.601602585986257,1560031.789254898903891 7172841.326522910967469,1559398.715310758212581 7175162.272457200102508,1558278.618594394531101 7177732.803795194253325,1556233.122951069846749 7180884.848681399598718,1553359.744254713412374 7181631.520458415150642,1550632.52804976888001 7182129.21451242826879,1549415.138098455267027 7180552.785492856055498,1550242.909831992583349 7177068.957026258111,1548701.468842978822067 7175207.944100338965654,1547650.056252437178046 7172838.484735211357474,1547124.294297417858616 7171997.360606805421412,1542949.813392671756446 7169891.912742538377643,1542609.621028807712719 7170208.375879127532244,1538744.496988972881809 7177102.129081013612449,1537175.560085734818131 7183730.621919110417366,1538149.271671701455489 7188710.685231941752136,1536675.624252579873428 7191223.926272539421916,1531755.970676462864503 7196661.552845137193799,1531601.459223242709413 7197137.203264712356031,1531323.049176766769961 7198668.297301165759563,1531477.894588460680097 7199988.097669545561075,1531663.241540630813688 7200516.041785303503275,1532188.892176158959046 7201361.738734981976449,1532838.44140493683517 7201994.169557489454746,1534106.259085580939427 7202628.172588893212378,1536270.643945075804368 7202681.991035751067102,1537662.248899481492117 7202153.901559179648757,1538218.735033956589177 7201414.027389189228415,1538373.357806667918339 7200938.118378311395645,1538775.443807414034382 7199512.274898992851377,1539053.631214907392859 7197347.387958203442395,1539331.929941889131442 7196397.800640229135752,1539733.904623144073412 7195553.966978441923857,1543568.304483517538756 7190383.685034474357963,1546493.112784622469917 7187276.236181613989174,1549707.240442296024412 7185698.587033405900002,1551606.573594209272414 7184785.125955098308623,1554626.003462485969067 7184038.344924838282168,1558619.478875205852091 7182544.615864975377917,1570161.417639125138521 7169891.912742538377643,1578633.944083399139345 7160323.907735708169639,1579283.270673197694123 7159693.299981384538114,1582313.832490552449599 7157382.006901117041707,1583581.538851708173752 7156646.324924129992723,1582499.179442725377157 7151239.059246807359159,1582839.260487098013982 7145388.983421822078526)) POLYGON ((1550773.124566641403362 7173260.59616072755307,1550216.638432166306302 7173627.783280280418694,1550371.149885386694223 7174207.397760728374124,1551175.210567387519404 7175417.544330146163702,1551670.025703962892294 7175365.428019927814603,1551824.537157183280215 7174891.280184879899025,1550773.124566641403362 7173260.59616072755307)) POLYGON ((1412489.604074438801035 7249252.634919972158968,1412211.305347457062453 7249783.900581362657249,1413417.229391219094396 7251059.697336432524025,1414066.555981017416343 7251697.67355730291456,1414530.535618642810732 7251591.276796009391546,1416787.760933458106592 7250687.536619270220399,1417313.411568983457983 7249890.464141698554158,1417066.059660442639142 7249465.749051242135465,1412489.604074438801035 7249252.634919972158968)) POLYGON ((1486949.875632171519101 7219498.407977689988911,1492021.257674239110202 7220556.955176550894976,1492608.801946646766737 7220556.955176550894976,1493165.288081121863797 7220450.789075921289623,1493907.343806749442592 7219815.347287071868777,1494123.971535834250972 7219392.256178009323776,1494618.675352918449789 7218015.46194199565798,1494896.974079902982339 7216481.930447698570788,1494958.756397291086614 7215370.02623397577554,1494402.270262815989554 7214522.82156962249428,1491433.824721323093399 7212301.438104316592216,1490908.062766306567937 7212195.383458035998046,1482620.771954711293802 7217380.217040959745646,1476993.126417148159817 7218598.354791543446481,1475756.14423545403406 7218809.304440607316792,1474952.306192435324192 7219233.698171205818653,1464531.577339786570519 7228237.503288531675935,1460728.12429785146378 7233008.922166247852147,1460326.038297105347738 7233645.43230783380568,1460202.47366232611239 7234705.88804304972291,1460264.255979717243463 7235395.051036525517702,1461532.073660361114889 7240171.014601109549403,1461717.643251513829455 7240701.66557838767767,1462088.55979483621195 7240966.908973953686655,1462923.567295275628567 7241392.693056788295507,1464005.815384770045057 7241551.699141959659755,1464593.359657174674794 7241551.699141959659755,1469478.949469109997153 7241073.352828699164093,1471334.422741652932018 7240542.67671681381762,1471983.749331451253965 7240436.240060306154191,1473406.078465315513313 7240861.995348724536598,1476251.070691520581022 7242188.90248944144696,1476683.76955223409459 7242506.759016290307045,1476931.344099757261574 7242932.626669025048614,1473756.846180804073811 7248472.541204737499356,1470284.345985 7251655.382585428655148,1465480.909957269439474 7252849.759714975953102,1472602.240422297036275 7258717.499625510536134,1473004.437742534326389 7262706.439995508641005,1465770.229313843650743 7267988.42951176315546,1463513.115318516734987 7274172.079961638897657,1472911.374648231314495 7275495.041372497566044,1475608.868549132719636 7275269.509876077994704,1480991.165928988950327 7275070.279796813614666,1484753.096800855360925 7278164.2921830303967,1488051.827271532267332 7280859.615410842001438,1489209.327336800051853 7276567.124601342715323,1491119.235840340843424 7273473.551317208446562,1493954.988548810360953 7269583.848109656944871,1496964.510982404230163 7263304.352533773519099,1501015.76121084485203 7262308.048603367991745,1503157.102935744915158 7264101.256900504231453,1504661.808492796262726 7267988.42951176315546,1503099.216800533002242 7274970.858589864335954,1498295.669453310780227 7275169.894202742725611,1494591.736036147456616 7278863.239495515823364,1489845.963504649000242 7280460.299489669501781,1488862.121845016721636 7286554.052541512064636,1486720.780120116891339 7285455.039353961125016,1483857.754136405419558 7281307.933751451782882,1481724.204775860533118 7279493.881782446056604,1479015.356286898721009 7277905.083082603290677,1478551.487968761473894 7277617.275033747777343,1475137.875783588038757 7276508.381772340275347,1474550.331511180615053 7276508.381772340275347,1474395.486099486704916 7276775.031683855690062,1472169.3189226037357 7291133.013455408625305,1472169.3189226037357 7292308.420766508206725,1472323.83037582389079 7292895.517371956259012,1472818.64551239926368 7293804.142025765962899,1474395.486099486704916 7295514.327984113246202,1476467.364462131867185 7297439.311846246011555,1478168.103642471833155 7298348.464695187285542,1479683.273231656523421 7298722.897780749946833,1487722.989495728630573 7299899.446665154770017,1489887.596994205843657 7300166.895093746483326,1494371.323444374836981 7300541.415291382931173,1495020.538714681984857 7300434.160115850158036,1495886.493033562554047 7299952.396491496823728,1496473.925986478570849 7299203.23794683162123,1496535.708303869701922 7298668.608684515580535,1496350.361351699568331 7298188.301365684717894,1494866.027261461829767 7296208.861092009581625,1494185.753853225149214 7295568.595182152464986,1491371.81976495240815 7294659.957912607118487,1490629.652719833655283 7294071.189627869985998,1490073.166585355531424 7293269.881797047331929,1489794.867858373792842 7292094.137637066654861,1490714.144213345600292 7285954.361991597339511,1492508.280446459306404 7282457.851325244642794,1495401.919290140969679 7280360.617740402929485,1497948.46396152745001 7279461.618702667765319,1500784.216669994173571 7279761.209559430368245,1504604.033677075523883 7280160.489814765751362,1513171.849605471594259 7281681.744083254598081,1515924.00137635320425 7281948.378189398907125,1517717.469692522659898 7281895.742206728085876,1518305.013964930083603 7281788.74241354689002,1519170.85696431924589 7281307.933751451782882,1521922.786096221301705 7278693.873456065542996,1522479.49486967897974 7277948.284003293141723,1522634.117642390308902 7277467.90317939966917,1522757.68227716954425 7273575.448179656639695,1522603.059504457982257 7271656.325458750128746,1522324.983416456030682 7270483.823662910610437,1521768.274642998352647 7269684.927165135741234,1521397.135460696183145 7269418.518576430156827,1519232.639281710144132 7268406.095154429785907,1518490.583556082565337 7267873.565776283852756,1512986.502653298666701 7262762.01327615045011,1512306.229245061986148 7262123.135960400104523,1511873.196425877511501 7261165.684014190919697,1511533.115381502080709 7260101.453564746305346,1511316.487652420066297 7258238.296688742004335,1511361.572046190965921 7257758.35701,1511471.110425131395459 7256590.230786599218845,1512120.659653909504414 7254250.289068546146154,1515892.943238420877606 7247073.999498478136957,1516233.135602284921333 7246755.766629800200462,1517408.112827608594671 7246490.327454176731408,1519108.85200794856064 7246542.726118778809905,1521366.299961746204644 7246703.366542059928179,1522417.489913305500522 7246437.737905505113304,1525355.211275339825079 7243994.338150886818767,1525911.808729306096211 7243250.513327450491488,1526623.028955983929336 7242029.883491246961057,1528540.173226426355541 7237624.50387907307595,1529962.724999273195863 7233115.258152510039508,1528509.337727476609871 7222569.808009616099298,1528138.198545171413571 7222303.660180361010134,1526746.593590765958652 7221774.251081096939743,1526313.894730052212253 7221880.435066026635468,1524582.208731271093711 7223468.790868346579373,1524396.639140118611977 7223893.627045250497758,1524674.937867103144526 7224158.303208802826703,1526282.947911611292511 7226595.726253507658839,1526499.241682222345844 7231206.011430525220931,1526437.459364831447601 7231789.88393749576062,1523901.935323034413159 7236403.411821288987994,1523345.337869068142027 7236562.125857115723193,1503740.751025973353535 7235342.535794446244836,1503307.829526277258992 7235129.995875702239573,1499411.647348512895405 7232585.119797260500491,1487166.503361253533512 7222357.422840385697782,1486857.36913532204926 7222038.855851247906685,1486671.799544169567525 7221509.464594282209873,1486949.875632171519101 7219498.407977689988911)) POLYGON ((1454172.630804525688291 7258770.171599520370364,1453925.278895985102281 7259515.470086960121989,1453832.438440661877394 7260899.564955903217196,1453956.114394932053983 7262814.712440844625235,1454265.471259848913178 7265424.818543641828001,1457914.190209578722715 7282802.897972921840847,1458718.139572088373825 7284029.896428570151329,1459398.412980325054377 7284670.563680847175419,1460233.197841784683987 7285151.579000337049365,1462397.916659750510007 7285418.331364364363253,1463572.893885073950514 7285418.331364364363253,1464005.815384770045057 7285151.579000337049365,1464314.94961070176214 7284777.602885926142335,1464438.736884463112801 7284296.802715539000928,1464747.871110397623852 7281574.747215809300542,1464747.871110397623852 7280988.499501549638808,1459491.142116157105193 7271922.618324822746217,1459243.790207613725215 7271443.949958885088563,1455347.608029849361628 7263241.492915647104383,1454172.630804525688291 7258770.171599520370364)) POLYGON ((751066.370490731205791 7087313.046944672241807,744789.175724390079267 7088667.22770073171705,742872.031453947653063 7089396.613139569759369,741820.618863405892625 7090229.180570777505636,741387.808683201204985 7091062.960473985411227,741202.239092048606835 7092155.988093191757798,741233.074590998468921 7093458.104672775603831,741511.484637474291958 7094501.377636595629156,741727.889727576752193 7094917.824832899495959,742439.109954254468903 7095490.381662609986961,751128.152808122220449 7098253.381027875468135,752365.134989816462621 7098306.304919430054724,753880.304579003946856 7097888.178279020823538,757436.29439290438313 7095594.879624365828931,757683.757620939053595 7095178.396844930015504,757498.18802978657186 7094866.236717786639929,756910.643757379148155 7094969.413283868692815,756601.398211956257001 7095178.396844930015504,754220.385623379494064 7095178.396844930015504,751746.643898970680311 7094448.479153910651803,751313.722399274702184 7094240.827885642647743,748685.357902154442854 7092312.013250234536827,748407.059175172704272 7091948.584406548179686,748839.980674865888432 7091114.523837874643505,751066.370490731205791 7087313.046944672241807)) POLYGON ((767393.266246910206974 7107224.525081160478294,766187.230883654323407 7107329.177969643846154,765723.473885011277162 7107538.487896650098264,765476.121976467780769 7107955.621129569597542,765692.527066570124589 7108372.776341564953327,766032.719430434051901 7108686.78469696175307,767980.810519317630678 7109313.335294873453677,772959.1294670823263 7109991.621879470534623,786317.579681765520945 7111140.274599626660347,788265.670770649099723 7111088.580705529078841,788760.37458773329854 7110930.869420263916254,789069.620133156189695 7110566.585280873812735,789069.620133156189695 7110096.311353722587228,788605.86313451314345 7109782.247084467671812,772495.372468436486088 7107799.287541255354881,767393.266246910206974 7107224.525081160478294)) POLYGON ((796892.819987637456506 7113333.346559356898069,795748.789580754935741 7113489.789485096000135,794913.893399804015644 7113960.265090770088136,794357.295945837744512 7114691.983569264411926,794511.807399057899602 7115214.251165832392871,794851.999762921826914 7115527.026405527256429,798748.293260177480988 7117356.905847727321088,801624.009665841120295 7118297.028005286119878,812477.660018186084926 7118611.430383102037013,815631.675150831579231 7118558.370577606372535,816744.870058764237911 7118350.086058034561574,817177.791558460216038 7118140.67815310228616,817858.06496669689659 7117513.240211769007146,817734.389012426603585 7116990.82085529062897,815693.457468222477473 7116312.122268796898425,808488.637385098496452 7114638.950040736235678,806231.30075079202652 7114377.736889900639653,796892.819987637456506 7113333.346559356898069)) POLYGON ((904378.022240024874918 7115214.251165832392871,904006.883057722589001 7115475.491931471042335,903914.153921890538186 7115997.811244462616742,904439.915876907063648 7117984.143249625340104,904934.619693994056433 7118819.533622488379478,905614.89310223062057 7119446.887000731192529,906449.677963690366596 7119864.648398477584124,907964.847552875056863 7120230.866400840692222,909820.209505926584825 7120074.290133906528354,910778.781641147914343 7119708.267562028951943,911520.94868626666721 7119185.703244997188449,912077.657459724228829 7118453.569099596701562,911830.305551180848852 7118192.229989159852266,904378.022240024874918 7115214.251165832392871)) POLYGON ((832453.274724094662815 7117722.819644777104259,831958.57090700767003 7117879.349377910606563,831463.755770432413556 7118663.173443947918713,831309.132997720967978 7119761.33508057333529,831401.973453041398898 7121067.999791787005961,831803.836814805050381 7122687.938866849988699,832546.11517941521015 7123891.53162141609937,833319.117723483825102 7124362.814538145437837,836658.702447281917557 7124832.619109984487295,843306.925076436833479 7125042.204260415397584,847110.378118371940218 7124989.101297973655164,847697.922390779363923 7124885.721021626144648,848068.95025359315332 7124624.169308320619166,848501.87175328633748 7123786.659458961337805,849213.091979966848157 7121903.715913665480912,849243.927478916710243 7121485.846815372817218,848439.978116406942718 7121903.715913665480912,846893.973028269479983 7122323.113043705001473,844574.742757083731703 7122687.938866849988699,843275.978257998474874 7122741.026379873976111,836936.889854775276035 7122165.17767,836473.132856129319407 7122008.563111335970461,836132.940492265392095 7121695.53152232337743,832453.274724094662815 7117722.819644777104259)) POLYGON ((853789.547565967892297 7123838.436385111883283,853232.838792510330677 7124571.06915295869112,853202.003293560468592 7125147.093074432574213,853356.626066271914169 7125670.05142262391746,853758.600747526739724 7126559.555521614849567,854315.198201493127272 7127290.933554795570672,855366.499472546391189 7128180.620029207319021,857005.345016004284844 7128442.289412470534444,868817.678823058842681 7128180.620029207319021,869405.111775975092314 7128077.198017291724682,869714.468640889157541 7127762.419125949963927,869497.952231298317201 7127290.933554795570672,869095.866230552201159 7127082.607296543195844,863715.461282041389495 7124937.316837606020272,853789.547565967892297 7123838.436385111883283)) POLYGON ((937557.46438790531829 7156226.614200555719435,936536.998615804710425 7156542.529518845491111,935856.725207568029873 7160272.080242301337421,935949.343023905996233 7160849.389651210978627,936382.264523601974361 7161742.67573688365519,937619.246705296216533 7163109.663600580766797,938546.872022076393478 7163477.892113502137363,939660.178249500342645 7163635.141901593655348,941206.294657128979452 7163161.698960998095572,941453.646565669565462 7162741.641487595625222,937897.656751769245602 7156542.529518845491111,937557.46438790531829 7156226.614200555719435)) POLYGON ((1274762.569716714089736 7162425.477847404778004,1272257.992493355413899 7162847.032183882780373,1271670.559540439397097 7162950.910567733459175,1268207.298862368101254 7164107.288563192822039,1267774.37736267503351 7164318.107863544486463,1266537.395180980674922 7165369.260534231550992,1266320.990090878447518 7165790.972247155383229,1266073.63818233483471 7166788.936465878970921,1266135.420499725732952 7167999.270048506557941,1266258.9851345049683 7168577.145378472283483,1266661.182454742258415 7169524.899591765366495,1267310.397725046612322 7170208.375879127532244,1269227.764634471619502 7171471.324760464020073,1273989.567172645358369 7174365.051177844405174,1274917.192489425651729 7174681.693950336426497,1275999.663217899622396 7174839.167354584671557,1277174.640443220501766 7174839.167354584671557,1278906.103803019272164 7174522.518255564384162,1279246.29616688308306 7174207.397760728374124,1279648.382167629199103 7173312.698554771952331,1279524.59489386761561 7172049.454651641659439,1277978.478486238978803 7167157.147496549412608,1277638.397441866341978 7166157.811637355014682,1276803.501260915305465 7164266.064873268827796,1276587.096170815872028 7163844.623926190659404,1275628.524035594658926 7162793.485208897851408,1274762.569716714089736 7162425.477847404778004)) POLYGON ((967582.78008364897687 7177734.130786491557956,966438.527037786901928 7177893.371364488266408,965882.040903308894485 7178629.521717742085457,965479.954902565572411 7179524.824797453358769,964707.063677988131531 7182265.403044558130205,964861.686450699577108 7184055.609641991555691,965016.197903919732198 7184530.499061216600239,965294.607950392761268 7184899.53997459821403,966036.66367602313403 7185478.655997902154922,967428.04599144635722 7186007.146017430350184,967737.402856360422447 7185795.745753892697394,968479.458581988001242 7181263.960203673690557,968201.048535515088588 7178419.836038890294731,967984.75476490391884 7177945.314820511266589,967582.78008364897687 7177734.130786491557956)) POLYGON ((1292688.569958136649802 7189988.18556560203433,1287749.880749092437327 7184319.138428661972284,1285090.792072513606399 7181790.650160569697618,1281410.903665360296145 7179367.257730196230114,1281410.903665360296145 7179947.294530897401273,1281812.878346615238115 7181633.037770551629364,1283390.052892173407599 7184741.675626928918064,1284162.832797259557992 7185795.745753892697394,1286389.333932616282254 7188115.386578351259232,1286791.419933362398297 7188326.849122101441026,1292688.569958136649802 7189988.18556560203433)) POLYGON ((1222226.00395377073437 7242560.660197351127863,1223957.689952552085742 7242826.157588758505881,1226493.213994351681322 7242506.759016290307045,1226400.484858519630507 7242188.90248944144696,1225998.510177264921367 7241975.985956302843988,1224854.368450890993699 7241869.529856295324862,1222968.282318380894139 7242082.443500669673085,1222226.00395377073437 7242560.660197351127863)) POLYGON ((1255007.812880537472665 7257518.589688713662326,1257971.916961892042309 7251325.482561790384352,1258343.056144197238609 7250421.773014653474092,1259270.792780465679243 7248137.61060084681958,1259332.575097856577486 7247606.456439974717796,1240006.286981746321544 7247552.520578687079251,1238305.547801406355575 7247606.456439974717796,1226857.562687717145309 7253693.837244801223278,1225627.370994959725067 7255100.260760948061943,1225411.077224348671734 7255525.281166699714959,1225163.725315807852894 7256536.233095415867865,1225225.50763319619 7258078.951923226937652,1225782.216406653635204 7261006.27972043864429,1226586.054449672345072 7263666.95505344774574,1227359.168313232250512 7265636.839201695285738,1228070.388539909850806 7266967.946266352199018,1229399.988537945086136 7268938.660739611834288,1231719.218809130834416 7271976.722160861827433,1232182.975807776674628 7272136.349841263145208,1245015.664067443460226 7269259.138072649016976,1250025.041153140366077 7266328.725909586064517,1250705.31456137704663 7265691.091340849176049,1251540.210742328083143 7264572.193036042153835,1251756.504512939136475 7264146.681741131469607,1255007.812880537472665 7257518.589688713662326)) POLYGON ((997484.308505631168373 7269099.569024206139147,996834.98191583273001 7267181.542618941515684,993526.344010475906543 7260472.720428659580648,992722.283328477642499 7259249.407057045027614,992382.202284102095291 7258930.871191246435046,991516.470604204223491 7258504.133868282660842,989970.354196575470269 7258131.619606307707727,983785.99988555489108 7258078.951923226937652,982610.91134074004367 7258292.306106355041265,981775.903840300743468 7258770.171599520370364,981250.364524266682565 7259515.470086960121989,981188.359567893203348 7260154.134840494021773,981188.359567893203348 7264997.727441687136889,981281.20002321654465 7265584.312753542326391,981497.71643280738499 7266009.902137937955558,986537.92901745706331 7270057.991138122975826,986940.015018200385384 7270271.671772250905633,993124.480648712255061 7270217.579714123159647,995474.435099356574938 7269844.31633363571018,997484.308505631168373 7269099.569024206139147)) POLYGON ((960996.228452393785119 7263773.707512620836496,959171.924637271789834 7264039.924200204201043,958739.003137578605674 7264253.440736713819206,958491.651229035109282 7264624.904377176426351,957100.046274629537947 7267128.814073079265654,956450.831004322390072 7268406.095154429785907,956265.261413169791922 7270164.830726452171803,956357.990549001842737 7270750.268491155467927,956574.39563910139259 7271177.67336846049875,957316.674003711552359 7271817.289588546380401,962047.641042935545556 7274215.260288330726326,967520.775127278408036 7275761.46482103690505,968788.592807922512293 7275761.46482103690505,969685.493945244001225 7275335.345207438804209,969716.440763685153797 7274748.221083773300052,969561.706671482417732 7271656.325458750128746,969407.08389877108857 7271123.383215131238103,967582.78008364897687 7267607.218787301331758,966871.55985697126016 7266328.725909586064517,966284.015584563836455 7265584.312753542326391,965263.549812463228591 7264624.904377176426351,964335.924495683051646 7264306.149933400563896,960996.228452393785119 7263773.707512620836496)) POLYGON ((929919.722805088036694 7287074.013764334842563,929393.960850071627647 7287233.757404427044094,928651.905124443932436 7287820.473227696493268,927507.763398070237599 7289155.357990073040128,923735.145855087786913 7295888.625360798090696,923394.953491223859601 7296797.597897732630372,923333.171173832844943 7297439.311846246011555,923580.523082376341335 7298508.438796105794609,924786.558445629430935 7301396.620964758098125,925281.262262713629752 7302198.76221277192235,928713.687441834947094 7305355.741972773335874,929486.801305394852534 7305945.360848530195653,929950.558304037898779 7306105.490519311279058,930414.426622175029479 7305890.828869744203985,935083.722663499298505 7289957.78275859169662,931527.621530107688159 7287393.696542851626873,931125.535529361572117 7287179.548005699180067,929919.722805088036694 7287074.013764334842563)) POLYGON ((953296.70455218560528 7300808.885685969144106,944916.907243740744889 7301076.365233298391104,943803.712335808086209 7301343.661354387179017,935361.798751498572528 7304767.901559899561107,934743.530299635371193 7305463.064710984006524,934743.530299635371193 7306533.289849194698036,935794.720251194550656 7309531.818477640859783,936567.834114754572511 7310764.368792708031833,937959.439069160143845 7311995.763719338923693,940062.041611263994128 7313174.312944632023573,941948.350382756558247 7313870.417450193315744,950513.717282354016788 7314513.535048003308475,951905.099597780033946 7314245.595095922239125,953976.977960422285832 7313549.650032759644091,955337.636096389731392 7312906.611797288060188,955646.77032232133206 7312531.305608913302422,955894.122230864828452 7312156.210299688391387,956759.96523025399074 7307978.731110918335617,956790.912048695026897 7307390.50061244238168,954255.38800689810887 7301824.162597080692649,953760.684189811116084 7301021.867218040861189,953296.70455218560528 7300808.885685969144106)) mapproxy-1.11.0/doc/Makefile000066400000000000000000000065531320454472400156740ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/OmniscaleProxy.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/OmniscaleProxy.qhc" latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." mapproxy-1.11.0/doc/_static/000077500000000000000000000000001320454472400156515ustar00rootroot00000000000000mapproxy-1.11.0/doc/_static/logo.png000066400000000000000000000025301320454472400173170ustar00rootroot00000000000000PNG  IHDR47CiEiCCPICC ProfilexTkA6n"Zkx"IYhE6bk Ed3In6&*Ezd/JZE(ޫ(b-nL~7}ov r4 Ril|Bj A4%UN$As{z[V{wwҶ@G*q Y<ߡ)t9Nyx+=Y"|@5-MS%@H8qR>׋infObN~N>! ?F?aĆ=5`5_M'Tq. VJp8dasZHOLn}&wVQygE0  HPEaP@<14r?#{2u$jtbDA{6=Q<("qCA*Oy\V;噹sM^|vWGyz?W15s-_̗)UKuZ17ߟl;=..s7VgjHUO^gc)1&v!.K `m)m$``/]?[xF QT*d4o(/lșmSqens}nk~8X<R5 vz)Ӗ9R,bRPCRR%eKUbvؙn9BħJeRR~NցoEx pHYs  IDAThYN1mW?x^׿/pU>B' x5$/ %l&nu:od; DLW„BW4:Nw_dUKP޼ c{}4 0U $ٛ7a;RmPt)gBuΐQ_!Lf τBWbR(9$"uolI dd{.P gӟû/|@b<-ބv[>Vݽ7H\VeBR7N}6נ k[jcV(DUtX!=!6:Gޟ>('}ZV{&9qTfBi?bdpt]-4@Rci0r\:!V;pKogW:Nw}mkK9o㷙-J|"`Bm*Dg=p&n`_`mG5bt.&cB:4 $F+PnXN!`mE {H6j2]1213)';dYIENDB`mapproxy-1.11.0/doc/_static/mapproxy.css000066400000000000000000000013701320454472400202430ustar00rootroot00000000000000body { font-family: Verdana, sans-serif; } h1, .h1 { margin-top: 10px; } a:link, a:visited, a:hover, a:active { color: #2e3436; color: #31A4B5; text-decoration: none; } code { color: #3a3740; font-size: 75%; background: inherit; padding: 0; } .footer a { color: inherit; text-decoration: underline; } .bs-sidenav { margin-top: 5px; } .bs-sidenav.affix { top: 5px; } .bs-sidenav .nav > li > a { padding: 3px 20px; } .toctree-l1 .current { font-weight: bold; } .nav-list li.toctree-l2 { background-color: #F5F5F5; } .navbar-default, .footer, .bs-sidenav { background-color: #ececec; } .section h2 { margin-top: 30px; padding-top: 10px; } .alert-warning, .alert-success, .alert-info { background-image: none; } mapproxy-1.11.0/doc/_templates/000077500000000000000000000000001320454472400163605ustar00rootroot00000000000000mapproxy-1.11.0/doc/_templates/layout.html000066400000000000000000000016661320454472400205740ustar00rootroot00000000000000{% extends "!layout.html" %} {% set bootswatch_css_custom = ['_static/mapproxy.css'] %} {%- block content %} {{ navBar() }}
{%- block sidebar1 %}{{ bsidebar() }}{% endblock %}
{% block body %}{% endblock %}
{%- endblock %} {%- block footer %}

Back to top

{% trans copyright=copyright|e %} © Copyright {{ copyright }}, Legal {% endtrans %}
{% trans last_updated=last_updated|e %} Last updated on {{ last_updated }} {% endtrans %}
{% trans sphinx_version=sphinx_version|e %} Created using Sphinx {{ sphinx_version }}. {% endtrans %}

{%- endblock %} mapproxy-1.11.0/doc/_templates/navbar.html000066400000000000000000000022721320454472400205220ustar00rootroot00000000000000 mapproxy-1.11.0/doc/_templates/toctree.html000077500000000000000000000000171320454472400207140ustar00rootroot00000000000000{{ toctree() }}mapproxy-1.11.0/doc/auth.rst000066400000000000000000000461161320454472400157260ustar00rootroot00000000000000Authentication and Authorization ================================ Authentication is the process of mapping a request to a user. There are different ways to do this, from simple HTTP Basic Authentication to cookies or token based systems. Authorization is the process that defines what an authenticated user is allowed to do. A datastore is required to store this authorization information for everything but trivial systems. These datastores can range from really simple text files (all users in this text file are allowed to do everything) to complex schemas with relational databases (user A is allowed to do B but not C, etc.). As you can see, the options to choose when implementing a system for authentication and authorization are diverse. Developers (of SDIs, not the software itself) often have specific constraints, like existing user data in a database or an existing login page on a website for a Web-GIS. So it is hard to offer a one-size-fits-all solution. Therefore, MapProxy does not come with any embedded authentication or authorization. But it comes with a flexible authorization interface that allows you (the SDI developer) to implement custom tailored systems. Luckily, there are lots of existing toolkits that can be used to build systems that match your requirements. For authentication there is the `repoze.who`_ package with `plugins for HTTP Basic Authentication, HTTP cookies, etc`_. For authorization there is the `repoze.what`_ package with `plugins for SQL datastores, etc`_. .. _`repoze.who`: http://docs.repoze.org/who/ .. _`plugins for HTTP Basic Authentication, HTTP cookies, etc`: http://pypi.python.org/pypi?:action=search&term=repoze.who .. _`repoze.what`: http://docs.repoze.org/what/ .. _`plugins for SQL datastores, etc`: http://pypi.python.org/pypi?:action=search&term=repoze.what .. note:: Developing custom authentication and authorization system requires a bit Python programming and knowledge of `WSGI `_ and WSGI middleware. Authentication/Authorization Middleware --------------------------------------- Your auth system should be implemented as a WSGI middleware. The middleware sits between your web server and the MapProxy. WSGI Filter Middleware ~~~~~~~~~~~~~~~~~~~~~~ A simple middleware that authorizes random requests might look like:: class RandomAuthFilter(object): def __init__(self, app, global_conf): self.app = app def __call__(self, environ, start_response): if random.randint(0, 1) == 1: return self.app(environ, start_response) else: start_response('403 Forbidden', [('content-type', 'text/plain')]) return ['no luck today'] You need to wrap the MapProxy application with your custom auth middleware. For deployment scripts it might look like:: application = make_wsgi_app('./mapproxy.yaml') application = RandomAuthFilter(application) For `PasteDeploy`_ you can use the ``filter-with`` option. The ``config.ini`` looks like:: [app:mapproxy] use = egg:MapProxy#app mapproxy_conf = %(here)s/mapproxy.yaml filter-with = auth [filter:auth] paste.filter_app_factory = myauthmodule:RandomAuthFilter [server:main] ... You can implement simple authentication systems with that method, but you should look at `repoze.who`_ before reinventing the wheel. .. _`PasteDeploy`: http://pythonpaste.org/deploy/ Authorization Callback ~~~~~~~~~~~~~~~~~~~~~~ Authorization is a bit more complex, because your middleware would need to interpret the request to get information required for the authorization (e.g. layer names for WMS GetMap requests). Limiting the GetCapabilities response to certain layers would even require the middleware to manipulate the XML document. So it's obvious that some parts of the authorization should be handled by MapProxy. MapProxy can call the middleware back for authorization as soon as it knows what to ask for (e.g. the layer names of a WMS GetMap request). You have to pass a callback function to the environment so that MapProxy knows what to call. Here is a more elaborate example that denies requests to all layers that start with a specific prefix. These layers are also hidden from capability documents. :: class SimpleAuthFilter(object): """ Simple MapProxy authorization middleware. It authorizes WMS requests for layers where the name does not start with `prefix`. """ def __init__(self, app, prefix='secure'): self.app = app self.prefix = prefix def __call__(self, environ, start_response): # put authorize callback function into environment environ['mapproxy.authorize'] = self.authorize return self.app(environ, start_response) def authorize(self, service, layers=[], environ=None, **kw): allowed = denied = False if service.startswith('wms.'): auth_layers = {} for layer in layers: if layer.startswith(self.prefix): auth_layers[layer] = {} denied = True else: auth_layers[layer] = { 'map': True, 'featureinfo': True, 'legendgraphic': True, } allowed = True else: # other services are denied return {'authorized': 'none'} if allowed and not denied: return {'authorized': 'full'} if denied and not allowed: return {'authorized': 'none'} return {'authorized': 'partial', 'layers': auth_layers} And here is the part of the ``config.py`` where we define the filter and pass custom options:: application = make_wsgi_app('./mapproxy.yaml') application = SimpleAuthFilter(application, prefix='secure') MapProxy Authorization API -------------------------- MapProxy looks in the request environment for a ``mapproxy.authorize`` entry. This entry should contain a callable (function or method). If it does not find any callable, then MapProxy assumes that authorization is not enabled and that all requests are allowed. The signature of the authorization function: .. function:: authorize(service, layers=[], environ=None, **kw) :param service: service that should be authorized :param layers: list of layer names that should be authorized :param environ: the request environ :rtype: dictionary with authorization information The arguments might get extended in future versions of MapProxy. Therefore you should collect further arguments in a catch-all keyword argument (i.e. ``**kw``). .. note:: The actual name of the callable is insignificant, only the environment key ``mapproxy.authorize`` is important. The ``service`` parameter is a string and the content depends on the service that calls the authorize function. Generally, it is the lower-case name of the service (e.g. ``tms`` for TMS service), but it can be different to further control the service (e.g. ``wms.map``). The function should return a dictionary with the authorization information. The expected content of that dictionary can vary with each service. Only the ``authorized`` key is consistent with all services. The ``authorized`` entry can have four values. ``full`` The request for the given `service` and `layers` is fully authorized. MapProxy handles the request as if there is no authorization. ``partial`` Only parts of the request are allowed. The dictionary should contains more information on what parts of the request are allowed and what parts are denied. Depending on the service, MapProxy can then filter the request based on that information, e.g. return WMS Capabilities with permitted layers only. ``none`` The request is denied and MapProxy returns an HTTP 403 (Forbidden) response. ``unauthenticated`` The request(er) was not authenticated and MapProxy returns an HTTP 401 response. Your middleware can capture this and ask the requester for authentication. ``repoze.who``'s ``PluggableAuthenticationMiddleware`` will do this for example. .. versionadded:: 1.1.0 The ``environment`` parameter and support for ``authorized: unauthenticated`` results. .. _limited_to: ``limited_to`` ~~~~~~~~~~~~~~ You can restrict the geographical area for each request. MapProxy will clip each request to the provided geometry – areas outside of the permitted area become transparent. Depending on the service, MapProxy supports this clipping for the whole request or for each layer. You need to provide a dictionary with ``bbox`` or ``geometry`` and the ``srs`` of the geometry. The following geometry values are supported: BBOX: Bounding box as a list of minx, miny, maxx, maxy. WKT polygons: String with one or more polygons and multipolygons as WKT. Multiple WKTs must be delimited by a new line character. Return this type if you are getting the geometries from a spatial database. Shapely geometry: Shapely geometry object. Return this type if you already processing the geometries in your Python code with `Shapely `_. Here is an example callback result for a WMS `GetMap` request with all three geometry types. See below for examples for other services:: { 'authorized': 'partial', 'layers': { 'layer1': { 'map': True, 'limited_to': { 'geometry': [-10, 0, 30, 50], 'srs': 'EPSG:4326', }, }, 'layer2': { 'map': True, 'limited_to': { 'geometry': 'POLYGON((...))', 'srs': 'EPSG:4326', }, }, 'layer3': { 'map': True, 'limited_to': { 'geometry': shapely.geometry.Polygon( [(-10, 0), (30, -5), (30, 50), (20, 50)]), 'srs': 'EPSG:4326', } } } } Performance ^^^^^^^^^^^ The clipping is quite fast, but if you notice that the overhead is to large, you should reduce the complexity of the geometries returned by your authorization callback. You can improve the performance by returning the geometry in the projection from ``query_extent``, by limiting it to the ``query_extent`` and by simplifing the geometry. Refer to the ``ST_Transform``, ``ST_Intersection`` and ``ST_SimplifyPreserveTopology`` functions when you query the geometries from PostGIS. WMS Service ----------- The WMS service expects a ``layers`` entry in the authorization dictionary for ``partial`` results. ``layers`` itself should be a dictionary with all layers. All missing layers are interpreted as denied layers. Each layer contains the information about the permitted features. A missing feature is interpreted as a denied feature. Here is an example result of a call to the authorize function:: { 'authorized': 'partial', 'layers': { 'layer1': { 'map': True, 'featureinfo': False, }, 'layer2': { 'map': True, 'featureinfo': True, } } } ``limited_to`` ~~~~~~~~~~~~~~ .. versionadded:: 1.4.0 The WMS service supports ``limited_to`` for `GetCapabilities`, `GetMap` and `GetFeatureInfo` requests. MapProxy will modify the bounding box of each restricted layer for `GetCapabilities` requests. `GetFeatureInfo` requests will only return data if the info coordinate is inside the permitted area. For `GetMap` requests, MapProxy will clip each layer to the provided geometry – areas outside of the permitted area become transparent or colored in the `bgcolor` of the WMS request. You can provide the geometry for each layer or for the whole request. See :ref:`limited_to` for more details. Here is an example callback result with two limited layers and one unlimited layer:: { 'authorized': 'partial', 'layers': { 'layer1': { 'map': True, 'limited_to': { 'geometry': [-10, 0, 30, 50], 'srs': 'EPSG:4326', }, }, 'layer2': { 'map': True, 'limited_to': { 'geometry': 'POLYGON((...))', 'srs': 'EPSG:4326', }, }, 'layer3': { 'map': True, } } } Here is an example callback result where the complete request is limited:: { 'authorized': 'partial', 'limited_to': { 'geometry': shapely.geometry.Polygon( [(-10, 0), (30, -5), (30, 50), (20, 50)]), 'srs': 'EPSG:4326', }, 'layers': { 'layer1': { 'map': True, }, } } Service types ~~~~~~~~~~~~~ The WMS service uses the following service strings: ``wms.map`` ^^^^^^^^^^^ This is called for WMS GetMap requests. ``layers`` is a list with the actual layers to render, that means that group layers are resolved. The ``map`` feature needs to be set to ``True`` for each permitted layer. The whole request is rejected if any requested layer is not permitted. Resolved layers (i.e. sub layers of a requested group layer) are filtered out if they are not permitted. .. versionadded:: 1.1.0 The ``authorize`` function gets called with an additional ``query_extent`` argument: .. function:: authorize(service, environ, layers, query_extent, **kw) :param query_extent: a tuple of the SRS (e.g. ``EPSG:4326``) and the BBOX of the request to authorize. Example +++++++ With a layer tree like:: - name: layer1 layers: - name: layer1a sources: [l1a] - name: layer1b sources: [l1b] An authorize result of:: { 'authorized': 'partial', 'layers': { 'layer1': {'map': True}, 'layer1a': {'map': True} } } Results in the following: - A request for ``layer1`` renders ``layer1a``, ``layer1b`` gets filtered out. - A request for ``layer1a`` renders ``layer1a``. - A request for ``layer1b`` is rejected. - A request for ``layer1a`` and ``layer1b`` is rejected. ``wms.featureinfo`` ^^^^^^^^^^^^^^^^^^^ This is called for WMS GetFeatureInfo requests and the behavior is similar to ``wms.map``. ``wms.capabilities`` ^^^^^^^^^^^^^^^^^^^^ This is called for WMS GetCapabilities requests. ``layers`` is a list with all named layers of the WMS service. Only layers with the ``map`` feature set to ``True`` are included in the capabilities document. Missing layers are not included. Sub layers are only included when the parent layer is included, since authorization interface is not able to reorder the layer tree. Note, that you are still able to request these sub layers (see ``wms.map`` above). Layers that are queryable and only marked so in the capabilities if the ``featureinfo`` feature set to ``True``. With a layer tree like:: - name: layer1 layers: - name: layer1a sources: [l1a] - name: layer1b sources: [l1b] - name: layer1c sources: [l1c] An authorize result of:: { 'authorized': 'partial', 'layers': { 'layer1': {'map': True, 'feature': True}, 'layer1a': {'map': True, 'feature': True}, 'layer1b': {'map': True}, 'layer1c': {'map': True}, } } Results in the following abbreviated capabilities:: layer1 layer1a layer1b TMS/Tile Service ---------------- The TMS service expects a ``layers`` entry in the authorization dictionary for ``partial`` results. ``layers`` itself should be a dictionary with all layers. All missing layers are interpreted as denied layers. Each layer contains the information about the permitted features. The TMS service only supports the ``tile`` feature. A missing feature is interpreted as a denied feature. Here is an example result of a call to the authorize function:: { 'authorized': 'partial', 'layers': { 'layer1': {'tile': True}, 'layer2': {'tile': False}, } } The TMS service uses ``tms`` as the service string for all authorization requests. Only layers with the ``tile`` feature set to ``True`` are included in the TMS capabilities document (``/tms/1.0.0``). Missing layers are not included. The ``authorize`` function gets called with an additional ``query_extent`` argument for all tile requests: .. function:: authorize(service, environ, layers, query_extent=None, **kw) :param query_extent: a tuple of the SRS (e.g. ``EPSG:4326``) and the BBOX of the request to authorize, or ``None`` for capabilities requests. ``limited_to`` ~~~~~~~~~~~~~~ .. versionadded:: 1.5.0 MapProxy will clip each tile to the provided geometry – areas outside of the permitted area become transparent. MapProxy will return PNG images in this case. Here is an example callback result where the tile request is limited:: { 'authorized': 'partial', 'limited_to': { 'geometry': shapely.geometry.Polygon( [(-10, 0), (30, -5), (30, 50), (20, 50)]), 'srs': 'EPSG:4326', }, 'layers': { 'layer1': { 'tile': True, }, } } .. versionadded:: 1.5.1 You can also add the limit to the layer and mix it with properties used for the other services:: { 'authorized': 'partial', 'layers': { 'layer1': { 'tile': True, 'map': True, 'limited_to': { 'geometry': shapely.geometry.Polygon( [(-10, 0), (30, -5), (30, 50), (20, 50)]), 'srs': 'EPSG:4326', }, 'layer2': { 'tile': True, 'map': False, 'featureinfo': True, 'limited_to': { 'geometry': shapely.geometry.Polygon( [(0, 0), (20, -5), (30, 50), (20, 50)]), 'srs': 'EPSG:4326', }, }, } } See :ref:`limited_to` for more details. KML Service ----------- The KML authorization is similar to the TMS authorization, including the ``limited_to`` option. The KML service uses ``kml`` as the service string for all authorization requests. WMTS Service ------------ The WMTS authorization is similar to the TMS authorization, including the ``limited_to`` option. The WMTS service uses ``wmts`` as the service string for all authorization requests. Demo Service ------------ The demo service only supports ``full`` or ``none`` authorization. ``layers`` is always an empty list. The demo service does not authorize the services and layers that are listed in the overview page. If you permit a user to access the demo service, then he can see all services and layers names. However, access to these services is still restricted to the according authorization. The service string is ``demo``. MultiMapProxy ------------- The :ref:`MultiMapProxy ` application stores the instance name in the environment as ``mapproxy.instance_name``. This information in not available when your middleware gets called, but you can use it in your authorization function. Example that rejects MapProxy instances where the name starts with ``secure``. :: class MultiMapProxyAuthFilter(object): def __init__(self, app, global_conf): self.app = app def __call__(self, environ, start_response): environ['mapproxy.authorize'] = self.authorize return self.app(environ, start_response) def authorize(self, service, layers=[]): instance_name = environ.get('mapproxy.instance_name', '') if instance_name.startswith('secure'): return {'authorized': 'none'} else: return {'authorized': 'full'} mapproxy-1.11.0/doc/caches.rst000066400000000000000000000434351320454472400162140ustar00rootroot00000000000000Caches ###### .. versionadded:: 1.2.0 MapProxy supports multiple backends to store the internal tiles. The default backend is file based and does not require any further configuration. Configuration ============= You can configure a backend for each cache with the ``cache`` option. Each backend has a ``type`` and one or more options. :: caches: mycache: sources: [...] grids: [...] cache: type: backendtype backendoption1: value backendoption2: value The following backend types are available. - :ref:`cache_file` - :ref:`cache_mbtiles` - :ref:`cache_sqlite` - :ref:`cache_geopackage` - :ref:`cache_couchdb` - :ref:`cache_riak` - :ref:`cache_redis` - :ref:`cache_s3` - :ref:`cache_compact` .. _cache_file: ``file`` ======== This is the default cache type and it uses a single file for each tile. Available options are: ``directory_layout``: The directory layout MapProxy uses to store tiles on disk. Defaults to ``tc`` which uses a TileCache compatible directory layout (``zz/xxx/xxx/xxx/yyy/yyy/yyy.format``). ``mp`` uses a directory layout with less nesting (``zz/xxxx/xxxx/yyyy/yyyy.format```). ``tms`` uses TMS compatible directories (``zz/xxxx/yyyy.format``). ``quadkey`` uses Microsoft Virtual Earth or quadkey compatible directories (see http://msdn.microsoft.com/en-us/library/bb259689.aspx). ``arcgis`` uses a directory layout with hexadecimal row and column numbers that is compatible to ArcGIS exploded caches (``Lzz/Rxxxxxxxx/Cyyyyyyyy.format``). .. note:: ``tms``, ``quadkey`` and ``arcgis`` layout are not suited for large caches, since it will create directories with thousands of files, which most file systems do not handle well. ``use_grid_names``: When ``true`` MapProxy will use the actual grid name in the path instead of the SRS code. E.g. tiles will be stored in ``./cache_data/mylayer/mygrid/`` instead of ``./cache_data/mylayer/EPSG1234/``. .. versionadded:: 1.5.0 .. _cache_file_directory: ``directory``: Directory where MapProxy should directly store the tiles. This will not add the cache name or grid name (``use_grid_name``) to the path. You can use this option to point MapProxy to an existing tile collection (created with ``gdal2tiles`` for example). .. versionadded:: 1.5.0 ``tile_lock_dir``: Directory where MapProxy should write lock files when it creates new tiles for this cache. Defaults to ``cache_data/tile_locks``. .. versionadded:: 1.6.0 .. _cache_mbtiles: ``mbtiles`` =========== Use a single SQLite file for this cache. It uses the `MBTile specification `_. Available options: ``filename``: The path to the MBTiles file. Defaults to ``cachename.mbtiles``. ``tile_lock_dir``: Directory where MapProxy should write lock files when it creates new tiles for this cache. Defaults to ``cache_data/tile_locks``. .. versionadded:: 1.6.0 You can set the ``sources`` to an empty list, if you use an existing MBTiles file and do not have a source. :: caches: mbtiles_cache: sources: [] grids: [GLOBAL_MERCATOR] cache: type: mbtiles filename: /path/to/bluemarble.mbtiles .. note:: The MBTiles format specification does not include any timestamps for each tile and the seeding function is limited therefore. If you include any ``refresh_before`` time in a seed task, all tiles will be recreated regardless of the value. The cleanup process does not support any ``remove_before`` times for MBTiles and it always removes all tiles. Use the ``--summary`` option of the ``mapproxy-seed`` tool. The note about ``bulk_meta_tiles`` for SQLite below applies to MBtiles as well. .. _cache_sqlite: ``sqlite`` =========== .. versionadded:: 1.6.0 Use SQLite databases to store the tiles, similar to ``mbtiles`` cache. The difference to ``mbtiles`` cache is that the ``sqlite`` cache stores each level into a separate database. This makes it easy to remove complete levels during mapproxy-seed cleanup processes. The ``sqlite`` cache also stores the timestamp of each tile. Available options: ``dirname``: The directory where the level databases will be stored. ``tile_lock_dir``: Directory where MapProxy should write lock files when it creates new tiles for this cache. Defaults to ``cache_data/tile_locks``. .. versionadded:: 1.6.0 :: caches: sqlite_cache: sources: [mywms] grids: [GLOBAL_MERCATOR] cache: type: sqlite directory: /path/to/cache .. note:: .. versionadded:: 1.10.0 All tiles from a meta tile request are stored in one transaction into the SQLite file to increase performance. You need to activate the :ref:`bulk_meta_tiles ` option to get the same benefit when you are using tiled sources. :: caches: sqlite_cache: sources: [mytilesource] bulk_meta_tiles: true grids: [GLOBAL_MERCATOR] cache: type: sqlite directory: /path/to/cache .. _cache_couchdb: ``couchdb`` =========== .. versionadded:: 1.3.0 Store tiles inside a `CouchDB `_. MapProxy creates a JSON document for each tile. This document contains metadata, like timestamps, and the tile image itself as a attachment. Requirements ------------ Besides a running CouchDB you will need the `Python requests package `_. You can install it the usual way, for example ``pip install requests``. Configuration ------------- You can configure the database and database name and the tile ID and additional metadata. Available options: ``url``: The URL of the CouchDB server. Defaults to ``http://localhost:5984``. ``db_name``: The name of the database MapProxy uses for this cache. Defaults to the name of the cache. ``tile_lock_dir``: Directory where MapProxy should write lock files when it creates new tiles for this cache. Defaults to ``cache_data/tile_locks``. .. versionadded:: 1.6.0 ``tile_id``: Each tile document needs a unique ID. You can change the format with a Python format string that expects the following keys: ``x``, ``y``, ``z``: The tile coordinate. ``grid_name``: The name of the grid. The default ID uses the following format:: %(grid_name)s-%(z)d-%(x)d-%(y)d .. note:: You can't use slashes (``/``) in CouchDB IDs. ``tile_metadata``: MapProxy stores a JSON document for each tile in CouchDB and you can add additional key-value pairs with metadata to each document. There are a few predefined values that MapProxy will replace with tile-depended values, all other values will be added as they are. Predefined values: ``{{x}}``, ``{{y}}``, ``{{z}}``: The tile coordinate. ``{{timestamp}}``: The creation time of the tile as seconds since epoch. MapProxy will add a ``timestamp`` key for you, if you don't provide a custom timestamp key. ``{{utc_iso}}``: The creation time of the tile in UTC in ISO format. For example: ``2011-12-31T23:59:59Z``. ``{{tile_centroid}}``: The center coordinate of the tile in the cache's coordinate system as a list of long/lat or x/y values. ``{{wgs_tile_centroid}}``: The center coordinate of the tile in WGS 84 as a list of long/lat values. Example ------- :: caches: mycouchdbcache: sources: [mywms] grids: [mygrid] cache: type: couchdb url: http://localhost:9999 db_name: mywms_tiles tile_metadata: mydata: myvalue tile_col: '{{x}}' tile_row: '{{y}}' tile_level: '{{z}}' created_ts: '{{timestamp}}' created: '{{utc_iso}}' center: '{{wgs_tile_centroid}}' MapProxy will place the JSON document for tile z=3, x=1, y=2 at ``http://localhost:9999/mywms_tiles/mygrid-3-1-2``. The document will look like:: { "_attachments": { "tile": { "content_type": "image/png", "digest": "md5-ch4j5Piov6a5FlAZtwPVhQ==", "length": 921, "revpos": 2, "stub": true } }, "_id": "mygrid-3-1-2", "_rev": "2-9932acafd060e10bc0db23231574f933", "center": [ -112.5, -55.7765730186677 ], "created": "2011-12-15T12:56:21Z", "created_ts": 1323953781.531889, "mydata": "myvalue", "tile_col": 1, "tile_level": 3, "tile_row": 2 } The ``_attachments``-part is the internal structure of CouchDB where the tile itself is stored. You can access the tile directly at: ``http://localhost:9999/mywms_tiles/mygrid-3-1-2/tile``. .. _cache_riak: ``riak`` ======== .. versionadded:: 1.6.0 Store tiles in a `Riak `_ cluster. MapProxy creates keys with binary data as value and timestamps as user defined metadata. This backend is good for very large caches which can be distributed over many nodes. Data can be distributed over multiple nodes providing a fault-tolernt and high-available storage. A Riak cluster is masterless and each node can handle read and write requests. Requirements ------------ You will need the `Python Riak client `_ version 2.4.2 or older. You can install it in the usual way, for example with ``pip install riak==2.4.2``. Environments with older version must be upgraded with ``pip install -U riak==2.4.2``. Python library depends on packages `python-dev`, `libffi-dev` and `libssl-dev`. Configuration ------------- Available options: ``nodes``: A list of riak nodes. Each node needs a ``host`` and optionally a ``pb_port`` and an ``http_port`` if the ports differ from the default. Defaults to single localhost node. ``protocol``: Communication protocol. Allowed options is ``http``, ``https`` and ``pbc``. Defaults to ``pbc``. ``bucket``: The name of the bucket MapProxy uses for this cache. The bucket is the namespace for the tiles and must be unique for each cache. Defaults to cache name suffixed with grid name (e.g. ``mycache_webmercator``). ``default_ports``: Default ``pb`` and ``http`` ports for ``pbc`` and ``http`` protocols. Will be used as the default for each defined node. ``secondary_index``: If ``true`` enables secondary index for tiles. This improves seed cleanup performance but requires that Riak uses LevelDB as the backend. Refer to the Riak documentation. Defaults to ``false``. Example ------- :: myriakcache: sources: [mywms] grids: [mygrid] cache: type: riak nodes: - host: 1.example.org pb_port: 9999 - host: 1.example.org - host: 1.example.org protocol: pbc bucket: myriakcachetiles default_ports: pb: 8087 http: 8098 .. _cache_redis: ``redis`` ========= .. versionadded:: 1.10.0 Store tiles in a `Redis `_ in-memory database. This backend is useful for short-term caching. Typical use-case is a small Redis cache that allows you to benefit from meta-tiling. Your Redis database should be configured with ``maxmemory`` and ``maxmemory-policy`` options to limit the memory usage. For example:: maxmemory 256mb maxmemory-policy volatile-ttl Requirements ------------ You will need the `Python Redis client `_. You can install it in the usual way, for example with ``pip install redis``. Configuration ------------- Available options: ``host``: Host name of the Redis server. Defaults to ``127.0.0.1``. ``port``: Port of the Redis server. Defaults to ``6379``. ``db``: Number of the Redis database. Please refer to the Redis documentation. Defaults to `0`. ``prefix``: The prefix added to each tile-key in the Redis cache. Used to distinguish tiles from different caches and grids. Defaults to ``cache-name_grid-name``. ``default_ttl``: The default Time-To-Live of each tile in the Redis cache in seconds. Defaults to 3600 seconds (1 hour). Example ------- :: redis_cache: sources: [mywms] grids: [mygrid] cache: type: redis default_ttl: 600 .. _cache_geopackage: ``geopackage`` ============== .. versionadded:: 1.10.0 Store tiles in a `geopackage `_ database. MapProxy creates a tile table if one isn't defined and populates the required meta data fields. This backend is good for datasets that require portability. Available options: ``filename``: The path to the geopackage file. Defaults to ``cachename.gpkg``. ``table_name``: The name of the table where the tiles should be stored (or retrieved if using an existing cache). Defaults to the ``cachename_gridname``. ``levels``: Set this to true to cache to a directory where each level is stored in a separate geopackage. Defaults to ``false``. If set to true, ``filename`` is ignored. ``directory``: If levels is true use this to specify the directory to store geopackage files. You can set the ``sources`` to an empty list, if you use an existing geopackage file and do not have a source. :: caches: geopackage_cache: sources: [] grids: [GLOBAL_MERCATOR] cache: type: geopackage filename: /path/to/bluemarble.gpkg table_name: bluemarble_tiles .. note:: The geopackage format specification does not include any timestamps for each tile and the seeding function is limited therefore. If you include any ``refresh_before`` time in a seed task, all tiles will be recreated regardless of the value. The cleanup process does not support any ``remove_before`` times for geopackage and it always removes all tiles. Use the ``--summary`` option of the ``mapproxy-seed`` tool. .. _cache_s3: ``s3`` ====== .. versionadded:: 1.10.0 Store tiles in a `Amazon Simple Storage Service (S3) `_. Requirements ------------ You will need the Python `boto3 `_ package. You can install it in the usual way, for example with ``pip install boto3``. Configuration ------------- Available options: ``bucket_name``: The bucket used for this cache. You can set the default bucket with ``globals.cache.s3.bucket_name``. ``profile_name``: Optional profile name for `shared credentials `_ for this cache. Alternative methods of authentification are using the ``AWS_ACCESS_KEY_ID`` and ``AWS_SECRET_ACCESS_KEY`` environmental variables, or by using an `IAM role `_ when using an Amazon EC2 instance. You can set the default profile with ``globals.cache.s3.profile_name``. ``directory``: Base directory (path) where all tiles are stored. ``directory_layout``: Defines the directory layout for the tiles (``12/12345/67890.png``, ``L12/R00010932/C00003039.png``, etc.). See :ref:`cache_file` for available options. Defaults to ``tms`` (e.g. ``12/12345/67890.png``). This cache cache also supports ``reverse_tms`` where tiles are stored as ``y/x/z.format``. See *note* below. .. note:: The hierarchical ``directory_layouts`` can hit limitations of S3 *"if you are routinely processing 100 or more requests per second"*. ``directory_layout: reverse_tms`` can work around this limitation. Please read `S3 Request Rate and Performance Considerations `_ for more information on this issue. Example ------- :: cache: my_layer_20110501_epsg_4326_cache_out: sources: [my_layer_20110501_cache] cache: type: s3 directory: /1.0.0/my_layer/default/20110501/4326/ bucket_name: my-s3-tiles-cache globals: cache: s3: profile_name: default .. _cache_compact: ``compact`` =========== .. versionadded:: 1.10.0 Support for format version 1 .. versionadded:: 1.11.0 Support for format version 2 Store tiles in ArcGIS compatible compact cache files. A single compact cache ``.bundle`` file stores up to about 16,000 tiles. Version 1 of the compact cache format is compatible with ArcGIS 10.0 and the default version of ArcGIS 10.0-10.2. Version 2 is supported by ArcGIS 10.3 or higher. Version 1 stores is one additional ``.bundlx`` index file for each ``.bundle`` data file. Available options: ``directory``: Directory where MapProxy should store the level directories. This will not add the cache name or grid name to the path. You can use this option to point MapProxy to an existing compact cache. ``version``: The version of the ArcGIS compact cache format. This option is required. Either ``1`` or ``2``. You can set the ``sources`` to an empty list, if you use an existing compact cache files and do not have a source. The following configuration will load tiles from ``/path/to/cache/L00/R0000C0000.bundle``, etc. :: caches: compact_cache: sources: [] grids: [webmercator] cache: type: compact version: 2 directory: /path/to/cache .. note:: MapProxy does not support reading and writiting of the ``conf.cdi`` and ``conf.xml`` files. You need to configure a compatible MapProxy grid when you want to reuse exsting ArcGIS compact caches in MapProxy. You need to create or modify existing ``conf.cdi`` and ``conf.xml`` files when you want to use compact caches created with MapProxy in ArcGIS. .. note:: The compact cache format does not include any timestamps for each tile and the seeding function is limited therefore. If you include any ``refresh_before`` time in a seed task, all tiles will be recreated regardless of the value. The cleanup process does not support any ``remove_before`` times for compact caches and it always removes all tiles. Use the ``--summary`` option of the ``mapproxy-seed`` tool. .. note:: The compact cache format is append-only to allow parallel read and write operations. Removing or refreshing tiles with ``mapproxy-seed`` does not reduce the size of the cache files. You can use the :ref:`defrag-compact-cache ` util to reduce the file size of existing bundle files. mapproxy-1.11.0/doc/conf.py000066400000000000000000000150111320454472400155200ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # MapProxy documentation build configuration file, created by # sphinx-quickstart on Thu Feb 25 15:36:04 2010. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os import sphinx_bootstrap_theme # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.append(os.path.abspath('.')) sys.path.append(os.path.abspath('_themes')) # -- General configuration ----------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo'] todo_include_todos = False # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8' # The master toctree document. master_doc = 'index' # General information about the project. project = u'MapProxy' copyright = u'Oliver Tonnhofer, Omniscale' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '1.11' # The full version, including alpha/beta/rc tags. release = '1.11.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%Y-%m-%d' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = 'bootstrap' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = { 'navbar_sidebarrel': False, 'navbar_pagenav': False, 'navbar_fixed_top': False, 'source_link_position': False, } # Add any paths that contain custom themes here, relative to this directory. html_theme_path = sphinx_bootstrap_theme.get_html_theme_path() # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". html_title = "MapProxy %s Docs" % (release, ) # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = '_static/logo.png' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%Y-%m-%d' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. html_sidebars = {'**': ['toctree.html']} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'MapProxydoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). latex_paper_size = 'a4' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'MapProxy.tex', u'MapProxy Documentation', u'Oliver Tonnhofer', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True mapproxy-1.11.0/doc/configuration.rst000066400000000000000000001210461320454472400176300ustar00rootroot00000000000000Configuration ############# There are two configuration files used by MapProxy. ``mappproxy.yaml`` This is the main configuration of MapProxy. It configures all aspects of the server: Which servers should be started, where comes the data from, what should be cached, etc.. ``seed.yaml`` This file is the configuration for the ``mapproxy-seed`` tool. See :doc:`seeding documentation ` for more information. .. index:: mapproxy.yaml mapproxy.yaml ------------- The configuration uses the YAML format. The Wikipedia contains a `good introduction to YAML `_. The MapProxy configuration is grouped into sections, each configures a different aspect of MapProxy. These are the following sections: - ``globals``: Internals of MapProxy and default values that are used in the other configuration sections. - ``services``: The services MapProxy offers, e.g. WMS or TMS. - ``sources``: Define where MapProxy can retrieve new data. - ``caches``: Configure the internal caches. - ``layers``: Configure the layers that MapProxy offers. Each layer can consist of multiple sources and caches. - ``grids``: Define the grids that MapProxy uses to aligns cached images. The order of the sections is not important, so you can organize it your way. .. note:: The indentation is significant and shall only contain space characters. Tabulators are **not** permitted for indentation. There is another optional section: .. versionadded:: 1.6.0 - ``parts``: YAML supports references and with that you can define configuration parts and use them in other configuration sections. For example, you can define all you coverages in one place and reference them from the sources. However, MapProxy will log a warning if you put the referent in a place where it is not a valid option. To prevent these warnings you are advised to put these configuration snippets inside the ``parts`` section. For example:: parts: coverages: mycoverage: &mycoverage bbox: [0, 0, 10, 10] srs: 'EPSG:4326' sources: mysource1: coverage: *mycoverage ... mysource2: coverage: *mycoverage ... ``base`` """""""" You can split a configuration into multiple files with the ``base`` option. The ``base`` option loads the other files and merges the loaded configuration dictionaries together – it is not a literal include of the other files. For example:: base: [mygrids.yaml, mycaches_sources.yaml] service: ... layers: ... .. versionchanged:: 1.4.0 Support for recursive imports and for multiple files. .. ################################################################################# .. index:: services services -------- Here you can configure which services should be started. The configuration for all services is described in the :doc:`services` documentation. Here is an example:: services: tms: wms: md: title: MapProxy Example WMS contact: # [...] .. ################################################################################# .. index:: layers .. _layers_section: layers ------ Here you can define all layers MapProxy should offer. The layer definition is similar to WMS: each layer can have a name and title and you can nest layers to build a layer tree. Layers should be configured as a list (``-`` in YAML), where each layer configuration is a dictionary (``key: value`` in YAML) :: layers: - name: layer1 title: Title of Layer 1 sources: [cache1, source2] - name: layer2 title: Title of Layer 2 sources: [cache3] Each layer contains information about the layer and where the data comes from. .. versionchanged:: 1.4.0 The old syntax to configure each layer as a dictionary with the key as the name is deprecated. :: layers: mylayer: title: My Layer source: [mysource] should become :: layers: - name: mylayer title: My Layer source: [mysource] The mixed format where the layers are a list (``-``) but each layer is still a dictionary is no longer supported (e.g. ``- mylayer:`` becomes ``- name: mylayer``). .. _layers_name: ``name`` """"""""" The name of the layer. You can omit the name for group layers (e.g. layers with ``layers``), in this case the layer is not addressable in WMS and used only for grouping. ``title`` """"""""" Readable name of the layer, e.g WMS layer title. .. _layers: ``layers`` """""""""" Each layer can contain another ``layers`` configuration. You can use this to build group layers and to build a nested layer tree. For example:: layers: - name: root title: Root Layer layers: - name: layer1 title: Title of Layer 1 layers: - name: layer1a title: Title of Layer 1a sources: [source1a] - name: layer1b title: Title of Layer 1b sources: [source1b] - name: layer2 title: Title of Layer 2 sources: [cache2] ``root`` and ``layer1`` is a group layer in this case. The WMS service will render ``layer1a`` and ``layer1b`` if you request ``layer1``. Note that ``sources`` is optional if you supply ``layers``. You can still configure ``sources`` for group layers. In this case the group ``sources`` will replace the ``sources`` of the child layers. MapProxy will wrap all layers into an unnamed root layer, if you define multiple layers on the first level. .. note:: The old syntax (see ``name`` :ref:`above `) is not supported if you use the nested layer configuration format. ``sources`` """"""""""" A list of data sources for this layer. You can use sources defined in the ``sources`` and ``caches`` section. MapProxy will merge multiple sources from left (bottom) to right (top). WMS and Mapserver sources also support tagged names (``wms:lyr1,lyr2``). See :ref:`tagged_source_names`. ``tile_sources`` """""""""""""""" .. versionadded:: 1.8.2 A list of caches for this layer. This list overrides ``sources`` for WMTS and TMS. ``tile_sources`` are not merged like ``sources``, instead all the caches are added as additional tile (matrix) sets. ``min_res``, ``max_res`` or ``min_scale``, ``max_scale`` """""""""""""""""""""""""""""""""""""""""""""""""""""""" .. NOTE paragraph also in sources/wms section Limit the layer to the given min and max resolution or scale. MapProxy will return a blank image for requests outside of these boundaries (``min_res`` is inclusive, ``max_res`` exclusive). You can use either the resolution or the scale values, missing values will be interpreted as `unlimited`. Resolutions should be in meters per pixel. The values will also apear in the capabilities documents (i.e. WMS ScaleHint and Min/MaxScaleDenominator). Pleas read :ref:`scale vs. resolution ` for some notes on `scale`. ``legendurl`` """"""""""""" Configure a URL to an image that should be returned as the legend for this layer. Local URLs (``file://``) are also supported. MapProxy ignores the legends from the sources of this layer if you configure a ``legendurl`` here. .. _layer_metadata: ``md`` """""" .. versionadded:: 1.4.0 Add additional metadata for this layer. This metadata appears in the WMS 1.3.0 capabilities documents. Refer to the OGC 1.3.0 specification for a description of each option. See also :doc:`inspire` for configuring additional INSPIRE metadata. Here is an example layer with extended layer capabilities:: layers: - name: md_layer title: WMS layer with extended capabilities sources: [wms_source] md: abstract: Some abstract keyword_list: - vocabulary: Name of the vocabulary keywords: [keyword1, keyword2] - vocabulary: Name of another vocabulary keywords: [keyword1, keyword2] - keywords: ["keywords without vocabulary"] attribution: title: My attribution title url: http://example.org/ logo: url: http://example.org/logo.jpg width: 100 height: 100 format: image/jpeg identifier: - url: http://example.org/ name: HKU1234 value: Some value metadata: - url: http://example.org/metadata2.xml type: INSPIRE format: application/xml - url: http://example.org/metadata2.xml type: ISO19115:2003 format: application/xml data: - url: http://example.org/datasets/test.shp format: application/octet-stream - url: http://example.org/datasets/test.gml format: text/xml; subtype=gml/3.2.1 feature_list: - url: http://example.org/datasets/test.pdf format: application/pdf ``dimensions`` """""""""""""" .. versionadded:: 1.6.0 .. note:: Dimensions are only supported for uncached WMTS services for now. See :ref:`wmts_dimensions` for a working use-case. Configure the dimensions that this layer supports. Dimensions should be a dictionary with one entry for each dimension. Each dimension is another dictionary with a list of ``values`` and an optional ``default`` value. When the ``default`` value is omitted, the last value will be used. :: layers: - name: dimension_layer title: layer with dimensions sources: [cache] dimensions: time: values: - "2012-11-12T00:00:00" - "2012-11-13T00:00:00" - "2012-11-14T00:00:00" - "2012-11-15T00:00:00" default: "2012-11-15T00:00:00" elevation: values: - 0 - 1000 - 3000 .. ``attribution`` .. """""""""""""""" .. .. Overwrite the system-wide attribution line for this layer. .. .. ``inverse`` .. If this option is set to ``true``, the colors of the attribution will be inverted. Use this if the normal attribution is hard to on this layer (i.e. on aerial imagery). .. ################################################################################# .. index:: caches .. _caches: caches ------ Here you can configure which sources should be cached. Available options are: ``sources`` """"""""""" A list of data sources for this cache. You can use sources defined in the ``sources`` and ``caches`` section. This parameter is `required`. MapProxy will merge multiple sources from left (bottom) to right (top) before they are stored on disk. :: caches: my_cache: sources: [background_wms, overlay_wms] ... WMS and Mapserver sources also support tagged names (``wms:lyr1,lyr2``). See :ref:`tagged_source_names`. Band merging ^^^^^^^^^^^^ .. versionadded:: 1.9.0 You can also define a list of sources for each color band. The target color bands are specified as ``r``, ``g``, ``b`` for RGB images, optionally with ``a`` for the alpha band. You can also use ``l`` (luminance) to create tiles with a single color band (e.g. grayscale images). You need to define the ``source`` and the ``band`` index for each source band. The indices of the source bands are numeric and start from 0. The following example creates a colored infra-red (false-color) image by using near infra-red for red, red (band 0) for green, and green (band 1) for blue:: caches: cir_cache: sources: r: [{source: nir_cache, band: 0}] g: [{source: dop_cache, band: 0}] b: [{source: dop_cache, band: 1}] You can define multiple sources for each target band. The values are summed and clipped at 255. An optional ``factor`` allows you to reduce the values. You can use this to mix multiple bands into a single grayscale image:: caches: grayscale_cache: sources: l: [ {source: dop_cache, band: 0, factor: 0.21}, {source: dop_cache, band: 1, factor: 0.72}, {source: dop_cache, band: 2, factor: 0.07}, ] Cache sources ^^^^^^^^^^^^^ .. versionadded:: 1.5.0 You can also use other caches as a source. MapProxy loads tiles directly from that cache if the grid of the target cache is identical or *compatible* with the grid of the source cache. You have a compatible grid when all tiles in the cache grid are also available in source grid, even if the tile coordinates (X/Y/Z) are different. When the grids are not compatible, e.g. when they use different projections, then MapProxy will access the source cache as if it is a WMS source and it will use meta-requests and do image reprojection as necessary. See :ref:`using_existing_caches` for more information. .. _mixed_image_format: ``format`` """""""""" The internal image format for the cache. Available options are ``image/png``, ``image/jpeg`` etc. and ``mixed``. The default is ``image/png``. .. versionadded:: 1.5.0 With the ``mixed`` format, MapProxy stores tiles as either PNG or JPEG, depending on the transparency of each tile. Images with transparency will be stored as PNG, fully opaque images as JPEG. You need to set the ``request_format`` to ``image/png`` when using ``mixed``-mode:: caches: mixed_mode_cache: format: mixed request_format: image/png ... ``request_format`` """""""""""""""""" MapProxy will try to use this format to request new tiles, if it is not set ``format`` is used. This option has no effect if the source does not support that format or the format of the source is set explicitly (see ``suported_format`` or ``format`` for sources). .. _link_single_color_images: ``link_single_color_images`` """""""""""""""""""""""""""" If set to ``true``, MapProxy will not store tiles that only contain a single color as a separate file. MapProxy stores these tiles only once and uses symbolic links to this file for every occurrence. This can reduce the size of your tile cache if you have larger areas with no data (e.g. water areas, areas with no roads, etc.). .. note:: This feature is only available on Unix, since Windows has no support for symbolic links. ``minimize_meta_requests`` """""""""""""""""""""""""" If set to ``true``, MapProxy will only issue a single request to the source. This option can reduce the request latency for uncached areas (on demand caching). By default MapProxy requests all uncached meta-tiles that intersect the requested bbox. With a typical configuration it is not uncommon that a requests will trigger four requests each larger than 2000x2000 pixel. With the ``minimize_meta_requests`` option enabled, each request will trigger only one request to the source. That request will be aligned to the next tile boundaries and the tiles will be cached. .. index:: watermark ``watermark`` """"""""""""" Add a watermark right into the cached tiles. The watermark is thus also present in TMS or KML requests. ``text`` The watermark text. Should be short. ``opacity`` The opacity of the watermark (from 0 transparent to 255 full opaque). Use a value between 30 and 100 for unobtrusive watermarks. ``font_size`` Font size of the watermark text. ``color`` Color of the watermark text. Default is grey which works good for vector images. Can be either a list of color values (``[255, 255, 255]``) or a hex string (``#ffffff``). ``spacing`` Configure the spacing between repeated watermarks. By default the watermark will be placed on every tile, with ``wide`` the watermark will be placed on every second tile. ``grids`` """"""""" You can configure one or more grids for each cache. MapProxy will create one cache for each grid. :: grids: ['my_utm_grid', 'GLOBAL_MERCATOR'] MapProxy supports on-the-fly transformation of requests with different SRSs. So it is not required to add an extra cache for each supported SRS. For best performance only the SRS most requests are in should be used. There is some special handling for layers that need geographical and projected coordinate systems. For example, if you set one grid with ``EPSG:4326`` and one with ``EPSG:3857`` then all requests for projected SRS will access the ``EPSG:3857`` cache and requests for geographical SRS will use ``EPSG:4326``. ``meta_size`` and ``meta_buffer`` """"""""""""""""""""""""""""""""" Change the ``meta_size`` and ``meta_buffer`` of this cache. See :ref:`global cache options ` for more details. ``bulk_meta_tiles`` """"""""""""""""""" Enables meta-tile handling for tiled sources. See :ref:`global cache options ` for more details. ``image`` """"""""" :ref:`See below ` for all image options. ``use_direct_from_level`` and ``use_direct_from_res`` """"""""""""""""""""""""""""""""""""""""""""""""""""" You can limit until which resolution MapProxy should cache data with these two options. Requests below the configured resolution or level will be passed to the underlying source and the results will not be stored. The resolution of ``use_direct_from_res`` should use the units of the first configured grid of this cache. This takes only effect when used in WMS services. ``disable_storage`` """""""""""""""""""" If set to ``true``, MapProxy will not store any tiles for this cache. MapProxy will re-request all required tiles for each incoming request, even if the there are matching tiles in the cache. See :ref:`seed_only ` if you need an *offline* mode. .. note:: Be careful when using a cache with disabled storage in tile services when the cache uses WMS sources with metatiling. ``cache_dir`` """"""""""""" Directory where MapProxy should store tiles for this cache. Uses the value of ``globals.cache.base_dir`` by default. MapProxy will store each cache in a subdirectory named after the cache and the grid SRS (e.g. ``cachename_EPSG1234``). See :ref:`directory option` on how configure a complete path. ``cache`` """"""""" .. versionadded:: 1.2.0 Configure the type of the background tile cache. You configure the type with the ``type`` option. The default type is ``file`` and you can leave out the ``cache`` option if you want to use the file cache. Read :doc:`caches` for a detailed list of all available cache backends. Example ``caches`` configuration """""""""""""""""""""""""""""""" :: caches: simple: source: [mysource] grids: [mygrid] fullexample: source: [mysource, mysecondsource] grids: [mygrid, mygrid2] meta_size: [8, 8] meta_buffer: 256 watermark: text: MapProxy request_format: image/tiff format: image/jpeg cache: type: file directory_layout: tms .. ################################################################################# .. index:: grids .. _grids: grids ----- Here you can define the tile grids that MapProxy uses for the internal caching. There are multiple options to define the grid, but beware, not all are required at the same time and some combinations will result in ambiguous results. There are three pre-defined grids all with global coverage: - ``GLOBAL_GEODETIC``: EPSG:4326, origin south-west, compatible with OpenLayers map in EPSG:4326 - ``GLOBAL_MERCATOR``: EPSG:900913, origin south-west, compatible with OpenLayers map in EPSG:900913 - ``GLOBAL_WEBMERCATOR``: similar to ``GLOBAL_MERCATOR`` but uses EPSG:3857 and origin north-west, compatible with OpenStreetMap/etc. .. versionadded:: 1.6.0 ``GLOBAL_WEBMERCATOR`` ``name`` """""""" Overwrite the name of the grid used in WMTS URLs. The name is also used in TMS and KML URLs when the ``use_grid_names`` option of the services is set to ``true``. ``srs`` """"""" The spatial reference system used for the internal cache, written as ``EPSG:xxxx``. .. index:: tile_size ``tile_size`` """"""""""""" The size of each tile. Defaults to 256x256 pixel. :: tile_size: [512, 512] .. index:: res ``res`` """"""" A list with all resolutions that MapProxy should cache. :: res: [1000, 500, 200, 100] .. index:: res_factor ``res_factor`` """""""""""""" Here you can define a factor between each resolution. It should be either a number or the term ``sqrt2``. ``sqrt2`` is a shorthand for a resolution factor of 1.4142, the square root of two. With this factor the resolution doubles every second level. Compared to the default factor 2 you will get another cached level between all standard levels. This is suited for free zooming in vector-based layers where the results might look to blurry/pixelated in some resolutions. For requests with no matching cached resolution the next best resolution is used and MapProxy will transform the result. ``threshold_res`` """"""""""""""""" A list with resolutions at which MapProxy should switch from one level to another. MapProxy automatically tries to determine the optimal cache level for each request. You can tweak the behavior with the ``stretch_factor`` option (see below). If you need explicit transitions from one level to another at fixed resolutions, then you can use the ``threshold_res`` option to define these resolutions. You only need to define the explicit transitions. Example: You are caching at 1000, 500 and 200m/px resolutions and you are required to display the 1000m/px level for requests with lower than 700m/px resolutions and the 500m/px level for requests with higher resolutions. You can define that transition as follows:: res: [1000, 500, 200] threshold_res: [700] Requests with 1500, 1000 or 701m/px resolution will use the first level, requests with 700 or 500m/px will use the second level. All other transitions (between 500 an 200m/px in this case) will be calculated automatically with the ``stretch_factor`` (about 416m/px in this case with a default configuration). ``bbox`` """""""" The extent of your grid. You can use either a list or a string with the lower left and upper right coordinates. You can set the SRS of the coordinates with the ``bbox_srs`` option. If that option is not set the ``srs`` of the grid will be used. MapProxy always expects your BBOX coordinates order to be west, south, east, north regardless of your SRS :ref:`axis order `. :: bbox: [0, 40, 15, 55] or bbox: "0,40,15,55" ``bbox_srs`` """""""""""" The SRS of the grid bbox. See above. .. index:: origin .. _grid_origin: ``origin`` """""""""" .. versionadded:: 1.3.0 The default origin (x=0, y=0) of the tile grid is the lower left corner, similar to TMS. WMTS defines the tile origin in the upper left corner. MapProxy can translate between services and caches with different tile origins, but there are some limitations for grids with custom BBOX and resolutions that are not of factor 2. You can only use one service in these cases and need to use the matching ``origin`` for that service. The following values are supported: ``ll`` or ``sw``: If the x=0, y=0 tile is in the lower-left/south-west corner of the tile grid. This is the default. ``ul`` or ``nw``: If the x=0, y=0 tile is in the upper-left/north-west corner of the tile grid. ``num_levels`` """""""""""""" The total number of cached resolution levels. Defaults to 20, except for grids with ``sqrt2`` resolutions. This option has no effect when you set an explicit list of cache resolutions. ``min_res`` and ``max_res`` """"""""""""""""""""""""""" The the resolutions of the first and the last level. ``stretch_factor`` """""""""""""""""" MapProxy chooses the `optimal` cached level for requests that do not exactly match any cached resolution. MapProxy will stretch or shrink images to the requested resolution. The `stretch_factor` defines the maximum factor MapProxy is allowed to stretch images. Stretched images result in better performance but will look blurry when the value is to large (> 1.2). Example: Your MapProxy caches 10m and 5m resolutions. Requests with 9m resolution will be generated from the 10m level, requests for 8m from the 5m level. ``max_shrink_factor`` """""""""""""""""""""" This factor only applies for the first level and defines the maximum factor that MapProxy will shrink images. Example: Your MapProxy layer starts with 1km resolution. Requests with 3km resolution will get a result, requests with 5km will get a blank response. ``base`` """""""" With this option, you can base the grid on the options of another grid you already defined. Defining Resolutions """""""""""""""""""" There are multiple options that influence the resolutions MapProxy will use for caching: ``res``, ``res_factor``, ``min_res``, ``max_res``, ``num_levels`` and also ``bbox`` and ``tile_size``. We describe the process MapProxy uses to build the list of all cache resolutions. If you supply a list with resolution values in ``res`` then MapProxy will use this list and will ignore all other options. If ``min_res`` is set then this value will be used for the first level, otherwise MapProxy will use the resolution that is needed for a single tile (``tile_size``) that contains the whole ``bbox``. If you have ``max_res`` and ``num_levels``: The resolutions will be distributed between ``min_res`` and ``max_res``, both resolutions included. The resolutions will be logarithmical, so you will get a constant factor between each resolution. With resolutions from 1000 to 10 and 6 levels you would get 1000, 398, 158, 63, 25, 10 (rounded here for readability). If you have ``max_res`` and ``res_factor``: The resolutions will be multiplied by ``res_factor`` until larger then ``max_res``. If you have ``num_levels`` and ``res_factor``: The resolutions will be multiplied by ``res_factor`` for up to ``num_levels`` levels. Example ``grids`` configuration """"""""""""""""""""""""""""""" :: grids: localgrid: srs: EPSG:31467 bbox: [5,50,10,55] bbox_srs: EPSG:4326 min_res: 10000 res_factor: sqrt2 localgrid2: base: localgrid srs: EPSG:25832 tile_size: [512, 512] .. ################################################################################# .. index:: sources .. _sources-conf-label: sources ------- A sources defines where MapProxy can request new data. Each source has a ``type`` and all other options are dependent to this type. See :doc:`sources` for the documentation of all available sources. An example:: sources: sourcename: type: wms req: url: http://localhost:8080/service? layers: base anothersource: type: wms # ... .. ################################################################################# .. index:: globals .. _globals-conf-label: globals ------- Here you can define some internals of MapProxy and default values that are used in the other configuration directives. ``image`` """"""""" Here you can define some options that affect the way MapProxy generates image results. .. _image_resampling_method: ``resampling_method`` The resampling method used when results need to be rescaled or transformed. You can use one of nearest, bilinear or bicubic. Nearest is the fastest and bicubic the slowest. The results will look best with bilinear or bicubic. Bicubic enhances the contrast at edges and should be used for vector images. With `bilinear` you should get about 2/3 of the `nearest` performance, with `bicubic` 1/3. See the examples below: ``nearest``: .. image:: imgs/nearest.png ``bilinear``: .. image:: imgs/bilinear.png ``bicubic``: .. image:: imgs/bicubic.png .. _image_paletted: ``paletted`` Enable paletted (8bit) PNG images. It defaults to ``true`` for backwards compatibility. You should set this to ``false`` if you need 24bit PNG files. You can enable 8bit PNGs for single caches with a custom format (``colors: 256``). ``formats`` Modify existing or define new image formats. :ref:`See below ` for all image format options. .. _globals_cache: ``cache`` """"""""" The following options define how tiles are created and stored. Most options can be set individually for each cache as well. .. versionadded:: 1.6.0 ``tile_lock_dir`` .. versionadded:: 1.10.0 ``bulk_meta_tiles`` .. _meta_size: ``meta_size`` MapProxy does not make a single request for every tile it needs, but it will request a large meta-tile that consist of multiple tiles. ``meta_size`` defines how large a meta-tile is. A ``meta_size`` of ``[4, 4]`` will request 16 tiles in one pass. With a tile size of 256x256 this will result in 1024x1024 requests to the source. Tiled sources are still requested tile by tile, but you can configure MapProxy to load multiple tiles in bulk with `bulk_meta_tiles`. .. _bulk_meta_tiles: ``bulk_meta_tiles`` Enables meta-tile handling for caches with tile sources. If set to `true`, MapProxy will request neighboring tiles from the source even if only one tile is requested from the cache. ``meta_size`` defines how many tiles should be requested in one step and ``concurrent_tile_creators`` defines how many requests are made in parallel. This option improves the performance for caches that allow to store multiple tiles with one request, like SQLite/MBTiles but not the ``file`` cache. ``meta_buffer`` MapProxy will increase the size of each meta-tile request by this number of pixels in each direction. This can solve cases where labels are cut-off at the edge of tiles. ``base_dir`` The base directory where all cached tiles will be stored. The path can either be absolute (e.g. ``/var/mapproxy/cache``) or relative to the mapproxy.yaml file. Defaults to ``./cache_data``. .. _lock_dir: ``lock_dir`` MapProxy uses locking to limit multiple request to the same service. See ``concurrent_requests``. This option defines where the temporary lock files will be stored. The path can either be absolute (e.g. ``/tmp/lock/mapproxy``) or relative to the mapproxy.yaml file. Defaults to ``./cache_data/tile_locks``. .. _tile_lock_dir: ``tile_lock_dir`` MapProxy uses locking to prevent that the same tile gets created multiple times. This option defines where the temporary lock files will be stored. The path can either be absolute (e.g. ``/tmp/lock/mapproxy``) or relative to the mapproxy.yaml file. Defaults to ``./cache_data/dir_of_the_cache/tile_locks``. ``concurrent_tile_creators`` This limits the number of parallel requests MapProxy will make to a source. This limit is per request for this cache and not for all MapProxy requests. To limit the requests MapProxy makes to a single server use the ``concurrent_requests`` option. Example: A request in an uncached region requires MapProxy to fetch four meta-tiles. A ``concurrent_tile_creators`` value of two allows MapProxy to make two requests to the source WMS request in parallel. The splitting of the meta-tile and the encoding of the new tiles will happen in parallel to. ``link_single_color_images`` Enables the ``link_single_color_images`` option for all caches if set to ``true``. See :ref:`link_single_color_images`. .. _max_tile_limit: ``max_tile_limit`` Maximum number of tiles MapProxy will merge together for a WMS request. This limit is for each layer and defaults to 500 tiles. ``srs`` """"""" ``proj_data_dir`` MapProxy uses Proj4 for all coordinate transformations. If you need custom projections or need to tweak existing definitions (e.g. add towgs parameter set) you can point MapProxy to your own set of proj4 init files. The path should contain an ``epsg`` file with the EPSG definitions. The configured path can be absolute or relative to the mapproxy.yaml. .. _axis_order: ``axis_order_ne`` and ``axis_order_en`` The axis ordering defines in which order coordinates are given, i.e. lon/lat or lat/lon. The ordering is dependent to the SRS. Most clients and servers did not respected the ordering and everyone used lon/lat ordering. With the WMS 1.3.0 specification the OGC emphasized that the axis ordering of the SRS should be used. Here you can define the axis ordering of your SRS. This might be required for proper WMS 1.3.0 support if you use any SRS that is not in the default configuration. By default MapProxy assumes lat/long (north/east) order for all geographic and x/y (east/north) order for all projected SRS. You need to add the SRS name to the appropriate parameter, if that is not the case for your SRS.:: srs: # for North/East ordering axis_order_ne: ['EPSG:9999', 'EPSG:9998'] # for East/North ordering axis_order_en: ['EPSG:0000', 'EPSG:0001'] If you need to override one of the default values, then you need to define both axis order options, even if one is empty. .. _http_ssl: ``http`` """""""" HTTP related options. Secure HTTP Connections (HTTPS) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ MapProxy supports access to HTTPS servers. Just use ``https`` instead of ``http`` when defining the URL of a source. MapProxy verifies the SSL/TLS connections against your systems "certification authority" (CA) certificates. You can provide your own set of root certificates with the ``ssl_ca_certs`` option. See the `Python SSL documentation `_ for more information about the format. :: http: ssl_ca_certs: /etc/ssl/certs/ca-certificates.crt .. versionadded:: 1.11.0 MapProxy uses the systems CA files by default, if you use Python >=2.7.9 or >=3.4. .. note:: You need to supply a CA file that includes the root certificates if you use older MapProxy or older Python versions. Otherwise MapProxy will fail to establish the connection. You can set the ``http.ssl_no_cert_checks`` options to ``true`` to disable this verification. ``ssl_no_cert_checks`` If you want to use SSL/TLS but do not need certificate verification, then you can disable it with the ``ssl_no_cert_checks`` option. You can also disable this check on a source level. :: http: ssl_no_cert_checks: true ``client_timeout`` ^^^^^^^^^^^^^^^^^^ This defines how long MapProxy should wait for data from source servers. Increase this value if your source servers are slower. ``method`` ^^^^^^^^^^ Configure which HTTP method should be used for HTTP requests. By default (`AUTO`) MapProxy will use GET for most requests, except for requests with a long query string (e.g. WMS requests with `sld_body`) where POST is used instead. You can disable this behavior with either `GET` or `POST`. :: http: method: GET ``headers`` ^^^^^^^^^^^ Add additional HTTP headers to all requests to your sources. :: http: headers: My-Header: header value ``access_control_allow_origin`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. versionadded:: 1.8.0 Sets the ``Access-control-allow-origin`` header to HTTP responses for `Cross-origin resource sharing `_. This header is required for WebGL or Canvas web clients. Defaults to `*`. Leave empty to disable the header. This option is only available in `globals`. ``tiles`` """""""""" Configuration options for the TMS/Tile service. ``expires_hours`` The number of hours a Tile is valid. TMS clients like web browsers will cache the tile for this time. Clients will try to refresh the tiles after that time. MapProxy supports the ETag and Last-Modified headers and will respond with the appropriate HTTP `'304 Not modified'` response if the tile was not changed. ``mapserver`` """"""""""""" Options for the :ref:`Mapserver source`. ``binary`` ^^^^^^^^^^ The complete path to the ``mapserv`` executable. Required if you use the ``mapserver`` source. ``working_dir`` ^^^^^^^^^^^^^^^ Path where the Mapserver should be executed from. It should be the directory where any relative paths in your mapfile are based on. Defaults to the directory of ``binary``. .. _image_options: Image Format Options -------------------- .. versionadded:: 1.1.0 There are a few options that affect how MapProxy encodes and transforms images. You can set these options in the ``globals`` section or individually for each source or cache. Options """"""" Available options are: ``format`` The mime-type of this image format. The format defaults to the name of the image configuration. ``mode`` One of ``RGB`` for 24bit images, ``RGBA`` 32bit images with alpha, ``P`` for paletted images or ``I`` for integer images. ``colors`` The number of colors to reduce the image before encoding. Use ``0`` to disable color reduction (quantizing) for this format and ``256`` for paletted images. See also :ref:`globals.image.paletted `. ``transparent`` ``true`` if the image should have an alpha channel. ``resampling_method`` The resampling method used for scaling or reprojection. One of ``nearest``, ``bilinear`` or ``bicubic``. ``encoding_options`` Options that modify the way MapProxy encodes (saves) images. These options are format dependent. See below. ``opacity`` Configures the opacity of a layer or cache. This value is used when the source or cache is placed on other layers and it can be used to overlay non-transparent images. It does not alter the image itself, and only effects when multiple layers are merged to one image. The value should be between 0.0 (full transparent) and 1.0 (opaque, i.e. the layers below will not be rendered). ``encoding_options`` ^^^^^^^^^^^^^^^^^^^^ The following encoding options are available: .. _jpeg_quality: ``jpeg_quality`` An integer value from 0 to 100 that defines the image quality of JPEG images. Larger values result in slower performance, larger file sizes but better image quality. You should try values between 75 and 90 for good compromise between performance and quality. ``quantizer`` The algorithm used to quantize (reduce) the image colors. Quantizing is used for GIF and paletted PNG images. Available quantizers are ``mediancut`` and ``fastoctree``. ``fastoctree`` is much faster and also supports 8bit PNG with full alpha support, but the image quality can be better with ``mediancut`` in some cases. The quantizing is done by the Python Image Library (PIL). ``fastoctree`` is a `new quantizer `_ that is only available in Pillow >=2.0. See :ref:`installation of PIL`. Global """""" You can configure image formats globally with the ``image.formats`` option. Each format has a name and one or more options from the list above. You can choose any name, but you need to specify a ``format`` if the name is not a valid mime-type (e.g. ``myformat`` instead of ``image/png``). Here is an example that defines a custom format:: globals: image: formats: my_format: format: image/png mode: P transparent: true You can also modify existing image formats:: globals: image: formats: image/png: encoding_options: quantizer: fastoctree MapProxy will use your image formats when you are using the format name as the ``format`` of any source or cache. For example:: caches: mycache: format: my_format sources: [source1, source2] grids: [my_grid] Local """"" You can change all options individually for each cache or source. You can do this by choosing a base format and changing some options:: caches: mycache: format: image/jpeg image: encoding_options: jpeg_quality: 80 sources: [source1, source2] grids: [my_grid] You can also configure the format from scratch:: caches: mycache: image: format: image/jpeg resampling_method: nearest sources: [source1, source2] grids: [my_grid] Notes ----- .. _scale_resolution: Scale vs. resolution """""""""""""""""""" Scale is the ratio of a distance on a map and the corresponding distance on the ground. This implies that the map distance and the ground distance are measured in the same unit. For MapProxy a `map` is just a collection of pixels and the pixels do not have any size/dimension. They do correspond to a ground size but the size on the `map` is depended of the physical output format. MapProxy can thus only work with resolutions (pixel per ground unit) and not scales. This applies to all servers and the OGC WMS standard as well. Some neglect this fact and assume a fixed pixel dimension (like 72dpi), the OCG WMS 1.3.0 standard uses a pixel size of 0.28 mm/px (around 91dpi). But you need to understand that a `scale` will differ if you print a map (200, 300 or more dpi) or if you show it on a computer display (typical 90-120 dpi, but there are mobile devices with more than 300 dpi). You can convert between scales and resolutions with the :ref:`mapproxy-util scales tool`. MapProxy will use the OCG value (0.28mm/px) if it's necessary to use a scale value (e.g. MinScaleDenominator in WMS 1.3.0 capabilities), but you should always use resolutions within MapProxy. WMS ScaleHint ^^^^^^^^^^^^^ The WMS ScaleHint is a bit misleading. The parameter is not a scale but the diagonal pixel resolution. It also defines the ``min`` as the minimum value not the minimum resolution (e.g. 10m/px is a lower resolution than 5m/px, but 5m/px is the minimum value). MapProxy always uses the term resolutions as the side length in ground units per pixel and minimum resolution is always the higher number (100m/px < 10m/px). Keep that in mind when you use these values. mapproxy-1.11.0/doc/configuration_examples.rst000066400000000000000000000710501320454472400215250ustar00rootroot00000000000000.. _configuration_examples: ###################### Configuration examples ###################### This document will show you some usage scenarios of MapProxy and will explain some combinations of configuration options that might be useful for you. .. _merge_layers: Merge multiple layers ===================== You have two WMS and want to offer a single layer with data from both servers. Each MapProxy cache can have more than one data source. MapProxy will combine the results from the sources before it stores the tiles on disk. These combined layers can also be requested via tiled services. The sources should be defined from bottom to top. All sources except the bottom source needs to be transparent. Example:: layers: - name: combined_layer title: Aerial image + roads overlay sources: [combined_cache] caches: combined_cache: sources: [base, aerial] sources: base: type: wms wms_opts: featureinfo: True version: 1.1.1 req: url: http://one.example.org/mapserv/?map=/home/map/roads.map layers: roads transparent: true aerial: type: wms req: url: http://two.example.org/service? layers: aerial .. note:: If the layers come from the same WMS server, then you can add them direct to the ``layers`` parameter. E.g. ``layers: water,railroads,roads``. Merge tile sources ------------------ You can also merge multiple tile sources. You need to tell MapProxy that all overlay sources are transparent:: sources: tileoverlay: type: tile url: http://localhost:8080/tile?x=%(x)s&y=%(y)s&z=%(z)s&format=png transparent: true Access local servers ==================== By default MapProxy will request data in the same format it uses to cache the data, if you cache files in PNG MapProxy will request all images from the source WMS in PNG. This encoding is quite CPU intensive for your WMS server but reduces the amount of data than needs to be transfered between you WMS and MapProxy. You can use uncompressed TIFF as the request format, if both servers are on the same host or if they are connected with high bandwidth. Example:: sources: fast_source: type: cache_wms req: url: http://localhost/mapserv/?map=/home/map/roads.map layers: roads format: image/tiff transparent: true Create WMS from existing tile server ==================================== You can use MapProxy to create a WMS server with data from an existing tile server. That tile server could be a WMTS, TMS or any other tile service where you can access tiles by simple HTTP requests. You always need to configure a cache in MapProxy to get a WMS from a tile source, since the cache is the part that does the tile stitching and reprojection. Here is a minimal example:: layers: - name: my_layer title: WMS layer from tiles sources: [mycache] caches: mycache: grids: [GLOBAL_WEBMERCATOR] sources: [my_tile_source] sources: my_tile_source: type: tile url: http://tileserver/%(tms_path)s.png You need to modify the ``url`` template parameter to match the URLs of your server. You can use ``x``, ``y``, ``z`` variables in the template, but MapProxy also supports the ``quadkey`` variable for Bing compatible tile service and ``bbox`` for WMS-C services. See the :ref:`tile source documentation ` for all possible template values. Here is an example of a WMTS source:: sources: my_tile_source: type: tile url: http://tileserver/wmts?SERVICE=WMTS&REQUEST=GetTile& VERSION=1.0.0&LAYER=layername&TILEMATRIXSET=WEBMERCATOR& TILEMATRIX=%(z)s&TILEROW=%(y)s&TILECOL=%(x)s&FORMAT=image%%2Fpng .. note:: You need to escape percent signs (``%``) in the URL by repeating them (``%%``). .. _osm_tile_conf: You can use the ``GLOBAL_WEBMERCATOR`` grid for OpenStreetMap or Google Maps compatible sources. Most TMS services should be compatible with the ``GLOBAL_MERCATOR`` definition that is similar to ``GLOBAL_WEBMERCATOR`` but uses a different origin (south west (TMS) instead of north west (OSM/WMTS/Google Maps/etc.)). Other tile services might use different SRS, bounding boxes or resolutions. You need to check the capabilities of your service and :ref:`configure a compatible grid `. You also need to create your own grid when you want to change the name of it, which will appear in the WMTS or TMS URL. Example configuration for an OpenStreetMap tile service:: layers: - name: my_layer title: WMS layer from tiles sources: [mycache] caches: mycache: grids: [webmercator] sources: [my_tile_source] sources: my_tile_source: type: tile grid: GLOBAL_WEBMERCATOR url: http://a.tile.openstreetmap.org/%(z)s/%(x)s/%(y)s.png grids: webmercator: base: GLOBAL_WEBMERCATOR .. note:: Please make sure you are allowed to access the tile service. Commercial tile provider often prohibit the direct access to tiles. The tile service from OpenStreetMap has a strict `Tile Usage Prolicy `_. .. _overlay_tiles_osm_openlayers: Overlay tiles with OpenStreetMap or Google Maps in OpenLayers ============================================================= You need to take care of a few options when you want to overlay your MapProxy tiles in OpenLayers with existing OpenStreetMap or Google Maps tiles. The basic configuration for this use-case with MapProxy may look like this:: layers: - name: street_layer title: TMS layer with street data sources: [street_cache] caches: street_cache: sources: [street_tile_source] sources: street_tile_source: type: tile url: http://osm.omniscale.net/proxy/tiles/ \ 1.0.0/osm_roads_EPSG900913/%(z)s/%(x)s/%(y)s.png transparent: true All you need to do now is to configure your OpenLayers client. The first example creates a simple OpenLayers map in webmercator projection, adds an OSM base layer and a TMS overlay layer with our MapProxy tile service.:: Note that we used the ``/tiles`` service instead of ``/tms`` here. See :ref:`the tile service documentation ` for more information. Also remember that OpenStreetMap and Google Maps tiles have the origin in the upper left corner of the map, instead of the lower left corner as TMS does. Have a look at the :ref:`example configuration for OpenStreetMap tiles` for more information on that topic. The OpenLayers TMS and OSM layers already handle the difference. You can change how MapProxy calculates the origin of the tile coordinates, if you want to use your MapProxy tile service with the OpenLayers OSM layer class or if you want to use a client that does not have a TMS layer. The following example uses the class OpenLayers.Layer.OSM:: var overlay_layer = new OpenLayers.Layer.OSM("OSM osm_layer", "http://x.osm.omniscale.net/proxy/tiles/ \ osm_roads_EPSG900913/${z}/${x}/${y}.png?origin=nw", {isBaseLayer: false, tileOptions: {crossOriginKeyword: null}} ); The origin parameter at the end of the URL tells MapProxy that the client expects the origin in the upper left corner (north/west). You can change the default origin of all MapProxy tile layers by using the ``origin`` option of the ``tms`` service. See the :ref:`TMS standard tile origin` for more informations. .. _using_existing_caches: Using existing caches ===================== .. versionadded:: 1.5.0 In some special use-cases you might want to use a cache as the source of another cache. For example, you might need to change the grid of an existing cache to cover a larger bounding box, or to support tile clients that expect a different grid, but you don't want to seed the data again. Here is an example of a cache in UTM that uses data from an existing cache in web-mercator projection. :: layers: - name: lyr1 title: Layer using data from existing_cache sources: [new_cache] caches: new_cache: grids: [utm32n] sources: [existing_cache] existing_cache: grids: [GLOBAL_WEBMERCATOR] sources: [my_source] grids: utm32n: srs: 'EPSG:25832' bbox: [4, 46, 16, 56] bbox_srs: 'EPSG:4326' origin: 'nw' min_res: 5700 Reprojecting Tiles ================== .. versionadded:: 1.5.0 When you need to access tiles in a projection that is different from your source tile server, then you can use the *cache as cache source* feature from above. Here is an example that uses OSM tiles as a source and offers them in UTM projection. The `disable_storage` option prevents MapProxy from building up two caches. The `meta_size` makes MapProxy to reproject multiple tiles at once. Here is an example that makes OSM tiles available as tiles in UTM. Note that reprojecting vector data results in quality loss. For better results you need to find similar resolutions between both grids. :: layers: - name: osm title: OSM in UTM sources: [osm_cache] caches: osm_cache: grids: [utm32n] meta_size: [4, 4] sources: [osm_cache_in] osm_cache_in: grids: [GLOBAL_WEBMERCATOR] disable_storage: true sources: [osm_source] sources: osm_source: type: tile grid: GLOBAL_WEBMERCATOR url: http://a.tile.openstreetmap.org/%(z)s/%(x)s/%(y)s.png grids: utm32n: srs: 'EPSG:25832' bbox: [4, 46, 16, 56] bbox_srs: 'EPSG:4326' origin: 'nw' min_res: 5700 Create grayscale images ======================= .. versionadded:: 1.9.0 You can create a grayscale layer from an existing source by creating a cache that merges multiple bands into a single band. The band sources can come from caches, but also from any direct source. You can ``disable_storage`` to make this conversion on-the-fly. The following example mixes the RGB bands of a source with factors that matches the intensity perception of most humans:: caches: grayscale_cache: disable_storage: true sources: l: [ {source: dop, band: 0, factor: 0.21}, {source: dop, band: 1, factor: 0.72}, {source: dop, band: 2, factor: 0.07}, ] Cache raster data ================= You have a WMS server that offers raster data like aerial images. By default MapProxy uses PNG images as the caching format. The encoding process for PNG files is very CPU intensive and thus the caching process itself takes longer. For aerial images the quality of loss-less image formats like PNG is often not required. For best performance you should use JPEG as the cache format. By default MapProxy uses `bicubic` resampling. This resampling method also sharpens the image which is important for vector images. Aerial images do not need this, so you can use `bilinear` or even Nearest Neighbor (`nearest`) resampling. :: caches: aerial_images_cache: format: image/jpeg image: resampling_method: nearest sources: [aerial_images] You might also want to experiment with different compression levels of JPEG. A higher value of ``jpeg_quality`` results in better image quality at the cost of slower encoding and lager file sizes. See :ref:`mapproxy.yaml configuration `. :: globals: jpeg_quality: 80 Mixed mode ---------- You need to store images with transparency when you want to overlay them over other images, e.g. at the boundaries of your aerial image coverage. PNG supports transparency but it is not efficient with arial images, while JPEG is efficient for aerial images but doesn't support transparency. MapProxy :ref:`has a mixed image format ` for this case. With the ``mixed`` format, MapProxy stores tiles as either PNG or JPEG, depending on the transparency of each tile. Images with transparency will be stored as PNG, fully opaque images as JPEG. .. note:: The source of your cache must support transparent images and you need to set the corresponding options. :: caches: mixed_cache: format: mixed sources: [wms_source] request_format: image/png sources: wms_source: type: wms req: url: http://localhost:42423/service layers: aerial transparent: true You can now use the cache in all MapProxy services. WMS GetMap requests will return the image with the requested format. With TMS or WMTS you can only request PNG tiles, but the actual response image is either PNG or JPEG. The HTTP `content-type` header is set accordingly. This is supported by all web browsers. Cache vector data ================= You have a WMS server that renders vector data like road maps. .. _cache_resolutions: Cache resolutions ----------------- By default MapProxy caches traditional power-of-two image pyramids, the resolutions between each pyramid level doubles. For example if the first level has a resolution of 10km, it would also cache resolutions of 5km, 2.5km, 1.125km etc. Requests with a resolution of 7km would be generated from cached data with a resolution of 10km. The problem with this approach is, that everything needs to be scaled down, lines will get thin and text labels will become unreadable. The solution is simple: Just add more levels to the pyramid. There are three options to do this. You can set every cache resolution in the ``res`` option of a layer. :: caches: custom_res_cache: grids: [custom_res] sources: [vector_source] grids: custom_res_cache: srs: 'EPSG:31467' res: [10000, 7500, 5000, 3500, 2500] You can specify a different factor that is used to calculate the resolutions. By default a factor of 2 is used (10, 5, 2.5,…) but you can set smaller values like 1.6 (10, 6.25, 3.9,…):: grids: custom_factor: res_factor: 1.6 The third options is a convenient variation of the previous option. A factor of 1.41421, the square root of two, would get resolutions of 10, 7.07, 5, 3.54, 2.5,…. Notice that every second resolution is identical to the power-of-two resolutions. This comes in handy if you use the layer not only in classic WMS clients but also want to use it in tile-based clients like OpenLayers, which only request in these resolutions. :: grids: sqrt2: res_factor: sqrt2 .. note:: This does not improve the quality of aerial images or scanned maps, so you should avoid it for these images. Resampling method ----------------- You can configure the method MapProxy uses for resampling when it scales or transforms data. For best results with vector data – from a viewers perspective – you should use bicubic resampling. You can configure this for each cache or in the globals section:: caches: vector_cache: image: resampling: bicubic # [...] # or globals: image: resampling: bicubic .. _sld_example: WMS Sources with Styled Layer Description (SLD) =============================================== You can configure SLDs for your WMS sources. :: sources: sld_example: type: wms req: url: http://example.org/service? sld: http://example.net/mysld.xml MapProxy also supports local file URLs. MapProxy will use the content of the file as the ``sld_body``. The path can either be absolute (e.g. ``file:///path/to/sld.xml``) or relative (``file://path/to/sld.xml``) to the mapproxy.yaml file. The file should be UTF-8 encoded. You can also configure the raw SLD with the ``sld_body`` option. You need to indent whole SLD string. :: sources: sld_example: type: wms req: url: http://example.org/service? sld_body: MapProxy will use HTTP POST requests in this case. You can change ``http.method``, if you want to force GET requests. .. _direct_source: Add highly dynamic layers ========================= You have dynamic layers that change constantly and you do not want to cache these. You can use a direct source. See next example. Reproject WMS layers ==================== If you do not want to cache data but still want to use MapProxy's ability to reproject WMS layers on the fly, you can use a direct layer. Add your source directly to your layer instead of a cache. You should explicitly define the SRS the source WMS supports. Requests in other SRS will be reprojected. You should specify at least one geographic and one projected SRS to limit the distortions from reprojection. :: layers: - name: direct_layer sources: [direct_wms] sources: direct_wms: type: wms supported_srs: ['EPSG:4326', 'EPSG:25832'] req: url: http://wms.example.org/service? layers: layer0,layer1 .. _fi_xslt: FeatureInformation ================== MapProxy can pass-through FeatureInformation requests to your WMS sources. You need to enable each source:: sources: fi_source: type: wms wms_opts: featureinfo: true req: url: http://example.org/service? layers: layer0 MapProxy will mark all layers that use this source as ``queryable``. It also works for sources that are used with caching. .. note:: The more advanced features :ref:`require the lxml library `. Concatenation ------------- Feature information from different sources are concatenated as plain text, that means that XML documents may become invalid. But MapProxy can also do content-aware concatenation when :ref:`lxml ` is available. HTML ~~~~ Multiple HTML documents are put into the HTML ``body`` of the first document. MapProxy creates the HTML skeleton if it is missing. ::

FI1

and ::

FI2

will result in::

FI1

FI2

XML ~~~ Multiple XML documents are put in the root of the first document. :: FI1 and :: FI2 will result in:: FI1 FI2 XSL Transformations ------------------- MapProxy supports XSL transformations for more control over feature information. This also requires :ref:`lxml `. You can add an XSLT script for each WMS source (incoming) and for the WMS service (outgoing). You can use XSLT for sources to convert all incoming documents to a single, uniform format and then use outgoing XSLT scripts to transform this format to either HTML or XML/GML output. Example ~~~~~~~ Lets assume we have two WMS sources where we have no control over the format of the feature info responses. One source only offers HTML feature information. The XSLT script extracts data from a table. We force the ``INFO_FORMAT`` to HTML, so that MapProxy will not query another format. :: fi_source: type: wms wms_opts: featureinfo: true featureinfo_xslt: ./html_in.xslt featureinfo_format: text/html req: [...] The second source supports XML feature information. The script converts the XML data to the same format as the HTML script. This service uses WMS 1.3.0 and the format is ``text/xml``. :: fi_source: type: wms wms_opts: version: 1.3.0 featureinfo: true featureinfo_xslt: ./xml_in.xslt featureinfo_format: text/xml req: [...] We then define two outgoing XSLT scripts that transform our intermediate format to the final result. We can define scripts for different formats. MapProxy chooses the right script depending on the WMS version and the ``INFO_FORMAT`` of the request. :: wms: featureinfo_xslt: html: ./html_out.xslt xml: ./xml_out.xslt [...] .. _wmts_dimensions: WMTS service with dimensions ============================ .. versionadded:: 1.6.0 The dimension support in MapProxy is still limited, but you can use it to create a WMTS front-end for a multi-dimensional WMS service. First you need to add the WMS source and configure all dimensions that MapProxy should forward to the service:: temperature_source: type: wms req: url: http://example.org/service? layers: temperature forward_req_params: ['time', 'elevation'] We need to create a cache since we want to access the source from a tiled service (WMTS). Actual caching is not possible at the moment, so it is necessary to disable it with ``disable_storage: true``. :: caches: temperature: grids: [GLOBAL_MERCATOR] sources: [temperature_source] disable_storage: true meta_size: [1, 1] meta_buffer: 0 Then we can add a layer with all available dimensions:: layers: - name: temperature title: Temperature sources: [temperature] dimensions: time: values: - "2012-11-12T00:00:00" - "2012-11-13T00:00:00" - "2012-11-14T00:00:00" - "2012-11-15T00:00:00" elevation: values: - 0 - 1000 - 3000 default: 0 You can know access this layer with the elevation and time dimensions via the WMTS KVP service. The RESTful service requires a custom URL template that contains the dimensions. For example:: services: wmts: restful_template: '/{Layer}/{Time}/{Elevation}/{TileMatrixSet} /{TileMatrix}/{TileCol}/{TileRow}.{Format}' Tiles are then available at ``/wmts/temperature/GLOBAL_MERCATOR/1000/2012-11-12T00:00Z/6/33/22.png``. You can use ``default`` for missing dimensions, e.g. ``/wmts/map/GLOBAL_MERCATOR/default/default/6/33/22.png``. WMS layers with HTTP Authentication =================================== You have a WMS source that requires authentication. MapProxy has support for HTTP Basic Authentication and HTTP Digest Authentication. You just need to add the username and password to the URL. Since the Basic and Digest authentication are not really secure, you should use this feature in combination with HTTPS. You need to configure the SSL certificates to allow MapProxy to verify the HTTPS connection. See :ref:`HTTPS configuration for more information `. :: secure_source: type: wms req: url: https://username:mypassword@example.org/service? layers: securelayer MapProxy removes the username and password before the URL gets logged or inserted into service exceptions. You can disable the certificate verification if you you don't need it. :: secure_source: type: wms http: ssl_no_cert_checks: True req: url: https://username:mypassword@example.org/service? layers: securelayer .. _http_proxy: Access sources through HTTP proxy ================================= MapProxy can use an HTTP proxy to make requests to your sources, if your system does not allow direct access to the source. You need to set the ``http_proxy`` environment variable to the proxy URL. This also applies if you install MapProxy with ``pip`` or ``easy_install``. On Linux/Unix:: $ export http_proxy="http://example.com:3128" $ mapproxy-util serve-develop mapproxy.yaml On Windows:: c:\> set http_proxy="http://example.com:3128" c:\> mapproxy-util serve-develop mapproxy.yaml You can also set this in your :ref:`server script `:: import os os.environ["http_proxy"] = "http://example.com:3128" Add a username and password to the URL if your HTTP proxy requires authentication. For example ``http://username:password@example.com:3128``. You can use the ``no_proxy`` environment variable if you need to bypass the proxy for some hosts:: $ export no_proxy="localhost,127.0.0.1,196.168.1.99" ``no_proxy`` is available since Python 2.6.3. .. _paster_urlmap: Serve multiple MapProxy instances ================================= It is possible to load multiple MapProxy instances into a single process. Each MapProxy can have a different global configuration and different services and caches. [#f1]_ You can use :ref:`MultiMapProxy` to load multiple MapProxy configurations on-demand. Example ``config.py``:: from mapproxy.multiapp import make_wsgi_app application = make_wsgi_app('/path/to/projects', allow_listing=True) The MapProxy configuration from ``/path/to/projects/app.yaml`` is then available at ``/app``. You can reuse parts of the MapProxy configuration with the `base` option. You can put all common options into a single base configuration and reference that file in the actual configuration:: base: mapproxy.yaml layers: [...] .. [#f1] This does not apply to `srs.proj_data_dir`, because it affects the proj4 library directly. .. _quadkey_cache: Generate static quadkey / virtual earth cache for use on Multitouch table ========================================================================= Some software running on Microsoft multitouch tables need a static quadkey generated cache. Mapproxy understands quadkey both as a client and as a cache option. Example part of ``mapproxy.yaml`` to generate a quadkey cache:: caches: osm_cache: grids: [GLOBAL_WEBMERCATOR] sources: [osm_wms] cache: type: file directory_layout: quadkey .. _hq_tiles: HQ/Retina tiles =============== MapProxy has no native support for delivering high-resolution tiles, but you can create a second tile layer with HQ tiles, if your source supports rendering with different scale-factor or DPI. At first you need two grids. One regular grid and one with half the resolution but twice the tile size. The following example configures two webmercator compatible grids:: grids: webmercator: srs: "EPSG:3857" origin: nw min_res: 156543.03392804097 webmercator_hq: srs: "EPSG:3857" origin: nw min_res: 78271.51696402048 tile_size: [512, 512] Then you need two layers and two caches:: layers: - name: map title: Regular map sources: [map_cache] - name: map_hq title: HQ map sources: [map_hq_cache] caches: map_cache: grids: [webmercator] sources: [map_source] map_hq_cache: grids: [webmercator_hq] sources: [map_hq_source] And finally two sources. The source for the HQ tiles needs to render images with a higher scale/DPI setting. The ``mapnik`` source supports this with the ``scale_factor`` option. MapServer for example supports a ``map_resolution`` request parameter. :: sources: map_source: type: mapnik mapfile: ./mapnik.xml transparent: true map_hq_source: type: mapnik mapfile: ./mapnik.xml transparent: true scale_factor: 2 With that configuration ``/wmts/mapnik/webmercator/0/0/0.png`` returns a regular webmercator tile: .. image:: imgs/mapnik-webmerc.png ``/wmts/mapnik_hq/webmercator_hq/0/0/0.png`` returns the same tile with 512x512 pixel: .. image:: imgs/mapnik-webmerc-hq.png Serve multiple caches for a single layer ======================================== .. versionadded:: 1.8.2 You have a data set that you need to serve with different grids (i.e. WMTS tile matrix sets). You can create a cache with multiple grids and use this as a layers source:: layers: - name: map title: Layer with multiple grids sources: [cache] caches: cache: grids: [full_grid, sub_grid] sources: [source] This `map` layer is available in WMS and in tile services. The grids are available as separate tile matrix sets in the WMTS. However, this is limited to a single cache for each layer. You can't reuse the tiles from the `full_grid` for the `sub_grid`. You need to use ``tile_sources`` to make multiple caches available as a single layer. ``tile_sources`` allows you to override ``sources`` for tile services. This allows you to `use caches that build up on other caches `_. For example:: layers: - name: map title: Layer with sources for tile services and for WMS tile_sources: [full_cache, inspire_cache] sources: [full_cache] caches: full_cache: grids: [full_grid] sources: [source] inspire_cache: grids: [sub_grid] sources: [full_cache] disable_storage: true mapproxy-1.11.0/doc/coverages.rst000066400000000000000000000143631320454472400167420ustar00rootroot00000000000000.. _coverages: Coverages ========= With coverages you can define areas where data is available or where data you are interested in is. MapProxy supports coverages for :doc:`sources ` and in the :doc:`mapproxy-seed tool `. Refer to the corresponding section in the documentation. There are five different ways to describe a coverage: - a simple rectangular bounding box, - a text file with one or more (multi)polygons in WKT format, - a GeoJSON file with (multi)polygons features, - (multi)polygons from any data source readable with OGR (e.g. Shapefile, GeoJSON, PostGIS), - a file with webmercator tile coordinates. .. versionadded:: 1.10 You can also build intersections, unions and differences between multiple coverages. Requirements ------------ If you want to use polygons to define a coverage, instead of simple bounding boxes, you will also need Shapely and GEOS. For loading polygons from shapefiles you'll also need GDAL/OGR. MapProxy requires Shapely 1.2.0 or later and GEOS 3.1.0 or later. On Debian:: sudo aptitude install libgeos-dev libgdal-dev pip install Shapely Configuration ------------- All coverages are configured by defining the source of the coverage and the SRS. The configuration of the coverage depends on the type. The SRS can allways be configured with the ``srs`` option. .. versionadded:: 1.5.0 MapProxy can autodetect the type of the coverage. You can now use ``coverage`` instead of the ``bbox``, ``polygons`` or ``ogr_datasource`` option. The old options are still supported. Coverage Types -------------- Bounding box """""""""""" For simple box coverages. ``bbox`` or ``datasource``: A simple BBOX as a list of minx, miny, maxx, maxy, e.g: `[4, -30, 10, -28]` or as a string `4,-30,10,-28`. Polygon file """""""""""" Text files with one WKT polygon or multi-polygon per line. You can create your own files or use `one of the files we provide for every country `_. Read `the index `_ to find your country. ``datasource``: The path to the polygon file. Should be relative to the proxy configuration or absolute. GeoJSON """"""" .. versionadded:: 1.10 Previous versions required OGR/GDAL for reading GeoJSON. You can use GeoJSON files with Polygon and MultiPolygons geometries. FeatureCollections and Features of these geometries are suppored as well. MapProxy uses OGR to read GeoJSON files if you define a ``where`` filter. ``datasource``: The path to the GeoJSON file. Should be relative to the proxy configuration or absolute. OGR datasource """""""""""""" Any polygon datasource that is supported by OGR (e.g. Shapefile, GeoJSON, PostGIS). ``datasource``: The name of the datasource. Refer to the `OGR format page `_ for a list of all supported datasources. File paths should be relative to the proxy configuration or absolute. ``where``: Restrict which polygons should be loaded from the datasource. Either a simple where statement (e.g. ``'CNTRY_NAME="Germany"'``) or a full select statement. Refer to the `OGR SQL support documentation `_. If this option is unset, the first layer from the datasource will be used. Expire tiles """""""""""" .. versionadded:: 1.10 Text file with webmercator tile coordinates. The tiles should be in ``z/x/y`` format (e.g. ``14/1283/6201``), with one tile coordinate per line. Only tiles in the webmercator grid are supported (origin is always `nw`). ``expire_tiles``: File or directory with expire tile files. Directories are loaded recursive. Union """"" .. versionadded:: 1.10 A union coverage contains the combined coverage of one or more sub-coverages. This can be used to combine multiple coverages a single source. Each sub-coverage can be of any supported type and SRS. ``union``: A list of multiple coverages. Difference """""""""" .. versionadded:: 1.10 A difference coverage subtracts the coverage of other sub-coverages from the first coverage. This can be used to exclude parts from a coverage. Each sub-coverage can be of any supported type and SRS. ``difference``: A list of multiple coverages. Intersection """""""""""" .. versionadded:: 1.10 An intersection coverage contains only areas that are covered by all sub-coverages. This can be used to limit a larger coverage to a smaller area. Each sub-coverage can be of any supported type and SRS. ``difference``: A list of multiple coverages. Clipping -------- .. versionadded:: 1.10.0 By default MapProxy tries to get and serve full source image even if a coverage only touches it. Clipping by coverage can be enabled by setting ``clip: true``. If enabled, all areas outside the coverage will be converted to transparent pixels. The ``clip`` option is only active for source coverages and not for seeding coverages. Examples -------- sources """"""" Use the ``coverage`` option to define a coverage for a WMS or tile source. :: sources: mywms: type: wms req: url: http://example.com/service? layers: base coverage: bbox: [5, 50, 10, 55] srs: 'EPSG:4326' Example of an intersection coverage with clipping:: sources: mywms: type: wms req: url: http://example.com/service? layers: base coverage: clip: true intersection: - bbox: [5, 50, 10, 55] srs: 'EPSG:4326' - datasource: coverage.geojson srs: 'EPSG:4326' mapproxy-seed """"""""""""" To define a seed-area in the ``seed.yaml``, add the coverage directly to the view. :: coverages: germany: datasource: 'shps/world_boundaries_m.shp' where: 'CNTRY_NAME = "Germany"' srs: 'EPSG:900913' .. index:: PostGIS, PostgreSQL Here is the same example with a PostGIS source:: coverages: germany: datasource: "PG: dbname='db' host='host' user='user' password='password'" where: "select * from coverages where country='germany'" srs: 'EPSG:900913' .. index:: GeoJSON And here is an example with a GeoJSON source:: coverages: germany: datasource: 'boundary.geojson' srs: 'EPSG:4326' See `the OGR driver list `_ for all supported formats. mapproxy-1.11.0/doc/decorate_img.rst000066400000000000000000000105701320454472400174020ustar00rootroot00000000000000Decorate Image ============== MapProxy provides the ability to update the image produced in response to a WMS GetMap or Tile request prior to it being sent to the client. This can be used to decorate the image in some way such as applying an image watermark or applying an effect. .. note:: Some Python programming and knowledge of `WSGI `_ and WSGI middleware is required to take advantage of this feature. Decorate Image Middleware ------------------------- The ability to decorate the response image is implemented as WSGI middleware in a similar fashion to how :doc:`authorization ` is handled. You must write a WSGI filter which wraps the MapProxy application in order to register a callback which accepts the ImageSource to be decorated. The callback is registered by assigning a function to the key ``decorate_img`` in the WSGI environment. Prior to the image being sent in the response MapProxy checks the environment and calls the callback passing the ImageSource and a number of other parameters related to the current request. The callback must then return a valid ImageSource instance which will be sent in the response. WSGI Filter Middleware ~~~~~~~~~~~~~~~~~~~~~~ A simple middleware that annotates each image with information about the request might look like:: from mapproxy.image import ImageSource from PIL import ImageColor, ImageDraw, ImageFont def annotate_img(image, service, layers, environ, query_extent, **kw): # Get the PIL image and convert to RGBA to ensure we can use black # for the text img = image.as_image().convert('RGBA') text = ['service: %s' % service] text.append('layers: %s' % ', '.join(layers)) text.append('srs: %s' % query_extent[0]) text.append('bounds:') for coord in query_extent[1]: text.append(' %s' % coord) draw = ImageDraw.Draw(img) font = ImageFont.load_default() fill = ImageColor.getrgb('black') line_y = 10 for line in text: line_w, line_h = font.getsize(line) draw.text((10, line_y), line, font=font, fill=fill) line_y = line_y + line_h # Return a new ImageSource specifying the updated PIL image and # the image options from the original ImageSource return ImageSource(img, image.image_opts) class RequestInfoFilter(object): """ Simple MapProxy decorate_img middleware. Annotates map images with information about the request. """ def __init__(self, app, global_conf): self.app = app def __call__(self, environ, start_response): # Add the callback to the WSGI environment environ['mapproxy.decorate_img'] = annotate_img return self.app(environ, start_response) You need to wrap the MapProxy application with your custom decorate_img middleware. For deployment scripts it might look like:: application = make_wsgi_app('./mapproxy.yaml') application = RequestInfoFilter(application) For `PasteDeploy`_ you can use the ``filter-with`` option. The ``config.ini`` looks like:: [app:mapproxy] use = egg:MapProxy#app mapproxy_conf = %(here)s/mapproxy.yaml filter-with = requestinfo [filter:requestinfo] paste.filter_app_factory = mydecoratemodule:RequestInfoFilter [server:main] ... .. _`PasteDeploy`: http://pythonpaste.org/deploy/ MapProxy Decorate Image API --------------------------- The signature of the decorate_img function: .. function:: decorate_img(image, service, layers=[], environ=None, query_extent=None, **kw) :param image: ImageSource instance to be decorated :param service: service associated with the current request (e.g. ``wms.map``, ``tms`` or ``wmts``) :param layers: list of layer names specified in the request :param environ: the request WSGI environment :param query_extent: a tuple of the SRS (e.g. ``EPSG:4326``) and the BBOX of the request :rtype: ImageSource The ``environ`` and ``query_extent`` parameters are optional and can be ignored by the callback. The arguments might get extended in future versions of MapProxy. Therefore you should collect further arguments in a catch-all keyword argument (i.e. ``**kw``). .. note:: The actual name of the callable is insignificant, only the environment key ``mapproxy.decorate_img`` is important. The function should return a valid ImageSource instance, either the one passed or a new instance depending the implementation. mapproxy-1.11.0/doc/deployment.rst000066400000000000000000000355531320454472400171500ustar00rootroot00000000000000Deployment ========== MapProxy implements the Web Server Gateway Interface (WSGI) which is for Python what the Servlet API is for Java. There are different ways to deploy WSGI web applications. MapProxy comes with a simple HTTP server that is easy to start and sufficient for local testing, see :ref:`deployment_testing`. For production and load testing it is recommended to choose one of the :ref:`production setups `. .. _deployment_testing: Testing ------- .. program:: mapproxy-util serve-develop The ``serve-develop`` subcommand of ``mapproxy-util`` starts an HTTP server for local testing. It takes an existing MapProxy configuration file as an argument:: mapproxy-util serve-develop mapproxy.yaml The server automatically reloads if the configuration or any code of MapProxy changes. .. cmdoption:: --bind, -b Set the socket MapProxy should listen. Defaults to ``localhost:8080``. Accepts either a port number or ``hostname:portnumber``. .. cmdoption:: --debug Start MapProxy in debug mode. If you have installed Werkzeug_ (recommended) or Paste_, you will get an interactive traceback in the web browser on any unhandled exception (internal error). .. note:: This server is sufficient for local testing of the configuration, but it is `not` stable for production or load testing. The ``serve-multiapp-develop`` subcommand of ``mapproxy-util`` works similar to ``serve-develop`` but takes a directory of MapProxy configurations. See :ref:`multimapproxy`. .. _deployment_production: Production ---------- There are two common ways to deploy MapProxy in production. Embedded in HTTP server You can directly integrate MapProxy into your web server. Apache can integrate Python web services with the ``mod_wsgi`` extension for example. Behind an HTTP server or proxy You can run MapProxy as a separate local HTTP server behind an existing web server (nginx_, Apache, etc.) or an HTTP proxy (Varnish_, squid, etc). Both approaches require a configuration that maps your MapProxy configuration with the MapProxy application. You can write a small script file for that. Running MapProxy as a FastCGI server behind HTTP server, a third option, is no longer advised for new setups since the FastCGI package (flup) is no longer maintained and the Python HTTP server improved significantly. .. _server_script: Server script ~~~~~~~~~~~~~ You need a script that makes the configured MapProxy available for the Python WSGI servers. You can create a basic script with ``mapproxy-util``:: mapproxy-util create -t wsgi-app -f mapproxy.yaml config.py The script contains the following lines and makes the configured MapProxy available as ``application``:: from mapproxy.wsgiapp import make_wsgi_app application = make_wsgi_app('examples/minimal/etc/mapproxy.yaml') This is sufficient for embedding MapProxy with ``mod_wsgi`` or for starting it with Python HTTP servers like ``gunicorn`` (see further below). You can extend this script to setup logging or to set environment variables. You can enable MapProxy to automatically reload the configuration if it changes:: from mapproxy.wsgiapp import make_wsgi_app application = make_wsgi_app('examples/minimal/etc/mapproxy.yaml', reloader=True) .. index:: mod_wsgi, Apache Apache mod_wsgi --------------- The Apache HTTP server can directly integrate Python application with the `mod_wsgi`_ extension. The benefit is that you don't have to start another server. Read `mod_wsgi installation`_ for detailed instructions. ``mod_wsgi`` requires a server script that defines the configured WSGI function as ``application``. See :ref:`above `. You need to modify your Apache ``httpd.conf`` as follows:: # if not loaded elsewhere LoadModule wsgi_module modules/mod_wsgi.so WSGIScriptAlias /mapproxy /path/to/mapproxy/config.py Order deny,allow Allow from all ``mod_wsgi`` has a lot of options for more fine tuning. ``WSGIPythonHome`` or ``WSGIPythonPath`` lets you configure your ``virtualenv`` and ``WSGIDaemonProcess``/``WSGIProcessGroup`` allows you to start multiple processes. See the `mod_wsgi configuration directives documentation `_. Using Mapnik also requires the ``WSGIApplicationGroup`` option. .. note:: On Windows only the ``WSGIPythonPath`` option is supported. Linux/Unix supports ``WSGIPythonPath`` and ``WSGIPythonHome``. See also the `mod_wsgi documentation for virtualenv `_ for detailed information when using multiple virtualenvs. A more complete configuration might look like:: # if not loaded elsewhere LoadModule wsgi_module modules/mod_wsgi.so WSGIScriptAlias /mapproxy /path/to/mapproxy/config.py WSGIDaemonProcess mapproxy user=mapproxy group=mapproxy processes=8 threads=25 WSGIProcessGroup mapproxy # WSGIPythonHome should contain the bin and lib dir of your virtualenv WSGIPythonHome /path/to/mapproxy/venv WSGIApplicationGroup %{GLOBAL} Order deny,allow # For Apache 2.4: Require all granted # For Apache 2.2: # Allow from all .. _mod_wsgi: http://www.modwsgi.org/ .. _mod_wsgi installation: http://code.google.com/p/modwsgi/wiki/InstallationInstructions Behind HTTP server or proxy --------------------------- There are Python HTTP servers available that can directly run MapProxy. Most of them are robust and efficient, but there are some odd HTTP clients out there that (mis)interpret the HTTP standard in various ways. It is therefor recommended to put a HTTP server or proxy in front that is mature and widely deployed (like Apache_, Nginx_, etc.). Python HTTP Server ~~~~~~~~~~~~~~~~~~ You need start these servers in the background on start up. It is recommended to create an init script for that or to use tools like upstart_ or supervisord_. Gunicorn """""""" Gunicorn_ is a Python WSGI HTTP server for UNIX. Gunicorn use multiple processes but the process number is fixed. The default worker is synchronous, meaning that a process is blocked while it requests data from another server for example. You need to choose an asynchronous worker like eventlet_. You need a server script that creates the MapProxy application (see :ref:`above `). The script needs to be in the directory from where you start ``gunicorn`` and it needs to end with ``.py``. To start MapProxy with the Gunicorn web server with four processes, the eventlet worker and our server script (without ``.py``):: cd /path/of/config.py/ gunicorn -k eventlet -w 4 -b :8080 config:application --no-sendfile An example upstart script (``/etc/init/mapproxy.conf``) might look like:: start on runlevel [2345] stop on runlevel [!2345] respawn setuid mapproxy setgid mapproxy chdir /etc/opt/mapproxy exec /opt/mapproxy/bin/gunicorn -k eventlet -w 8 -b :8080 \ --no-sendfile \ application \ >>/var/log/mapproxy/gunicorn.log 2>&1 Spawning """""""" Spawning_ is another Python WSGI HTTP server for UNIX that supports multiple processes and multiple threads. :: cd /path/of/config.py/ spawning config.application --threads=8 --processes=4 \ --port=8080 HTTP Proxy ~~~~~~~~~~ You can either use a dedicated HTTP proxy like Varnish_ or a general HTTP web server with proxy capabilities like Apache with mod_proxy_ in front of MapProxy. You need to set some HTTP headers so that MapProxy can generate capability documents with the URL of the proxy, instead of the local URL of the MapProxy application. * ``Host`` – is the hostname that clients use to acces MapProxy (i.e. the proxy) * ``X-Script-Name`` – path of MapProxy when the URL is not ``/`` (e.g. ``/mapproxy``) * ``X-Forwarded-Host`` – alternative to ``HOST`` * ``X-Forwarded-Proto`` – should be ``https`` when the client connects with HTTPS Nginx """"" Here is an example for the Nginx_ webserver with the included proxy module. It forwards all requests to ``example.org/mapproxy`` to ``localhost:8181/``:: server { server_name example.org; location /mapproxy { proxy_pass http://localhost:8181; proxy_set_header Host $http_host; proxy_set_header X-Script-Name /mapproxy; } } Apache """""" Here is an example for the Apache_ webserver with the included ``mod_proxy`` and ``mod_headers`` modules. It forwards all requests to ``example.org/mapproxy`` to ``localhost:8181/`` :: ProxyPass http://localhost:8181 ProxyPassReverse http://localhost:8181 RequestHeader add X-Script-Name "/mapproxy" You need to make sure that both modules are loaded. The ``Host`` is already set to the right value by default. Other deployment options ------------------------ Refer to http://wsgi.readthedocs.org/en/latest/servers.html for a list of some available WSGI servers. FastCGI ~~~~~~~ .. note:: Running MapProxy as a FastCGI server behind HTTP server is no longer advised for new setups since the used Python package (flup) is no longer maintained. Please refer to the `MapProxy 1.5.0 deployment documentation for more information on FastCGI `_. Performance ----------- Because of the way Python handles threads in computing heavy applications (like MapProxy WMS is), you should choose a server that uses multiple processes (pre-forking based) for best performance. The examples above are all minimal and you should read the documentation of your components to get the best performance with your setup. Load Balancing and High Availablity ----------------------------------- You can easily run multiple MapProxy instances in parallel and use a load balancer to distribute requests across all instances, but there are a few things to consider when the instances share the same tile cache with NFS or other network filesystems. MapProxy uses file locks to prevent that multiple processes will request the same image twice from a source. This would typically happen when two or more requests for missing tiles are processed in parallel by MapProxy and these tiles belong to the same meta tile. Without locking MapProxy would request the meta tile for each request. With locking, only the first process will get the lock and request the meta tile. The other processes will wait till the the first process releases the lock and will then use the new created tile. Since file locking doesn't work well on most network filesystems you are likely to get errors when MapProxy writes these files on network filesystems. You should configure MapProxy to write all lock files on a local filesystem to prevent this. See :ref:`globals.cache.lock_dir` and :ref:`globals.cache.tile_lock_dir`. With this setup the locking will only be effective when parallel requests for tiles of the same meta tile go to the same MapProxy instance. Since these requests are typically made from the same client you should enable *sticky sessions* in you load balancer when you offer tiled services (WMTS/TMS/KML). .. _nginx: http://nginx.org .. _mod_proxy: http://httpd.apache.org/docs/current/mod/mod_proxy.html .. _Varnish: http://www.varnish-cache.org/ .. _werkzeug: http://pypi.python.org/pypi/Werkzeug .. _paste: http://pypi.python.org/pypi/Paste .. _gunicorn: http://gunicorn.org/ .. _Spawning: http://pypi.python.org/pypi/Spawning .. _FastCGI: http://www.fastcgi.com/ .. _flup: http://pypi.python.org/pypi/flup .. _mod_fastcgi: http://www.fastcgi.com/mod_fastcgi/docs/mod_fastcgi.html .. _mod_fcgid: http://httpd.apache.org/mod_fcgid/ .. _eventlet: http://pypi.python.org/pypi/eventlet .. _Apache: http://httpd.apache.org/ .. _upstart: http://upstart.ubuntu.com/ .. _supervisord: http://supervisord.org/ Logging ------- MapProxy uses the Python logging library for the reporting of runtime information, errors and warnings. You can configure the logging with Python code or with an ini-style configuration. Read the `logging documentation for more information `_. Loggers ~~~~~~~ MapProxy uses multiple loggers for different parts of the system. The loggers build a hierarchy and are named in dotted-notation. ``mapproxy`` is the logger for everything, ``mapproxy.source`` is the logger for all sources, ``mapproxy.source.wms`` is the logger for all WMS sources, etc. If you configure on logger (e.g. ``mapproxy``) then all sub-loggers will also use this configuration. Here are the most important loggers: ``mapproxy.system`` Logs information about the system and the installation (e.g. used projection library). ``mapproxy.config`` Logs information about the configuration. ``mapproxy.source.XXX`` Logs errors and warnings for service ``XXX``. ``mapproxy.source.request`` Logs all requests to sources with URL, size in kB and duration in milliseconds. The duration is the time it took to receive the header of the response. The actual request duration might be longer, especially for larger images or when the network bandwith is limited. Enabling logging ~~~~~~~~~~~~~~~~ The :ref:`test server ` is already configured to log all messages to the console (``stdout``). The other deployment options require a logging configuration. Server Script """"""""""""" You can use the Python logging API or load an ``.ini`` configuration if you have a :ref:`server script ` for deployment. The example script created with ``mapproxy-util create -t wsgi-app`` already contains code to load an ``.ini`` file. You just need to uncomment these lines and create a ``log.ini`` file. You can create an example ``log.ini`` with:: mapproxy-util create -t log-ini log.ini .. index:: MultiMapProxy .. _multimapproxy: MultiMapProxy ------------- .. versionadded:: 1.2.0 You can run multiple MapProxy instances (configurations) within one process with the MultiMapProxy application. MultiMapProxy can dynamically load configurations. You can put all configurations into one directory and MapProxy maps each file to a URL: ``conf/proj1.yaml`` is available at ``http://hostname/proj1/``. Each configuration will be loaded on demand and MapProxy caches each loaded instance. The configuration will be reloaded if the file changes. MultiMapProxy as the following options: ``config_dir`` The directory where MapProxy should look for configurations. ``allow_listing`` If set to ``true``, MapProxy will list all available configurations at the root URL of your MapProxy. Defaults to ``false``. Server Script ~~~~~~~~~~~~~ There is a ``make_wsgi_app`` function in the ``mapproxy.multiapp`` package that creates configured MultiMapProxy WSGI application. Replace the ``application`` definition in your script as follows:: from mapproxy.multiapp import make_wsgi_app application = make_wsgi_app('/path/to.projects', allow_listing=True) mapproxy-1.11.0/doc/development.rst000066400000000000000000000112551320454472400173030ustar00rootroot00000000000000Development =========== You want to improve MapProxy, found a bug and want to fix it? Great! This document points you to some helpful information. .. .. contents:: Source ------ Releases are available from the `PyPI project page of MapProxy `_. There is also `an archive of all releases `_. MapProxy uses `Git`_ as a source control management tool. If you are new to distributed SCMs or Git we recommend to read `Pro Git `_. The main (authoritative) repository is hosted at http://github.com/mapproxy/mapproxy To get a copy of the repository call:: git clone https://github.com/mapproxy/mapproxy If you want to contribute a patch, please consider `creating a "fork"`__ instead. This makes life easier for all of us. .. _`Git`: http://git-scm.com/ .. _`fork`: http://help.github.com/fork-a-repo/ __ fork_ Documentation ------------- This is the documentation you are reading right now. The raw files can be found in ``doc/``. The HTML version user documentation is build with `Sphinx`_. To rebuild this documentation install Sphinx with ``pip install sphinx sphinx-bootstrap-theme`` and call ``python setup.py build_sphinx``. The output appears in ``build/sphinx/html``. The latest documentation can be found at ``http://mapproxy.org/docs/lates/``. .. _`Epydoc`: http://epydoc.sourceforge.net/ .. _`Sphinx`: http://sphinx.pocoo.org/ Issue Tracker ------------- We are using `the issue tracker at GitHub `_ to manage all bug reports, enhancements and new feature requests for MapProxy. Go ahead and `create new tickets `_. Feel free to post to the `mailing list`_ first, if you are not sure if you really found a bug or if a feature request is in the scope of MapProxy. Tests ----- MapProxy contains lots of automatic tests. If you don't count in the ``mapproxy-seed``-tool and the WSGI application, the test coverage is around 95%. We want to keep this number high, so all new developments should include some tests. MapProxy uses `Nose`_ as a test loader and runner. To install Nose and all further test dependencies call:: pip install -r requirements-tests.txt To run the actual tests call:: nosetests .. _`Nose`: http://somethingaboutorange.com/mrl/projects/nose/ Available tests """"""""""""""" We distinguish between doctests, unit, system tests. Doctests ^^^^^^^^ `Doctest `_ are embedded into the source documentation and are great for documenting small independent functions or methods. You will find lots of doctest in the ``mapproxy.core.srs`` module. Unit tests ^^^^^^^^^^ Tests that are a little bit more complex, eg. that need some setup or state, are put into ``mapproxy.tests.unit``. To be recognized as a test all functions and classes should be prefixed with ``test_`` or ``Test``. Refer to the existing tests for examples. System tests ^^^^^^^^^^^^ We have some tests that will start the whole MapProxy application, issues requests and does some assertions on the responses. All XML responses will be validated against the schemas in this tests. These test are located in ``mapproxy.tests.system``. Communication ------------- Mailing list """""""""""" The preferred medium for all MapProxy related discussions is our mailing list mapproxy@lists.osgeo.org You must `subscribe `_ to the list before you can write. The archive is `available here `_. IRC """ There is also a channel on `Freenode `_: ``#mapproxy``. It is a quiet place but you might find someone during business hours (central european time). Tips on development ------------------- You are using `virtualenv` as described in :doc:`install`, right? Before you start hacking on MapProxy you should install it in development-mode. In the root directory of MapProxy call ``pip install -e ./``. Instead of installing and thus copying MapProxy into your `virtualenv`, this will just link to your source directory. If you now start MapProxy, the source from your MapProxy directory will be used. Any change you do in the code will be available if you restart MapProxy. If you use the ``mapproxy-util serve-develop`` command, any change in the source will issue a reload of the MapProxy server. .. todo:: Describe egg:Paste#evalerror Coding Style Guide ------------------ MapProxy generally follows the `Style Guide for Python Code`_. With the only exception that we permit a line width of about 90 characters. .. _`Style Guide for Python Code`: http://www.python.org/dev/peps/pep-0008/mapproxy-1.11.0/doc/epydoc.ini000066400000000000000000000005651320454472400162150ustar00rootroot00000000000000[epydoc] # Epydoc section marker (required by ConfigParser) # Information about the project. name: MapProxy url: http://mapproxy.org/ modules: mapproxy exclude: mapproxy.tests docformat: reStructuredText output: html target: api/ # Include all automatically generated graphs. These graphs are # generated using Graphviz dot. # graph: all # dotpath: /usr/local/bin/dotmapproxy-1.11.0/doc/imgs/000077500000000000000000000000001320454472400151625ustar00rootroot00000000000000mapproxy-1.11.0/doc/imgs/bicubic.png000066400000000000000000000477251320454472400173070ustar00rootroot00000000000000PNG  IHDR ߁PLTEۻ٪ٷխӞϣΏɠ|v~hnֿսԼԻӺʩӸѵгθίί̫˩˧ɥˣijǧǣƝƝŜěٖÐۼ^ƺUCNݸN~}||{yxͬծOÏ~xđwqsivmaZWQYSzzxqtrjji]\[MIKGHDC@B>:9A>0.BCC...mALIDATxe @SW66QYEE3J+>M+5M *[)" iV"aMRfj lo+[XÚsps9}>s. Fu 7:\BFyw`{Q f= [܅M&DJehW0N$;(zUQBdBՉU5UCʯ;cg< 2loM?1H&>m[=N"h,o E?iaC'D'ɹLZySRPz]bNRۜm5zTTxv*ΧlSwe%KmeroWf]ĭ<Kў?Zf8 -U33N Ic0=%V.+;_h\b0zGe' G_LMhm?a:SS T`wֽe k}&>=hm-{&n~hQԵ6~~ @2Z$vʗt2U$>K|D.|፧՝g3ǭ SSv`^`61_ {hvH~bm–ւ]ns,E/Y-h'!_ =/+e6sRMKp7tJ (g~;PTƦ0ܣ{7\|SsH*t=,>lon73!ل¯ ^cli~z"$k"ҿ(J6& cg"Jò_NH-(/t+N9nK ÿy|74q.[ %&o} ny%XU,Ex/ؖ%TDt}^'wvv{ɓZa-xn2;4>3:ѫN.ԯޥzR"!jmFbyq5ue9 AhRK[^  -Қ ;y5s)3-]dC_7,04 B1>~7mp'c1sϠʫ0tzɶ\ZF1b ]\b"3&"##co-/ !1`PFޮo0NC~yZ5r~oE)P38%;1T13Y;ӛyR[m7V]oƧCCg/jI;gϛJ#?綷N0eX" f֋lo*Dh9:DLOEmftu=}˓4F(OgݒCL􀤬1-_3D¦ VQrA{\/ 0}2ڠ.dZAJ} }% t vPUQIrskM$ =XAWۯ\Aa'2ܫ%vNck+t[%vA?']W__s0\vn2Ie{U5!ywBvZj\Ц"9 z?jޏ=NݔZYM¼N<-Je Vrzhkԇ p(3lݽ&t ;I5 Dz:PhĚJ4W:wVB"jGU[D L 4z77LͪM0p4<Y J[gNXP=\$ F:@̸1K@98 B)wԇՑ0< x۴/ hLN{Ap<'IŽ0h}3|fH%E5fE!&f'Q,wd2xԺS -+AJ6U$V7<1B&}6@B?1уѱ|o @W7#Y~ #lpG K1ƒ04Bb- 2;NDEaq\ h$pu:|=wK"aInT*zMVF 뚚`̯[Y K(yBVߏdҐl2C #(Bh^Hd 3E(o>:K׆$m(]Ȋꚿ54~zHytZYpȪP"=E#`Y|60.5/}|!]= lDzU"RXdRH|/:sLߌ}Pgfkfɩ-!J x%ŇGj$ƆFR:iHar[ܘjz*@hldEefh5u*foEiCmŅ0Rl )( QR""a@)^"Nޘ1WS%?O=@KaC?@J.8^\LӋU̔B/Ef!jgׯ:N@m =GӖO=*A mj|/J^IDEvq{vFrr?ҀwȺHM*CT^К O9 LGvNƶYtd]s[DK|VH9B+s^-nq#Xx&x#pO/U.M䔓5+8,}"Q;7n)!tݜ~ `KbOAC#E"O -hwcPyE6<D\@Z24痾q̀^ݺO= |(X(j랧@&1`=1G&m]MO=_"J8f1V(SncxLChCDuN3qvܓIT6Wg 56m3V1gAxw_Ig<{/zeCzF4FxZu %nɶگDuq2،BT46"{8GF\w$$[p|iw7BnS*N,ۇOiLj'&ށ:帺]rvrWGC9'`r0+]#hPn}k]CccN~eCǃOBՠ{,38<*6Yt2]OuE&tڥ\a5p.wv|^; ߡ n`=&-(-jxm$WLFZR߄#F.ӭ8wnkY ^v^+2PEtuc<[ 8^$̤GUV6Ac BUQ>+RMfE06艠+j}Q8ӌVXߜ/ٗQ`y =ܺxgf^M¶fͤ"6݊6ћ<)DZa%Dfh\VQ)ഗ,۬,hgq_V%nN+47^ O8joS0]! 0)NDN^ehhd,܋3,,=_ :={0#"3ܔ5*L?l+& -bJWwWr% Bmq|!vVV^qeښ^[wVi6otj.?̦\;?p(z C..437VYmFfliy$:~^=_$q:~]cIf`6 gzY/ NTVf C xPAHBLt%#ȹFZi%!1smFd6#ΑvaxgOl9OHg!^PipNF.7pDzE#F,z9GkkDضmK~Lzpue1L JŰd&.=L/(P⭓;Fe:p[1C"{l[O$'}⡪jUgX;7SwdɭBvk_g itxUH$ i/ev+{/hi@Jc]dq*>!Qt&}ڬE ۰<ģ|}ThKĒMfЙ\H%gtHn%Ch ~mL''켽^^NQIUd==)ƭEŽ _ &S0oDlEM3?]6(h4^+K"B_,cXD6! KI"򼷵3^vo.4RsA dz`CU`Xa|L4DtoQhm6\vR/#4,3ϨtU[T"oH"* ^hYG8$M\@a*;¤7 O,R}Tit`;x<p? :o+HB,H~S-W>=ܣm(5=1#_J@\3Gڅ—np Ofok1۵a[dȘAa3a!M-3[r1z6WT_]UNim;"-$.P<(}:8j ,2ЙbS~ʉR$2,`d )^>  '3Y"QeR2#%1nJ>**[lUkɤ y/BҼL1 &FR)/4_ƢޓlTwR>RA@@7o.9cBE 򮒒r}@e%ܢ5|ږ;]*jb"KϠm!- )n&tv>#<'rQ.@D&\ٷMF;OA+aҠs9h4G'ZȦ(3AGh|L7aꭚL(4<}#|~~gwxQ;MP2t]=Te$LHQ}!PThFK _!8I N.kijeyE}],.Lo#"B-xwPNy NP*HmpJ¢ڗSMZ^\6Q4yBĐ}! d)䈂vXi chfw;Di)D19N, M!ꐀ-)n!{8tS7ꫧi|?2xYXElM։+(ϻy٘S?;v}NLp@7gaS4WIf"p(a/} M.Xq7+,c~mAKosd,&G(kGu=xaWWb cD.L u~+ӠqsB;EV1d{V E)MZ'E$r!i{$I/ q6nʻyɟc ,x,{Ak3݈ˊ@L fg'x1l tJBo'Qء^l!q|[;bW44OےiR7BztTMNFoSL?HC: 1)d?3$ eFXy]M hI%e?x%?JwV_9~EZ-ŽuSAl}gf\)Sj\އ xRZ˫Hp`֔ͩFFȱ/`|{\2駯!t{whi5~HH޵G {FssY~Q6M9O7'L騲 <Εڑ=ėgЗHNݙ_~MEUS iOrT?~V-AwʻkŪ0?Ph2kd !:lz?PG4t@ vj%@8?;`RVz/!.:s!GiuGZ|{=|u|h:sJJVJ[Z]#u!dkkknuumvU:NX[]SY>6zf#XY/-~:q<':W>;oȗh G_J8bna6 f,.jHtݒocܡ}I-AK_Z%hN _dZ39ȨGp*z}45'ǽ{}mo^\XPެ5⛢1%*j J8gu zOHa]E/5C6w!fk*cmf07*3_U'Ujx洵pX߿\pPҤ }w,_C:p4RU|Ra}CyHwmg'1^b95*L9iG M\J NVWgՆWׂWkuX`kscc24N5~ei~zzrqyqiqyRv(}aqqep5VfZQWXNb{0+3e3/@,ϋkG ]u uF8 b_zadK]R2()#AskUanj7au|uu$_ZZBkqQ177<3+sNP=crq`ra~ba^R/&c]LMJY;QZf .ڽYAjT΀XW*zNxo !'ǭángHV:&!) dP(`}X]]U#kYs_FV*ˌ@W1 #Nar\? ;eVI VWP<~i̕&`xHY=읾vDys)z 2ZrcpұPQ" Yci#;ڽֳc6sܚcnL{!gPQYVT//d)5,DžfXT]v~8\b hiϬGKؙvʻ+}I^I(vJ^['֚EW~moa|xuMAX]Z5G.69vtkdx޾$Iʨ׏r{[DSg,Ke{HBQszNk$X M$f}09Twk3')/AYy]+E67!R*}Qjm^<0C"@HC؆Dp͸Fꗔs]dcPL)G{7y1+k7 FsK(k+%j{xUo76r<>2637 b*{NU lhř 'NQq0-N,=9vhzQmpc'۠r (ׅID/ ++=XZwFkj =ʼngtf6o7R-@3_ [: hc $LOvUSOS;C}7J=S[ISW<}X\ \޳brRxٟ gXPN&BB \cy)96}ʉ|cTG'Z zTXQꛒ4=+w-Ž;.޸ѹ(OfȬ+ݰ\wW\&v-U@N m{1.:twx&!!G$(.y&yۢLoWmEGF&ryl۩V$`app,TU08ncfIYm&IBwɯrk)UeAbX53Sj{Y&K 0އ4g7.h*-M\Pw ׉ _t--5DߚG+=WOCg5f/tE:geQTjܢر#Ԛ#*ͳj's 3Z:L)z}vAYUWoȗÚ5w'CGhPʦ4eMjnh77roeO;#)arL|ES.j\\Ȯ8j|B\աC`ϡCV৹2#'_;1@!3<@;27D䴼 vu)f%|5=T@N@7{3S#TtvP;{=(_z\y.GD޸i@ŋA~'GFF:ikkV>yɓV4gkGF)5Nq|rR>ݧ2*Xba~yyiûpX| 2+RVe`şR.;Qm3Ύ0*:ʫI&ުR̭)4̜9;{xT6}q7g:_2gF"ftݿzcţ"yWaU"05/; ڽ@ESz3l 6ز%V RAaz̏4R#tGKd.ff]Weí;Hd2 i+lv$c ws9XGǢxXPW'p03Vhs6fNpmLk GR/FAV22ײzq]_,CxuxtNSPdá Qjb=\^K3'EdU)49\Xk!X-ᵏgqXcb Af՞59B*Z[%d N,MNɆz+Y[ݝX@JYR ͮs2a9b ! xsgfk5ӽ-ez߼:17zquG[nmVcLU˯K++۵wC͕q[? wJH)ߋN)4LNQ怌/!2 Z_;_`}F]ZQۢh[]E:Zke2##>~bFVA_I 6ɚ<0l_Byhߟ /{^AϬά1ZYہObkok2pG5kQ T^oF|>^@{f6MSkeP8lʷ!>xaT1(?^D0R#l%tv489N[!U;|X4u._{w?gwN6g7zV`иt,L ^7ּ(p%3kF$%pcM9N-xmnuE{|JqjU1()TXh8bPCƿ 8w멥iOGlXjPJaKK]6|5!t*P O  y ipz=*ơQ z?")(0_x5109=Y \\Y~]lihn@|*"'τmnkUS"&j#홒oGboE B~4|èLpeq@0}jm,"{WRL/NQP.Kx|?_EݒS7N:g  #ojXa0Z_iNq*`Xٷji>sNx^$7;U݅s~g'?|O>lraJtsOD="Q%4ߊyK|K脇 0Ezwfd6+vu/gdD~#Utk{  w.[ :p sܫ7Vޔf 8*1+513xbG엉ifw&[| XqfǶ_\6{PgW J3co'Q @ݼgG1wICCCgq_z)oJYfͪJ%z1 ]םc" D艰rb,Sa@ EywJ`CȿLO$ E ~pBR4dA˝_rt9>.h#]m@` E"u 0K"hhWϤ`*lfg p15;rLj&Y98jfG/_@ɚ]J1EM] ZJ(T" 6E?6>j f3#={(TYs:vweҲs^cRY<*fK2<^A=d{Dnt-0AsSpi"TڦB%4ۑqYara~$qq7y8=DȚ=`gOsG:;vUQq+u?v֌:F1{X\9ɛӺR c+neaB.`/B+C|Iؼ;dG0qu=#~h3]Yt!-5>mltX67xW%)T&.]62&➸q|c;^"א7쀷tO~_]vQމ^[6X1zE!cZ~Ӭ[kYTz^U7:0 kzBH$*]v+)zCτ¶W`]Fk/"dZ5CmZ>!emί8N3w.5SF>pM d` ^oVRjr^k_>o=!O.LmU|5̀7;v'Q?RyѦw*h %3]ŋ.~?^FX%$~=s \XW{FVVS2WGs[Eob .3ϧX0t4mjͲ"5#on,ߺZݫkZU =Zr) qX)s9C#pynVL,+fPfRV#c j=V`oG\-?\> _[CS#R5Z/a7VAܟQ]@?sdznĜ2Ot&*X:ir| uZf|\3/=ؚ9C5zzztBpI/)V8:PgwNS5Գ[kgj#m[|_)z.z**kjyUF"y䬥5Xiu@3>LUO܁.1"8_kцW9Z}1;잹36v/MT6245T)}C,M[Tv X SVE} qH3 R|NԺ>4uke3Ygg/3ddzw:܀GijFU[wDN/V̎^wuC9/]ds#ԮWd#,0R;#ln~A?$ 0Dn9QSzYwvr4\փI5f2n[ Dfɦ}%\q;>؁g67EGtտ5c-?ix89; $$$$S!,nI ܺ-S[x2=d†-ƌKS-mKXjV +w4F1=u]~)v %Q쿹L\+BDUN1qwaڥB@VgbtYO2[; |X3VK-pXɨPYʃщq[Z7*p=EAZ؅7ƶFҕV"*#{)zlJS^3jeõ(W&f"mAIm+$Ah*ipiߌ vJI}T]iې j1T}c.oAWЛ뎧#ޘ.Uq7ap0u`&r@Ii}880n^1KUμzT'\],jG0[puByT^6&EZlLm-՟q!Fk>Q`J"U]k=7{aT\G5'LS>뺟Z3nʐ?@P lШ}ƢT ذ0cl+%MKDtRPO εMj/yH ;#b*[}TwntO.#oN؛TEM$2+ A))QjJۉg`08DOo}$ 樔,kHI15B,-|wBa1 w}_]k.'o7%> 6 {;_ rӸ W/g^=s=A@%^]}|+!@ҰYZWb,,gzU:rga8PAO5`e=캺qFIܵ!¨SΠxwa[]=%{9\% g6+HZoczPQW9 K'uF˺9=[Q[_=1JTzgܛp/E^vۖLˏ-~nB٫b3.X9mxsp{+yH3N_†"~G{z*z߳,5؂gC~ /ɣ58FսR&oR$Ha#eC7e™xPBH^C3BL#< ԧbTP;剉Uxs) J%4,T,p@sKxGآgu&;qY^Tkk*탨_:& u\qfiZhv\\ d¯DceFX91eh`BRyP¦SSϤd8IvL.^!2vdsqE}ڒPH> y=N2_CjՃZ`չx#ۨ]-_#5mO%iB@Y\s9sZ5{u=n[X7Yb$bt4ܬ1'P-nQ_XdJSb|+?tgl=>Y~r lpg)D%ijJoRa L&y_w=4*lM۵+ -l/歞{4W /S豄kSzJi{ Qx&ɣ`3[SA!§&ï镈.yu+L&S Z$M-M ?:Qͼ]1I/"$ٛ. &)Usѻ,ifbݽBپJ|;d,*33 680Խ%:2D$&c8,MFIS-54Y27bZ@ss#B \0LSq}d< ȸDˋD%k8/VZG̋`̣Y(n xC-G^2[&`FB3zhև*'456Ӊj^5r$ݻ~{VCYx? &NpjiF<`K #m!ג6)RN?W *ٺiM5dA_%ⶢrNKfe 15/Hp?q˽n2J}N4ct_1qwh|7.ZWWѢӣ+m`Ky1$ x1i6|.Cf6޲.3A }ztحuXP'я1#-–WR:3pj:a߳i? J] ֣'g\gJqg?(FuN`^& E,U* _Q_]-diWz7ck\'cƯ&_wj?UЦZhi𽒖N_b[5BnHԫDjSXQKDFz-LtMT7#d oKgWWʽ̴+}qf2+MGUӛqG7*gH~RC>){.ɵ6 .$Й\ѬS۞=q=\XXPBSQ;PR./,N|1>5+irKI~֊ -XA] X<\eG+]G7 IFoAR=k ÞoUZ,̘x=$wO/M.|tga[ '[fd`0 n9FB91#A?4Z_wi`׉?&|4X1SVo $UD6t؅3,|EQCkSnS ӣ]GKYusC9٫l7c{PTV fHm%W}6%U&"42FTbn"XWq2E$|˯ONm=i̒H?1\=)3zd`aqaA(jvge<{kLT ᲽWNu|%1 [*ءI_cov}rK*^T9@/gmG?ʖkk%Dm>F$SXf1,Yߙ ͣAdԩCU(Ⱦ[uMmmmo*1"򷊞Bi~[uyR%{я^=>k}&᷵9^E>\ /ȻIŰf$W bL>V;Ee&a>;<:] kIENDB`mapproxy-1.11.0/doc/imgs/bilinear.png000066400000000000000000000442341320454472400174640ustar00rootroot00000000000000PNG  IHDR ߁PLTEۿڸ֭ٸձҺҡИКɻ͔̎ˇɅvmwֿվԻԼԻӺӹҸѷѵдгǽζșίΰί̪̪ͭͬ˩ʧɥȣǠƞƝŜĚØսۿrǽcTZٻ`–ҳ۴Oųl̩ȟœǎčĘ{yq}{onfe^f_~}zyxurrpjgfZTTOOKMILHKGKFOK\UTHHHDDDD5EWIDATxm T[W.,)gs[h=SE?ӀL$P$`6 )l(1lU4cL^܅Aˡ fĠ2?J'wн{piq x *#!c*!{ Ldkg Ӵڂʻп*'~?wdÛ"2h!{=!LO`+M7Ѕ Xܽs {.7å* ;UV)^)Y[ٖ/KVFFO< ?_2 E Jz/>qI"Ά;LXMd_+Iܭ .eţJ|&oo0ki1i%7ZRE}>)I ۼkS[;b\.kڼKשa($+A߿ nVU=V'lR"byIFvzRUo:/+BhQ%eKt^^@+Xf'3t"F6K&>ǼD ;S/^NhB;$BӊoqL*K<2Ŕ0~| |^V =ƽqE~aTNgGO9HwsBgs|si 30UHndOkLTг~sՄl0#'ABG]e]m6LWvKJE1,& t}Y᠆b6aEgWzgH찜${n`,Ag6 i"{vl"~ziۭČtqY*V%ԥ4z}c3qm*+,$z$T޵Bī}iw~BU-lhiB8 %˝:KWA2 ĵ[?2sˎO-+w(+< ܏ElK_FDBg& !{O&s{ߒ/#0Ԃ{y#WQ%JM'`gI$eV{"HJE<@>w>'E]^,SQ82Z{'z5J^,<0s6u@BTU*dLnl'wSs.]~k Sbw>,.T> >2R,QBZJ +;ݯR.Y9(l.*?lJpKZY_[.N|188etG~RE-.loHi;o񑨸G݇u^ ??{ޖ-Acf 6 $TԒ{ǁ6OK\}) ˄W )QtɮDׁؙ 9WMo)C~2m2*z;*,qrO;iHH)cjOu!A$rdAb*-Y;HʦD`VW¨8\47]s()K(aIۉ e1 Cu3IrvG;穃ֈMvFIg\9p ͱ1`RdQ|]ث3m{#?!ȷA^KW9:= 4{XCx#3RbjN~$ݞRWP0"8ќїQ@OuC@4z$6k;uΒttL\ZfY.Xe粪y~ٖI*ʌ pƺvи0&ЈKOKM\ӭ~.HxZE]d dwԊ/\FYgd-3X;Nۈ ^.9Qt"O)ԣKMKGnոXTK:" M6|/WBANНOރb(aqW ;ڒ1/wʰ; VtR,IRJdk3%We@pĩ ߋj?r\﷛x(I&cXn'$ +N:|/PMT䨬#3Z)liۄaN hV1Z{K2SV0^'L۸@Ϣr6%!4D?‘_|ЎQYym#U؄=jP;װAY5neNXRX&Të ;XT*TI=XnLKS'@v%ȩPJGEp2t6k5By305Gjx-B3HԙvL3'ۄ:!jt!\-d]ّi`k.(^uH(Mh ascd[Og[UZF5T)X@;rn!(L$"f=}'*8,K` Y_ ?A.+”sO3lrnaޓVby8DŏIʊLM6:cP q#՞/=Ź񛝗dj 잖fij )ɗWMm3ZOZ|ld` ^.B9 u y'wQ <]Wyj{* Edw=a9~348f8|WHw jfgIES[`YzmݝAH6zNf3qg \JN.-UBҦz;뒬*ols$Z:;: [{Zz =1• . D8g L8Ycw)[?(@SI?nB#k,d.}`נzֹ4iBzz[ur`CQtakC]6d3Eqʡic(?p#+4HyAfyE34[d$~GtVtYsIakNC! dn!(x::si2 pd 2P,e>BcV'C!Y!$+ۛcQJ.o5So! t`x<웺o <Ȼ%S]d~hFL$ɽB)q2"6GVی(7(n8k ;jhWJ]Pl;5jSΰƕe!n A<*]&rhKdi3vGo}?bR,Bv TK r[]MťXgbg)LKrbtYspξ i`_ZQٛ)vǑ4aXޗُauǵjCd?evWebb@S3Rryɦ,6pu/A4.d@FjN3[p0%;r@.x ֈl /I-mbgF'ց6q!1,cVLW+p"OG>]8Z)u5xv=%2pWAtd{dDHS[ssY%Ӂ 'K-xDm{qK 1s^"%F.x6j?~}{sIN UDdW >t~x$RF>NPG 8Ϸr֒B"(SĆg.,,Fs@1$î:Y;f^2K=., H|Se /d +%ށBp A ~sZo0.JwAo{̙fG Ř Pb(3oks'Ά%B#xm)=nh!‚,PXQIKQEFE(ևBTscT|묞1`GaLvRT1-^\|:Hq z3ʧzlCIQaԶҍ)l=QV 3\#N1gi|f`TXϠGؤpOQ*jj j߄Ёς:\IQG)ғ#"q绽>k#̅ͷ~ F%meɧIBfXA\{Y·6LkEkZzuxc'(7Ӄ@: kҊJb8'ccKf6]pKIٜ5bb~D32C))5rP; r[,lJ ] B`mhPP sE(gER%ZtI4;Xz5+4g:Ax),7sxoW`<=.6H,v6Y/(Jm(kL >n5u>1IͷZ;g4+_(:Iwe.HyFMI`FI_N8Eh:8ʩFccm3m ?*g\&$mxa?p6>`9C}lgFG^?P*T.*9?_5X)nb|yH%g0вwr0ʇvaa5{ݔu5ΝmDWcjhgOG 1=~Z!}iYU=g#ջtmc68^WnL1T .O%:I]F;[J娚k{Чہ(g ٞ! ^2 pң8ǐ pxIMΈlzrX01#>j*%޻{9+uZJHݠH*]7E `AqPN-q zOvv Z>;}|P:wU L c6T"g/&Os൤<UVi:]՘[BJ oP5"X0,3G~aav@r ff7O?GKijTQ''b3ga|bϟM1AcGZ8 tj.SrrՈ_T{{r zxfvZ*~|X]\Qnnnn+gXT*[;+ W|%l)<So.dA5i DOnltX\?c6s {K7veSK{%]+(vvx\XY^zYܼR1[ j&ɊE5RQK7XYg!/묩c&ַ Qض? D0h8v3":O?aV'@닼~'[!My}8T>l."ژa3"oIn tYxFJ>1Bbl9 d;?l\Suj)ȅܜiOX(4vWw]0 Զab`z 3;ܹ?x"8Gi\ # 8$!`C~} e DIV(G!v7mrў͸#Gd3$;K*"XIR1-woeoo6]i.[xhu+iDž )/ WܺY7- ( ΁Zoqh\6$XZL7B8AxG[]S'B Y}K#HC47yݽ|!WKMSFUMEǪڔa NHl4m22gߊce%9o_@[`-8SXjqhB)K(bzL@moG]#1 ;6JRT?3TSO6'?tڻqjTQd)BpXg nPǂuKvJ!X3Y+r&t<,%IQA'Eę(QmK%m`X ) \TB;#\:[%rgsSϾDѣ(}e8aT -C>i6e}>d.w8sIj 8Psmݵ] GIҨjcw=6k{*y|胼lΓq>6XEkP0skl}?Df;jG"!ROl|7̏{qh$\-%Ψ=y+лhF{jPXkW_']Tlm8шMq!bGx&f֦A*.RwI^ .xt[ d=錒쭹ʲBdBxڳ'w/7Ж]Lx<qbrb/벥Օ d o'˕Y|0{&Vs4ȶ'"Z]U:nK߄A(&Qm2@TgH.;X0 QB?>|zyܳˍo oJ!2se3NjJba.F00DmV1"Tw9ʱG3M څBo='p,lz?Dف`ml.MSn_)) vɠv}SrD``K3:)7(0%eL094=88Wb*{m;+J q-,kY#u6 =~\jEVY Z ? T6@D;[D; U T8!r1X[_ZytDocnBxVx] 7 ]}n}#0ӂ3ҽ.[R]`}>R$pƶ\y: dg643f|kڅHw ɑO‘j0finm&zB9'YrS+'po!(i&߈@:.4VOfo8m.BP u<$o!/BV[G[OVs۸/*YٮNyRQkQ#BG7 m`yˑ i) y"ډl1+ܲFc' 7"yzaAb%zVuuwl!YYP|:ma}{;BYgh}C t󛀈BA Q& T GӾHBVTmhjV9.*Iyc:K8GM*ve(ȜcK>>` yt|""e2=}7fjH*e3 $>>aZӨKBG0Ôs'pW|2<8鏕*+D#GZYyYѱӡ3ƨkwJ>ݐ[gW۰]XWL r[?Ʊn5jP@5z3,Bp3J?y 5'ga"ׅՄԅs_K_E|-4wyY^E{!k-Hy>(f@ԋVT< ~~|sJIWnWRx!`Uf)*Ũ1<ۊV(|)N_m~y_̝RMK^M##AJs鹾]Kcw #1jWIz~z42r,8$h(޻@eCR%9K%VKv; X#Xd{uD'wqNbvS{($x7e]jHV{M֢]>do8.(>Dm5"SxṨLƓB8ԸElSDyI뷨¹:3Rvh7) Ӫ5R!Y{'7f i՞ڛbd ڽU.#wOvR1569:6.^{6{DmuVq ?: ~*$?Mt/.wUj؇BWY殺jU#lWTڍjw5Ujq|ܦ+܃6P Lhts-K 5X-4uH\R媆eUNj.C{+tCkZ[Z{{*i F'BHd|'DcvR&7i, GXjXX}Ij8 ӽ)$ʟWT|.srs(;Ee\ Hp:n?a cٚ1U;(w"1Bҵyū;,>#GR~ s7 48je`SQ~U\^ulj >0xl/~ )P; x%=(Y 4Nv(U<ZQm840LZ/,^Evi_@]_հȮ e k '`uɼX]P^7Ũ5BFmp.xFYkq;fc5j ó£`{ EM'%q 4lNی^0Oj4$:|wg@^ < yma.<}9=Zx2:^Gt&Y.J0]=XPo Z\y5C>;&^[hwfچP/9 rAG.#62:2矟ZQi,f&1ֲvQdGuayG'[Sõh:2Y;6vэw5z>^P`:y,[m.0 88S$B y2XS>.ј([!̎믄ݹSGe-RFf]R庺|+C5'_ j,@?h豯w vz\>ADʍ8Dz]#AEdх:WWF&ǡzF٠f y>R'少ϛ/,/&'(b\4NY+߽a˭O trWCVOB>6z }]ѵ%=~j~y2k[w'/r6ְ-u-=_a\,(#Qd?, dZ[M Ƃq+/#=T.}vf#c&ÿMK566䓲5y^(4 o\V$I~:ިc"jbkzzk4t8qEU9$B$[o.YLl+Uc@&@c\dZ/#n}Ó-Ss[C}@d #X6A1`)r+Ko3FG޸2zKoNJB3FA#Ӳ3'T6 !~ @"Z}`Vs⣎ץU[;/-66Tn쾻1BZ\rH2Rmd8y$2MEޒEt3ЩYY؄v7 dZ$CYqeP])-Ʈ o1U bvvnh)uڒ]T7.W;JNQ{럑4 gIKraˀ8Tzfgv0޴:^_}M. aO\U{(MM!%2䚒1 UjTؚM@W^hTw^SZEB2V>Ƕ&5 pS{%{'`*+>F޺:Civ2ΠPy|*~tX;uIwn4%@oUj폧<,T)H{ƍ=#tPI>e[B0hK6SAÛvk5u<ظ4R;+⭒-4C`e # /mȃy}9o_[ 2G 7wy+DlMsה&Re(p$w^Ck6wl2 =,jt>VM@Bق_g cz m8-}f_,P +?X36Ջ[6=B}tv}ru;H:=N!̛#;9|NOݝ^E|OG$ҞqA,['z0Z*d!ոz{gS\f_u^ ubȩ;:VW_4_L87?lk{ Xp*Ѳohd% _S9[ATz#wms[CwCoߣ<$+K8 pg6'OVcɌS֠4QKQ9!"r)y'ENv?{`|Y(" M <~ĢP-G&VmM ʭAtР w+͠|{GZHgku,lB.8Υ&QeN`hϨtH.rwy(1T3+UcKFwbH}Sjqa<:iְy!4˙2d+iipx ܆1l+ŝU&:J>7 %Mj` ^ I@O+zL[Xȓ"-v\m(t-[a:aI ë-cіxnr9K~Wdk8^$1O}a9Quya!V;QNHS"B B-{ğ}!!;:p_MT3_lĴ ު"I]9ĶTL6BfVYTbK%ypŔͿ&_/Ym:?-sVv?ZZ&j]0Ge2ŒBu@BcE.4\#IBɀJ..b^[|f8:5~` VIBuwvߣ]Y|g!0^.˯ %E"bF)DiYlui|lqx GM:e /8 w`j^wuTK{% zΊB3Jla* LӔ򹭌^ꬄ7/W76B«0 4m}X(r%=IA_]I`N_m)`Vlj ؆Quհ`1^oExp%o]KDbȁBu#P템E/`۪v 8u?dw7'X0'^d6Ko= p\r)%1i8\ll ]smzlP΍1v5LO-~xX֧WJ%$@VjY}g9IrQmEu[% s֚%H&"$rdyk/ۼͩ\͡~^$K}Aj~K2um4@Ʊه\$Ӂn 3x/u4? lV\)m6Z )A4إZ"^)ߨv*6fDxP {pM -`9z[Zu&.N] sQuO7[+@Ew@Yll~$)F䐷m'ZgLmޮӁ|/XOdWs!IoZ `]:Zdkyhe8}<( 2I9qEMqKO77QճI6J7հRpXppVSǹRձ e3zjǻQ8i×<U.;\_0Ka!.ҸNR+p^d'޲ui)}Wǖ+հ<8gKl΢qEk\UKvDk\޷H |BJ7",2UKvi){a[p}7`ݽVDZRyB..EEQ ,v =ɨ$.zC vͪ( q(lisF=g?\/F~wy!~a*#Q]vd]VFXdOsB(9o3Pa|T=r4Sr:'\S kGx6. !J9%5q꩹]6GLIb˒aݳVȘ 0[TuwMQTQ˛Pg,*vdg/r>=+f onjd0];+p1~:W;19̼RYj *R}f Q BT b ޅ#oIrk3a.CjjPؖ*6z[݄͝ҕ-rq7=r _wQj Egjq}{|/gj[Fx s l1YUcRJVꁮgg75 KW+3 vŊ~~ԁ~ھO s Q8ZXvRaT;Ee)* 7DMygT뙤9ˡY?6' 'GfOv3ά7UJķ8|~ZC ;/m65E ([J*%rp?o|jd:Rr[>IMM!hCF'f|a'] T>ywPaTӽFbY?P˂kuIYMw P6S7O=lέ>="[nWWCܔѕs-|#N$~䆪SExd6yy?ٓMg^#L.]²l{c  hH7"DdHK$Jxxt_x{M%J9^G-8 RچѱI3"7IENDB`mapproxy-1.11.0/doc/imgs/labeling-dynamic.png000066400000000000000000000167211320454472400210760ustar00rootroot00000000000000PNG  IHDReiCCPICC ProfilexKK#AOZ4d /tYWha6NIl:k!+f5 ʥf7܈_!nTS)B` z:uM1qBsg/A*9AS˸^ Dҝ.ެ9ɻQ[ify]]O%cek^7r4c,@gYKV! -7CepWd4_ =eࢵ{Tk-m@Na_}g?/ֶSR_=;TFjQU!t1qy1`5mQɤGhjq3Kg|*BH`I0"4"\a"pIlT}eY|='r\vL[,ZGEXףW= 5򈾘XSˣz pHYsaa?iIDATx] lU.{ K'$(+1!9&DGOb@ ?cÈuXq&wāS#9r9։0:ɀ N#O1` }U=z^Lzi{&_z}_;al 0565ʨ @eS@-Cg,ɲY1 Cx)% U3lc7{.@B (ٜ̋Ę] 7ހK.Ħ'BpwSB ۛDO-ZK,IK=(** h %69{0g\! OA]$J7U IozX\! t(  W*B@ &QA@ bUpL$x\Ū !+DI!7UAB@W0 B@op+`%A*VW]$J7U IozX\! t(  W*B@ &QA@ bUpL$x\Ū !+DI!7UAB@W0 B@op+`%A*VW]$J7U IozX\! t(  W*B@ &QA@ bUpL$x\Ū !+DI!7UAB@W0 B@op+`%AN) {aAo@cG>>7#cΣٲFU&U:|p'aB?.۵S 0eM/6o v?tϐBŹWҢb(j;GRx;_%H?T'W73BQ0urlGND*a šDp_ω 1W8_Ss"h=?"zf $%’r(Aꂮ}g]_P]X X|np e:Ё2_5,_Pn5vA628Ch<ˎ 0ѽe%aaVUpx]ŃҘ)S'NS$* ;X Z 6wܻܺD o955غuc oAxP ϬY j:7M@C <&]~+k6;_fWa܇f7vO_6W%Ckܐ`=p9S0yFc<`,(?|r+fl,B9%Y b, KCZ}#Gc2:d&i|V KgbC[w8~T_r-MI/q'&IӶJ0y!Lf$< *-bݻ'ޢfo сc@/a#Ҷ)h6ɨ~XA֨ ~VL׶Q}p`ȮF(6˔9n+TScCۺi+im[m'FuuF( htl{l0~Q} 6'+6P\35g / {?5 `L2z"FƖ7Ú)\=3xp j wb ~5i@G;bF(5Zcx5=~fP1^&:P0Fw〱-7AOXjX1+~Nc6i伀HÈz3CwQ"mnBAB1‘~LLF.+7Yp}m#'&hؤcÏ)-1jI$:Qqc 5}+~ȃ!#ȄEyCEvv\i, dԄ2¼!Ӷq[i̜^d#y)'91kXVFRYK2'!f _9mIpUV}0&:++R>"<֡Ӥ)ok㏍\{|h T{6ucHJa {GN,J1 /K̜ȈR">wçε7^ ΟqI>ҋ.:|g [ֲ&EűKwGϚI `?pvXZZ0rR=Q3vd'ܸ>~d%,x>9$u^V]Uc ԉ{֔4v0lBgNcCcI<zCDMtObj$-[϶J?އT[`Ŝa`ۡ/\}ҌwުdcaR'ٓ#~N{~~eyVT >$~J7Gƛ-yڻO$yf'xaձz3 fTmzNNũ]; ]?Vc+ 4-\t%Jh }+BAA΅k|uWU:"=jz5<8U力kꅡ.衏`߮*?؏/wmGݫbbҽ n+a%ܥނ#&ݰmc~z7[IHЮϟU@矉—c \;H!`VB9KP)t{T݀Pw]AD=EpOrϽj-EC)̿u O^$q" dXAB@xRS&o-bI L&[]ly<͑=z^|EO ͛1Xpa*/|믿61*++/.TcK˘ISWog6 dS[H,T}YcllHi R%姄;w4oga &YG@9⪝߇oU'$Yx'mX 7xc1qszJ*# c`ݺu17nt-h^bt,<@8J'[;1~iطo&Cp ׃o&|7iXѳ^xRpǎ@?$iڣ1y-I@@8 3Sv]N|)O?9dSC@eƖ|sooo LLjD"I6DrZGz0|>~-1lpN^7fԑbŊD|#.]:1ǔzfLMvmPQQ>JDLlsq=?dC! Z.??믛g(zjUS8qǠ-J=@ e<KL: p®]3τO?vbc9!`" ǐɘ4ʪUރ:qiӦs=ׯO+op[?Ni7p|l~ J+\vep]w7xW1{SzQEMt?͋.'gn?>a;6Cxp$l!/ٲ'vhdtӡm|$OOYP.A;t#^΀yK!`Ms H$3L %R~26j3/ER:I0T΀)a!`ƍg|?qOtry #LqvnSp#0񙀼5='H}_}V-IA@8)0{6ϳİW iGo;<ߙte@D@ C{=-ܢ0"2mg720+#_gS!/.%)Ʈ" !̫%a0<g0U )SxŸ !`z|U!qA =BH )B@O@zVAS+#=W '[˰_zr'B$@]O{N󖀸~*pM Ng޴Jo<&~_ܟk$/1KUZ 4A v*vĒ;j# cb`ybin>e*'ö*m n.6U},Qe(wSW] z]eXB޴mP56JTAZPV˸z,b9ec)OcLFT.c}rMgӸoThs\n=SF5-bcjmǽX_]_h_϶x2<2Nszy2JJ]:GPAg!sU?6ܸv?cH|/_rI.‚m%ۋ aA3$ȲRG1ful^$*liSPuQ7l&vba!?lf56l%6*ИdO[DAy/GxNx\ۧւWcS? NTwˆϯRSlOg*"SڰU:kN:\>Ut(QGS{s @&- Tu@R n@0Xx!vK D'S?eJNmfU;'<qtr+>D a10!INŒ1B ~j㽹N}Xm d6T{POQ?Ɛ@uƎe&IT;O㡕dMb)~\dbb8KN; 8l.CX@$$)HyXYlF3/倓MZLscP7~ZdRrgBbcTCsl(]"*?@g9 c1]Nj y }|Q'i~(e_c:dq`W85N Z,dB:r<˂bHNa^cAA1{^P,1(#{Sz,tg >Wc%UwkARW{:9g vj=|x I> fmĦv*4gң>L~GaHzHd3FQ:&.ImTk.a!9AWp,BcJ f-;T^@`qt,D:xZ'pmڛAWmI^IZF`][Q΋tie'/3tx'/b/|$\f4l=T\y Kz:f[5\Rٞ*:.[#,u5.J ֆrJ&Q,c[V\y;/hNgp6@[憶X*)![#]89 h8A|Ec1?bHh;%B 9zɲL䯰uK% ^l)HBM8ďH.P.ϷcOMxʅ\&K?̝IENDB`mapproxy-1.11.0/doc/imgs/labeling-meta-buffer.png000066400000000000000000000152511320454472400216440ustar00rootroot00000000000000PNG  IHDR"iCCPICC ProfilexKK#AOZ4d /tYWha6NIl:k!+f5 ʥf7܈_!nTS)B` z:uM1qBsg/A*9AS˸^ Dҝ.ެ9ɻQ[ify]]O%cek^7r4c,@gYKV! -7CepWd4_ =eࢵ{Tk-m@Na_}g?/ֶSR_=;TFjQU!t1qy1`5mQɤGhjq3Kg|*BH`I0"4"\a"pIlT}eY|='r\vL[,ZGEXףW= 5򈾘XSˣz pHYsaa?iIDATx]{pUE)] kͬ+΄rKFVVWYdCѩYkuFr,fJj@!y@ `H;9ߡ#<_Ϲ}SWW'9ҍ@tWs.a3J7)Or^t)Э2t){ؔC7M?=ؔgʡSk[nʡx|)G;dŏ O-;ԇn̏SGB:q ãA`$={6ߺu+uO$]zv)Lrѐ=z iWu ,kgICӦM;S5;r9. 颞MӧhH!cI Οs=_%\"}r9o.:'cÆ ˥"F%ȿ[FXׯ_?=ztòm۶yyyRXX#3FDe„ uSNƍ tE&M$;w?lذAjkk#oĉҭ[@α۴i2^m:K]CvZ;w.'KF,1 ܵkW: iIf8kb;;v,]0ՎLKK~~> o0\<(((A[(܌eO G`rJI(VqC J.l>X3QFcܥhaXM9\A:b"SB Tb>}:%#R!:n M57d+'Ǐ*GW^r w;ThjU!رc`LwJ$b#=fD F,nz\!УG-^8}IC3z#<:& ?&XMٷv[=9be;sB:k¹C 4bqW_}55q1?|0eʔ9X::Ղ1c{R_|'lgk_IJ5[rl-sIJE駄@hV |4Խ@-q8ЈF[7qsه@>43'Nv,7n<#` n;b"e/&!r2Vw$,֋k 駟vggBԳf)}:Ų/\0)?\`x[kB%1N}!@AkR\}6cq%^8}F,@9O 0}hfX;w.jmeX?^fϞm[X5rHX߶WN?-{rʏ&=LV%֖rŲ:U^GE ynxn qLE [-̙K/4S~a etd=K^yM?TV.pZw\yt AN}-9#"3ǜ{.Vyҿ&YXoʛA(~*oI&t#o}R諯ݪZMsfJA鐑/By^owOV Qw,S9UAsƍA~ѓ낲TmX K=Yֿh]e]jOֱ,p zDIhBӈ#J*AoxMbcU}qWcMn\2)ZD>XZ>x aȏR/[W3gWdelNsiB Z)\f[h#qdu+2?O'yOd-ʿf}:Xz̛6,eѫL.w'>.2pUan'$œƯ_7zzK%?KxMHb< $xUѓҚ>|-X^֋M=}y)gO>gBx$IƟA^Z0eaHSı[UP+^J\7\ų(ΧtMl@EJNtjws~M3lep5{/hbX <_њ#wIz7[nŻц{L'rwȥOɟw-$/)sS~?}u\]0Cedu3s{պVB Lb;w?s=*ݽTCIo=(7{J=dI%y$YcOvnS_ϐ7N-݋è3q?iBv+U}kyOzH_W^^^}1(ǰJOgʺ31Z-HT_4-ZR%Ԭp)y( X6M:#Vڍ+Tmy2){7V^]liݜWڭB7RULm72zw50IA0Msj 9S2w^k¼-8;;#K\ќŒ&VR!9znbr3_o'n9[-z[ڬHoVqW}cʖrsه@ZEFB7}f^oThd%O?uɾ}O̘1C 8lb*Tn}'+V>44ۆbe~Ϟ=e֭ϣ'm{bQP$}P7@:޳;tCMM{W^$,]g+-#A˗/kz3*Ƈ=ݻ7^`[h!IEsرK[odO?I=кl޼zqX$'([hћzluElqGusw-&LJ#l[NئXq^_u+H20`FܿF!jcǎMXqWl[EBuVbM>]8#5qԩ@OsbWZc=&ڐ#nF! A,%C: Gb?k߿??Z-yyy 3f= /pks@:ebb؃믗m۶]wdz$ꪫ䷿p5s.sh'EҥK\., &C YݶmXz˺lw"ЧOo-TWMRi!Xm8g)zƽt:X^J0%I1xw¹yeX>{_9EQcٜ⮌l㦀Ç ^:A, tݭOo٦N7zꕂ9CqG _':h ܙ:ތNt3s.N'Zr/B8ܣ7nQFbMkaX6v8v@EE ilsIJE駄@rL<yq>?ػwN>,&_^\QUU%<\䢋.o7NHKp';CxK___Pm-OKBўZP5_<ۢ//nRޯW)s'Q9Of:u M^]Dȧ,OeahEC(GgsEkupYiIsnrgoOx;-dMaI^a*@̡7VqՍe7 +܁f2%[ԧBFvIyZ)cP@ݜIB'׀^c9 ,)ԣї{ӁA>P/p>Aq <8})}5?ϛx 4;;(O^$h«joz#]:3:f+L|)<1 Oɴ'a&@>w P7D)ԫo#gZ=tވ"!d>mR.pQϐ3I!{!,qcGD\gk.})˘7HBi>Bk% KTLH3Oyh@BF}ӅM 2Ҝ{>˲_S@xu04G#Z`.C%)ԁO@x_V˙GF:dھ>wkBmPEO&W=bv>_bPbEs8"=`r0QOGqくwM_gǁ1)O *uR =vv6o|mJF,=JOCx̐,e CRxŐi{ȕXf<_{ďwxȼkM$/IP 9^$ h?Œ&Q)yB3! YL@V쁏#&Qȃ0 ;4OO@QFQ k]z&Pgm  f @uK ]@_N)vƜE|uJ6b.GcYP~*9=C$YGN/Wb<~&+gm&4P!Z?ԙ$; %Ѭg&qh<''֥,m @|A q(uF|9^ t|d| >{ZNC$Lv$'8go@VYVF]l(Z|3#:B=*Xޛڵn&=G_ϵt/ N)Hs0=s@&A>6y@YG mз_Q`_ifǰԯNYam\1qS>P/֡ĢɎJN^hM#OꄙDuC.Ly <О5 E9\ LzƲDK6%p{Kqe[c;pXO@T HH Ƥt@cS;LM<]3VF;X6ƺ 8Lw42d+ i[bQ뇮0䤭N?H2ף ⻌VgnIENDB`mapproxy-1.11.0/doc/imgs/labeling-metatiling-buffer.png000066400000000000000000000076531320454472400230620ustar00rootroot00000000000000PNG  IHDR`iCCPICC ProfilexKK#AOZ4d /tYWha6NIl:k!+f5 ʥf7܈_!nTS)B` z:uM1qBsg/A*9AS˸^ Dҝ.ެ9ɻQ[ify]]O%cek^7r4c,@gYKV! -7CepWd4_ =eࢵ{Tk-m@Na_}g?/ֶSR_=;TFjQU!t1qy1`5mQɤGhjq3Kg|*BH`I0"4"\a"pIlT}eY|='r\vL[,ZGEXףW= 5򈾘XSˣz pHYsaa?i IDATx][UW>{f(0T&64Uy@F[SQڤb1&M0ii#*QJ[01%Ӛԇ6Z/ Bb3rggξ̷e[wgxZD@GY*zam<E.b~ⶅ6A_4p1 ͮⶂNA;U(h*T PХjNVJ]de(hjT PХjNVJ]de(hjT W`}`e{2~7#'a~%fpwڅ NDxk9ga^/7߹Y'?+3y>8Y~ GDx5T[*O؉7!)( S~!w8%0\CO wboqQosk WC#~+O#).b=4|Q1<ga`qp|3=o|om8X#mniGeDQ896Nz +$1#oUܶp=&iدaLd sTƧ,jp0Z02PgL21@A5YSH@;PC ˍ?>2hS4l1;% I `Os+$-hcs >k+c[Z-hCж8&f[:Aw-ä́q Wq4ʙ׿ծǹ{?>g[=o W}ˁ@VoTW@2 BRg"#(WqedYP]Ɉ0ٖ"'9uBp l]ܪc.x.{ȿJШU4ט611_l:QTI-NidyaBKK0+ PVh$H^  (輴€V)`^0~%+M=R.4"(cv(|fXD |^X8jr̼a|>uAʪ[oڍ["Y}Ο}+3K+ZѡTQXk@l*SSރؘ Xs2攩pۏ|f@u1K]M/3Ҭ&Wtvy/4ށŦNP_h%n:k#lkZAoGw2= ks$FT+\C(* u;/MAhΩ[?plSNdܷKsjpfpo4}Ξz\ sx m7mڔ9+V mI : l1@Ab8`E30[ 4r0@]u V~{|OAk`Jp qH+1KRŽuϓ qmцqp 'K6`\B%m?xjgl!b)1;5|GrA(w2qv3JkWr `ob-'`w*`U7`ΐ2S+_Vi ^& hm3b5B :OX Pj '(<cQ34V7Av?[ k eQb(%Xo٬<ma|KBX+h.c+fĀVqg?%M h?"4 dĀ-&ΨXl=lD!2@AgH>mS"f!,>}N!th P9-`Ubm<ƌa8&]}0Y,,(.G3;u(ѮqC.E]3Ew i Nԓg'w)Uqz]2X'Ezj&29μ M(hl+s(̛d&2gμ M[6[#,<曟AΊ Y`v-rܗ֞(RF`d[4. -vmVfl-^K ɀ5fХf.uνQsK]c ;*GAϽ6/u2m h I@_ ;As~ݨɕJfkap`wB3DlS@\!`w.^R^iir|lԕ+::.-?ї۫ae*m tb|cs$DV/\B|]tb#e/ mEOT\5ik-mysGv>(v0Uў|]Gص!5X҆Pum0&29[|jeM2 u=4$bUs0-A^+(\52@AkbUs=ȒQy0WEN+zELI0N&}cFI؟aA6Д@"\H] 2cbBnhCić@Nf۽EXp1ʬжpq> dmay|\r.hA|`.+\CpP}~N~c.^1(rU<(#a@+b!%0+ Ͱ=U 2a`rR~I LAXܲ }ۖ 9^$|w l_ ISp: 2+g՗j` ߨg |z`+~9CeDF}|\}G: c auc)p%aCJ/F}5 ӟl+1en^R JfDzE/9>;8Ò?|F`zI I{g,,׽С8>?7KEz)CR M$!삿Ք&1}]$$aoW hkGdd;ntHw P%r P"1@A0@Ag@:t["g$bjZ8 1.8LWIENDB`mapproxy-1.11.0/doc/imgs/labeling-metatiling.png000066400000000000000000000050421320454472400216010ustar00rootroot00000000000000PNG  IHDRtײiCCPICC ProfilexKK#AOZ4d /tYWha6NIl:k!+f5 ʥf7܈_!nTS)B` z:uM1qBsg/A*9AS˸^ Dҝ.ެ9ɻQ[ify]]O%cek^7r4c,@gYKV! -7CepWd4_ =eࢵ{Tk-m@Na_}g?/ֶSR_=;TFjQU!t1qy1`5mQɤGhjq3Kg|*BH`I0"4"\a"pIlT}eY|='r\vL[,ZGEXףW= 5򈾘XSˣz pHYsaa?iIDATx휿oEocs : RH@$% B"POHTHDiPb5%w(E |&Y޳oOMfo ;s6^|7{ޝ, 09CW& 05 Ld,.lz1Ƭ J1Od"+!-FcYmȘ'5@|0Q|9#D KGŗ3"nL԰N|0Q|9#ȪNwQ_DNdږ1 ,tn֖۞Xmݷ.tEŗ'1"~ʵA B^var'<ܤzV*NZ)t;g[JWyXiyO貭\]T7dׄnTZVt]V-gmz[O+/&tc-OK;ENʈn \wAW9fu{E{S|Ľ-D>bIq&ݏ)w}l/nmT-+];QzwyHXukDHbn]Xq>n~_l}H?LƃN?ATDS~:&A&:t9yRdVopoG#w>%\+91V}kwU-Npn}F{(*coud"y|z@$,f,ܳx{ʵmi}d]_EV _q{P7n}-!{Fc|XmԘyC 5` %IHu`"jDjQ;Ƙ5Vq[62/[.b%//´ʗ9IENDB`mapproxy-1.11.0/doc/imgs/labeling-no-clip.png000066400000000000000000000171251320454472400210120ustar00rootroot00000000000000PNG  IHDR8> ViCCPICC ProfilexKK#AOZ4d /tYWha6NIl:k!+f5 ʥf7܈_!nTS)B` z:uM1qBsg/A*9AS˸^ Dҝ.ެ9ɻQ[ify]]O%cek^7r4c,@gYKV! -7CepWd4_ =eࢵ{Tk-m@Na_}g?/ֶSR_=;TFjQU!t1qy1`5mQɤGhjq3Kg|*BH`I0"4"\a"pIlT}eY|='r\vL[,ZGEXףW= 5򈾘XSˣz pHYsaa?i-IDATx ؖUT}HDJIB3GLӼL)ZLM%r#\$GsKGlIî1lD->6Q6w{wߗ]?9}noFd1F";b\'#`0#вҲ5sŌ@"Э[7=oEwi@↡L%גJ*[[ )X^AW$%ܿ$ @{3s$7I7o2\BMDè+V gz e&OZq$g+8(9Jw M $ m,ϒwcpB/E$ǻh\-o AH*I'@d}rLuxOFbD 5UT:ZET,MMu@'_ĥ h tW&ڂB}wUC&ʕq(htmR^!,҉n6d)dh=+i0W˴#|DM-&21,GϧCµN[5S$0EѨg$o@V@Z?#a6"׺X[ } ^H~䙫qLp|"(H1<H0*Lk?5KSÎ\)h+OF%V',Y糉R>X5-ɛ4p`_s!,:-Wr(Oө\\a&TXb؈8 n$] 苺mT"KZ sJϩwğߋ[?)'> Eq qJQLBL2eOSK6I+nN#0lN#f:|(ٌ7j4!$.ӵ4cEpICt` Ek=` 0J L R~Mc%|-D~2@}`5W 4I$=Դm/õFyLNۦ1PQ7΢4偎ՂF'Lдb -hM"X @jj5c- PnyOSɫ y.vo {hđDi3^,l  -|=2tOq Q9,̐AFm8C; +&6E9:b .]i0gK jKi4o&$cyFdt5rMZQm&')Mn`19t>nwq95 ٓ m%]x ݈vF9F#Fl d#hzG޶%8E;ADL#5m1͆| Qo;!pÊLo,-FdqCϕ]Eno58@Tom&*ҍ"? t5dp#8 *}[UȿݲH~*Gg3:["8ю:E 0Al 3JɄF#o1g}*\>`#DǏ R.O`1yG_F6WsiMuE'On͘1Vn@bASK͝PLZ/_s9AM4)]B,FfthHZАSRAXpabZ:qh̘1f ZmY^?V0Y }ѽM>=9R7#`HymLq]'8m&̞=;ZbEtWF-#` Hm 6 Nնങ0eʔ_Ol&ܪ](Hm!9܁^+/w7-=ѹ瞛8N̈́=+0$0\Uܦ${, wO:FoOg@D'ArU㡪eV]s=ќ9siӦErHuFF~Z%7 i3k,X]uU^{U-L05@Q>}tQdSSW_}59swJ{qH+mB F?|o&T8?#Pg 0 }44-Z(q dܸqرc+Yoe@!ɽ9g@ry5AG͋N 6j -~@ޓq-! ;lْe®6ě#Pbn˚Ѧm޼9#[vshY zSц!8*2|[9ӼPN^#@r? G/tV$ia/Jo2F7*ɝYjNpqMV#Z0wtx[@_F?J%#D>u3 7y3 L0ꔳck+m>;ŋS}I0ѴP%)\&)0-Wb~{@TG )"t$b@hWddv!YBpI%ӷ<"s34 AG) ѫC)qrK"5L>H!)M8MQ7TERGBTk#{vI倶D@ۛy$^J^m6H&2d_q@ ?yR 4uXmuU2A>kquvܩi2۔kE+)G#;upr Ibޝ-ۣp/'b $'UM&+Ү)~e'K&hAN( F = <@cr[$$xF?uxX4pm`7}k}/RS[FRx+H><$kіN^UKѽjm3F]6:\2sÕo5F.,ݙ}8J#0҈^i|T#.p]&ԋ wa78K&6F4""#8u8OS@~ٞ.#d!F4 Ԥ@A)jˮ0̈́g'ċ 2uze^?m1F4p աߏpoF4  +z N Xh~ ř%>dk獆os/CnJ#8-Jkp 4:'nDV}7F4-6xkMܶL䍆8Z#Јhr?8\:"6Fi`6cY6eBaF4 g`| 7 %gaFz!/Iٽџg!'۸qg n5 ^ OeHNK퍆Z3F Ip䦏g\Vha@#"m v Eg .B7FabWPMMS !aW2b#PON^W[yat#`,}oa?BKpɌB3u:#`@%z7Yη ԉ@c )eV8:TۧK1l ! ASQ-YT^ؾ/9|؜v)Xe y :G!80E%y^_'O=W/,hulG%ab *t|W,Noj$,E]h;_ xܿz>iE#f0r9?ZJA* II ёyL ;M|}lQDCKɜ5;R|0EdbA^z> yߏ{ z;p'.o:`_ބ]J-ӼR }FfQ[y+i-GLF1b=;>{ ?JJ=t/m1!b iG/[D08:ѣfβcǎN' 'wʕiӦW^` tu.us}ղ߇R9E⹨Ԍ*.l~?wxnK-|4QUp܎z50`@B߸qc QK.׽{h1_ @={g@=NF?ލ^ /ع#J";qY9 7BIitu0ټys )oߞqC kVH? 1ЉW[B2 v3Ng,\a"CɟIEtCG;"֫IW oh dS,@Z|CyLMFa(k<\UP|/ h(^v7q}P?:k$hi7i@ R][iIENDB`mapproxy-1.11.0/doc/imgs/labeling-no-placement.png000066400000000000000000000117441320454472400220340ustar00rootroot00000000000000PNG  IHDRiCCPICC ProfilexKK#AOZ4d /tYWha6NIl:k!+f5 ʥf7܈_!nTS)B` z:uM1qBsg/A*9AS˸^ Dҝ.ެ9ɻQ[ify]]O%cek^7r4c,@gYKV! -7CepWd4_ =eࢵ{Tk-m@Na_}g?/ֶSR_=;TFjQU!t1qy1`5mQɤGhjq3Kg|*BH`I0"4"\a"pIlT}eY|='r\vL[,ZGEXףW= 5򈾘XSˣz pHYsaa?iIDATx] lU~ -?6 Cĥ2K6fAb5 i5#K1L-,*YIYmèr<}_|>ys-yDB4)@niuVE@  ;@4EDt$<U"#xsw5P 4'L~KYYYZ?D fYt)*J (D!)H`S2ܬ"Q-'ЧNҪAق O'P:Ie *@[R?D@$-maJtjP )ITNҪAق O'P:Ie *@[R?D@$-maJtjP )ITNҪAق O'P:Ie *@[R?D@$-maJtjP )ITNҪAق O'P:Ie *@[R?D@$-maJtjP )ITNҪAق O'P:Ie *@[R?D@$-maJtjP )ITNҪAق O'P:Ie *@[R?D@$-maJtjP )ITNҪAق 0Lu˙&9,m9wFa`}˙>.w^Mt0ؙ;//9ƶ]YC7ʺ~#YHG'fxZrqYC&N P (KA_xh"0B5M>8||ȘˤSd^blktJӱ&9vAƔL9R#k~[0ޘb2oapH7:Ԍa`֘//93v4=%wg洢VA:d&ݻ=^#񆧥!jyrᅟ-?}&T<,<(eDu;w a{R4lۦ%۞ZnASȿb.NTBקjFjV+d[Fy:9!2د⫨g[3b)Uū%^o|HV>} 0R苯Zj %>c4EQY- DBhHÇQ37na -`Owmx mqߎMlMjÉCǺ<\y7eN|?/_|.o7`^doz9)chihYw{_ M؈i QwN_WrTgJ9y˜%bG< &_\ >) %;`7eZ>j搣r8k֬L MG@CΝ+|2D.X ܕT|ad,sHTuuL2EF]d}V^D ӺsH㽗c]gqGY.pBydɞgBx"پ}5 w?a4'Qbnfgv11c̙3Gxz5X*cr= vF~`9F@oAsL#T}8Dsy 0}n ]0:w@ GN8(n 3`H3_O6U9cYG(2_T+],A7եPFvx^B@ ΧV  K#Ph#{Yh)qԞbUۏ ~5_CXL^&כ35k[rKY] Gt@O-F<r=qսtf54.%@FsFۅ@r]UG@ϡF`1*@SG@h?-&O]j#N"}{xTqA겕Sšmd/5P&a7~6~9ɓ ç Ǎs3v7 9O絭>WfaVa@ ~p]42qG7Iq:DLel%-8> D ?@gd-`{`%ض?&yjح~:s%0vcm6þS8&㨃Y87qÿLXA;&FsP upY֚u ._wL>DwXIPny7[~yX_7#(sLs(T|>{f)Ї-c*בss!Loε8eIv晰|?gX23n\SP&׆R߄S8ELKso/;hL}7| 6ET;sW o9HHY0F2a\Q؟>I7X:W*ӶǜﷵېV¼yw<{ޝі݌ nK~a?^b@ܹxm 9Xvr[8%w-0Ar[D m2 64eCdi qm"wCUdݏƲ }X:)±0s%0|G_]ly+R1Kr}@ :xBn wumVpDž\$ۆl ApW0V@nvF`D^?/j>HXHeU 2䜇mܬAQP^ 8/,ӟcs^/=RWA, Ff)C?Xǚ` Ʋy9ꍟh3.7Q_ Xd(sc`Nju͜zp6b<ꜚ4#s-Lydy;P̚r `\= h=uQ6H$ܻB\yxO1Q ޙ7Ku^|mM 'xTr '*ӆzjepOF\з/?F|E7S}7 &$9OxFo)g0'`ԜxaVÞn݌m\([Ye( ;""d=XAqWSxl0"dQ ?#7FA\-|B QD^&`)'8s-\@˕:"`zM=UIENDB`mapproxy-1.11.0/doc/imgs/labeling-partial-false.png000066400000000000000000000123111320454472400221650ustar00rootroot00000000000000PNG  IHDRriCCPICC ProfilexKK#AOZ4d /tYWha6NIl:k!+f5 ʥf7܈_!nTS)B` z:uM1qBsg/A*9AS˸^ Dҝ.ެ9ɻQ[ify]]O%cek^7r4c,@gYKV! -7CepWd4_ =eࢵ{Tk-m@Na_}g?/ֶSR_=;TFjQU!t1qy1`5mQɤGhjq3Kg|*BH`I0"4"\a"pIlT}eY|='r\vL[,ZGEXףW= 5򈾘XSˣz pHYsaa?iIDATxym0ؐQ̏lHW[]*[XԥP$MX\ EU@H 8B ԈHb1lj  }9殹w9s3yܙ3yIa@862b Ԇ)L< H]`5.grfD.l86 4c`~ydj\f,g0,Ƽ팖!F2sB=h@f ĘsB=h@f ĘsB=h@f ĘsB=h@f ĘsB=h@f ĘsB=h@f ĘsB=h@f ĘsB=h@fr12Yrh/ۍ1Qׇ`rG5h;$ /n.?lr0imX6AKdcS޺.%}Yc?P/-tg`6ò{I~BQur r 1/7?|R iwFpb#WU`yG`xŤg7&[џ~ &2l%+#?\ۤ=0MQ~ I`?\/y 惯M炍4N1Ak5>sjXObd4'Vbĺ8~"Y9h`> gYϰggǟ6RY׃gSn߯7+W*_ ]A43P$x:sX%QGz)Vs:Gp#_:G.J)s/}7 Ro+f8U-ˁ3_IqFen!; %3H;{.1GFpx̡+T/|@a; y >w3Ǒ\G]oy8@;JASO~)Ngkz]_n-?ʢ+#}q ȯ z]5w&HHtt"bp:lɳG[G |9fOuW3O#|PԸ8o[-8h߰"+>xiSB=}:?aGI|7pL㈸h]N?/{'o;b)$w\m#O)}l1'D^ c~  Վ˖GGBxƈlICz(\мbHJܼDFe:Eaŋ3l|pDi녬/{y O++PJ?ʺ"xzQ}`5(^u!v7}(/|k/Nu'qB nGU)+l3@*F?(2 z!^krkUV0YB7(Ȋe Y~'\t~px~V'v, V'v̓=0օˏxT>#FcHȀ"ؼ?|5{7͹7x):h!D20>)bb'.?hw2oO HE"h"> RtB2 ]nq48Q>Hډ kq@{r'8cZ[ vAS`` BL ΀9l34?#p') /ǁ?Xub~4w/`VmC~0bρ?N{3} -"NppZ_geuC?x 4;MpE>6 hOt_'jq>8 /(_g͢/ w2L(_gN^ 7UxaiE`S˦(Â5?kԻ Nb-Rv,fH p*&6{ 1Ub#_×56`aXw$bq 0hJvJTWi?; ]Jw뭏 fޮJ)!nݹm(FP0[ZA^xҗ3xp`6[o 0KZv!Ɩu})[qCOAإ%xp5(c \ 3Y@St mXQFVbȣGz(4Ivh3T0^y?ue r5K+`y\ =laUXգ |:;S^KH9,kI_B0Ro} 82j'w؆#ilc3:~̟7aC)n;}a{Ƹ`X߯rEqy¡-#Ѹ끾XH QmS*^&Qx0ruˆj T}^BC 82p?Xq a10wkfmy\Eq 0ƭsJfo͉XidO܍X-QoG[Qc`` 4*@ ۦ0T@*&#N ۦ0T-E`f' UĊ0@hV|"QFSˍec(/?Ji?N_WNg';ݏ>=i'Z/S>.9 g7Ǒ> 苯ZtC[Kl}pTFۑX( VIx %#Ai>^%~\=ݓ`.Aй []KޏT箷 `̰0Loς9Ư/vQ߰ъ"\F8_>vߌ!jC7Q% Ү*Ж)4@;DZnߐ l$V' m_,} 8ƾ\ Qv9x̾R'7kĊ1}!zZNq }| Md2yΤ1e)I{49/'Xh<)Ű-ªa@~Ϸ/zdJ;ɹr4 <9(1L\ u/} k(,&|S(Y6 \Qc wë`_%ߗ<2i}1Ҳ~xkĚg>P ǃ@W>pi r+t]7t;䋋P OQhQ؅?1M Mt4"NayCwF# п q.{Z j@c6 kѐch6 kѐch6 kѐch6 kѐch6 4{61^;5,N)E  A:'s~9I2s}gڳ{{f⪪H 4 ͚o_A47'XeY; וUjfv Bg5n5^d edd7|CFBӕ ~.93PNKj?~|+ox܍WQQ@4*y.J֭[SRR ~i馛hذaԬLpBx!%*++D1Ɵ4cǎɓ'}jyZj%Mbb"ui~:5o^I6oLs?;v,}"2NKKrtu[a S'A0 )ݟP]||1re\1.\s=gRݻSQQ=zx 55T"scnҥ>#?1۷/}'ԿÎ'[oE6l^z)FIHHHPwJq?B_)//zk49άՔ>j ҲeW_&f6 /$ȡC%8nvj׮]SfחtfE4GEwj*zWhQ GlF( L  "^vea'̚۷Z# Gѳ>Ko&a]N\!A~gzS[o@T:ҍ9Ru(ltMXN$t^pƍ裏+GLAO6/ ??ڶmnӧd t^qtFxڴi|vw|Ν;i޼y:hjaw}wի:]Ct2[njX5)t҅|I^XܭS" ̤k߾ZÜ4i:]Cwz1ۋ.(Xrq Ůb5UcItT[lٲf͚ #]N#[b wXZ`A6'={T;l^`=n8–HIgnFGyIGW\G}a+-6O0A 6R:+C뺅tf wU2%%ŜTPof={6ZB\"Ǡ X/{u#mNnFX ¾ڈ16mu^o%(6Xzq~70W{U|Ӧ' >*![u߳gOo kwlVqn_}JX'ݻw #yδQ_,n/Zv ܀]H\MatG\ky*B@w\4̂KK5 bK/TXD9+ <8+kpN2E}oOq@#+!Axj[JJJh̙a) hC ik`S EVׯWo#يIp-ZP@#u9[Q%.e%]p V7f`| Ozu 1@ׁzߩw,YJ#03xiU]m6:uzq.>%N|ڵoowE'N_/gmE ih vއw"s#&"_<χ;xw8A@8yzoj\as!>u-ʇE@<߄7ᅫݺu3U/^A@ |C$ ()8A@ ŠA(W++**RU|'TC$^p*;w&p8 I@3N> Nۿk;mںs[(hڦAw Zkg*l'W:=PөOrZ TBħivVu͌Twyg@b3dWPڀ 40dMO{57fA-)b@pRf_|{o\rI_k1-妽Vg-Y@sKo.8 _Ps2^w#`Np韱ùtaihx<^{PS]id:OF+Qپ_^*m!:Ί&:yPj_󐷬dJViTJ.<1(C5j ¶EѠA0bNI房1@b9-يR:x(##X읕AiItϪ cTS^2R(%E4G<%I6_h;y߮{_ FѣzU9~!?aa5tΡq=Ρ:@kqGEw?Ch$ho=;a meΟbrgJu77lb~ E.0}nVҚR"L}έC9x#]ٶ+uoXӰY1(~59CW\БA:ЁS$f;fM\C" F?ѫXLbЙDe|OkgQ2k̝iSh Flvlq/#NڪI'kCJI 5LJ ?)ל'SX# KzxIKH4}TW-U Ν˶/ϡk[K42A3_L[e'b=UKa9x.w2] VtVtu ݸcۗRJ(.NzѦrz{=|ȐКCq# /|غu7̥^Mm+CX_UH쑹`.2x+cFePa#)y~N\Ο_}C 5M\9[#ED2gX!}?g(˃4;Xp=aDuvf_0`'*xQݲVcta?H. Jk4&.I3Ihֲ_ ,͚5y XsO2!9ꄒBJkEXoFkKjTdIJTz y:=?%4ΈDKwU߈C~:t7q׋t&%х&gI,m > 3m.͝6:fkiuNpgj))tyI\UFu)B18x1+ fp=nbN]?fsJJ;܌qV Q4Pp3ͻCZLtyEbPls (؅.$AB@ IA.v!)ZMv!  IGhRD !]HJ=@"]BR, B@hR `!Ф `B@z -&Eڅ#X@@h4)"؅.$AB@ IA.v!)ZMv!  IGhRD !]HJ=@"]BR, B@hR `!Ф `B@z -&Eڅ#X@@h4)"؅.$AB@ IA._RRBv/4~mZ~!qL~qUUUJttH8taBF_8p9.O/ #Dǩ) C9I(3lh'Dr&l*-rB@KT>gSiXj! >J\EU!`TZ"I@^PsQ*#3D tX8&Fcև4s6,nj:*+NX8G)2~.,y[:2CXЉu@Ќ[% cx 3N`[,_s=|tJC=~8oCt­6e!A:ֆ&DZ,[X`=X"~GvX Ͳ%Yn7 {shߟEg-6simL`ϒ19nHip\EoS`DםI<]Fǽɞ^\g,gn7<;a˵,pgT">bV#|0ݡ7o >q`YŢ];ƲIG8 ˷ ]:3 89S6#8}Nf?lmm,f8rNhzxte-I9+ kBP(')_EץFGqty#mynŦ]) fn;:&PXDt/qi2uήԈGǩNR#)965쇎F $:ecCVYG=ʨ "n aX̵q$Fn <^`~q#qCͤ^±?q'ΏZ7XRux4kRc;C[ g`x#&FR;٦C `D п3AkKQ &1qhz$$@c 1i޴fp%&R<#dR7a5A_LF:hY8}|s#aAXDW3 >CDcaM޸8`pmgi8B1$FسJ)VÂ3Ϝhu:{O #;\~q;s: ~G&aѝ80m4OPuS(,:5aE*a(8#]ؔ'(.7m :`n[$GUI;>?X83{Kc3qpS:I2س _~WF,fYɂ)h:I5".m, <_aC)gY cW&wncTcaJu*@GJ_{pMc^j"C{d}IENDB`mapproxy-1.11.0/doc/imgs/labeling.graffle000066400000000000000000005060251320454472400202770ustar00rootroot00000000000000 ActiveLayerIndex 0 ApplicationVersion com.omnigroup.OmniGraffle 138.17.0.133677 AutoAdjust BackgroundGraphic Bounds {{0, 0}, {559, 783}} Class SolidGraphic FontInfo Font GeezaPro Size 12 ID 2 Style shadow Draws NO stroke Draws NO CanvasOrigin {0, 0} ColumnAlign 1 ColumnSpacing 36 CreationDate 2011-02-15 10:27:28 +0100 Creator olt DisplayScale 1.000 cm = 1.000 cm GraphDocumentVersion 6 GraphicsList Bounds {{316.261, 358.353}, {66.3628, 67.419}} Class ShapedGraphic ID 133 Line ID 128 Position 0.39329057931900024 RotationType 0 Magnets {1, 1} {1, -1} {-1, 1} {-1, -1} Shape Rectangle Style fill Draws NO shadow Draws NO stroke Width 2 Bounds {{334.191, 369.687}, {76.968, 46.6472}} Class ShapedGraphic ID 132 Magnets {1, 1} {1, -1} {-1, 1} {-1, -1} Shape Rectangle Style fill Draws NO shadow Draws NO stroke Color c 0.73 k 0 m 0.15 y 0.25 Width 2 Class LineGraphic ID 129 Points {300, 408.732} {425.714, 408.732} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 128 Points {300, 392.062} {425.714, 392.062} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 127 Points {300, 375.393} {425.714, 375.393} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 126 Points {300, 358.724} {425.714, 358.724} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 125 Points {300, 425.143} {425.714, 425.143} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 122 Points {414.805, 343.309} {414.805, 439.416} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 121 Points {398.526, 343.309} {398.526, 439.416} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 120 Points {382.247, 343.309} {382.247, 439.416} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 119 Points {365.969, 343.309} {365.969, 439.416} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 118 Points {349.69, 343.309} {349.69, 439.416} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 117 Points {333.411, 343.309} {333.411, 439.416} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 116 Points {316.884, 343.309} {316.884, 439.416} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Bounds {{337.843, 256.459}, {76.968, 46.6472}} Class ShapedGraphic ID 115 Magnets {1, 1} {1, -1} {-1, 1} {-1, -1} Shape Rectangle Style fill Draws NO shadow Draws NO stroke Color c 0.73 k 0 m 0.15 y 0.25 Width 2 Bounds {{386.415, 245.125}, {65.8534, 67.419}} Class ShapedGraphic ID 113 Line ID 107 Position 0.72275584936141968 RotationType 0 Magnets {1, 1} {1, -1} {-1, 1} {-1, -1} Shape Rectangle Style fill Draws NO shadow Draws NO stroke Width 2 Bounds {{320.16, 245.055}, {66.3628, 67.419}} Class ShapedGraphic ID 112 Line ID 96 Position 0.50616776943206787 RotationType 0 Magnets {1, 1} {1, -1} {-1, 1} {-1, -1} Shape Rectangle Style fill Draws NO shadow Draws NO stroke Width 2 Class LineGraphic ID 108 Points {312.5, 295.504} {460.326, 295.504} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 107 Points {312.5, 278.835} {460.326, 278.835} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 106 Points {312.5, 262.166} {460.326, 262.166} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 105 Points {312.5, 245.497} {460.326, 245.497} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 104 Points {312.5, 311.916} {460.326, 311.916} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 102 Points {451.014, 238.375} {451.014, 318.17} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 101 Points {434.735, 238.375} {434.735, 318.17} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 100 Points {418.457, 238.375} {418.457, 318.17} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 99 Points {402.178, 238.375} {402.178, 318.17} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 98 Points {385.899, 238.375} {385.899, 318.17} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 97 Points {369.62, 238.375} {369.62, 318.17} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 96 Points {353.342, 238.375} {353.342, 318.17} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 95 Points {337.063, 238.375} {337.063, 318.17} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 94 Points {320.536, 238.375} {320.536, 318.17} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Bounds {{424.53, 126.332}, {15, 6}} Class ShapedGraphic FitText YES Flow Resize FontInfo Font Helvetica Size 5 ID 92 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\fs10 \cf0 Lorem} VerticalPad 0 Wrap NO Bounds {{418.311, 162.119}, {21, 6}} Class ShapedGraphic FitText YES Flow Resize FontInfo Font Helvetica Size 5 ID 90 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\fs10 \cf0 Loremips} VerticalPad 0 Wrap NO Bounds {{325.992, 153.239}, {14, 6}} Class ShapedGraphic FitText YES Flow Resize FontInfo Font Helvetica Size 5 ID 89 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\fs10 \cf0 ipsum} VerticalPad 0 Wrap NO Bounds {{390.547, 148.007}, {28, 6}} Class ShapedGraphic FitText YES Flow Resize FontInfo Font Helvetica Size 5 ID 88 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\fs10 \cf0 Loremipsum} VerticalPad 0 Wrap NO Bounds {{331.621, 162.119}, {28, 6}} Class ShapedGraphic FitText YES Flow Resize FontInfo Font Helvetica Size 5 ID 87 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\fs10 \cf0 Loremipsum} VerticalPad 0 Wrap NO Bounds {{403.51, 103.1}, {28, 6}} Class ShapedGraphic FitText YES Flow Resize FontInfo Font Helvetica Size 5 ID 86 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\fs10 \cf0 Loremipsum} VerticalPad 0 Wrap NO Bounds {{404.948, 137.17}, {28, 6}} Class ShapedGraphic FitText YES Flow Resize FontInfo Font Helvetica Size 5 ID 85 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\fs10 \cf0 Loremipsum} VerticalPad 0 Wrap NO Bounds {{392.514, 116.914}, {28, 6}} Class ShapedGraphic FitText YES Flow Resize FontInfo Font Helvetica Size 5 ID 84 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\fs10 \cf0 Loremipsum} VerticalPad 0 Wrap NO Bounds {{334.707, 140.094}, {28, 6}} Class ShapedGraphic FitText YES Flow Resize FontInfo Font Helvetica Size 5 ID 83 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\fs10 \cf0 Loremipsum} VerticalPad 0 Wrap NO Bounds {{338.226, 103.1}, {28, 6}} Class ShapedGraphic FitText YES Flow Resize FontInfo Font Helvetica Size 5 ID 82 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\fs10 \cf0 Loremipsum} VerticalPad 0 Wrap NO Bounds {{326.325, 130.835}, {22, 6}} Class ShapedGraphic FitText YES Flow Resize FontInfo Font Helvetica Size 5 ID 81 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\fs10 \cf0 remipsum} VerticalPad 0 Wrap NO Bounds {{340.547, 118.069}, {28, 6}} Class ShapedGraphic FitText YES Flow Resize FontInfo Font Helvetica Size 5 ID 79 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\fs10 \cf0 Loremipsum} VerticalPad 0 Wrap NO Bounds {{120.443, 648.6}, {57.0152, 25.2748}} Class ShapedGraphic FontInfo Font Verdana Size 14 ID 78 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fnil\fcharset0 Verdana;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\fs28 \cf0 Park} VerticalPad 0 Wrap NO Class LineGraphic ID 77 Points {40.7444, 661.836} {98.0224, 614.029} {143.525, 618.345} {222.662, 609.712} {252.878, 643.525} {261.628, 674.806} {243.885, 702.158} {217.266, 687.189} {184.109, 705.216} {113.953, 698.837} {47.2868, 693.798} {40.6475, 661.914} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Bounds {{64.9493, 628.736}, {76.4797, 76.4797}} Class ShapedGraphic ID 72 Magnets {1, 1} {1, -1} {-1, 1} {-1, -1} Shape Rectangle Style shadow Draws NO Bounds {{63.429, 575.54}, {107, 13}} Class ShapedGraphic FitText YES Flow Resize FontInfo Font BradleyHandITCTT-Bold Size 12 ID 70 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fnil\fcharset0 BradleyHandITCTT-Bold;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\b\fs20 \cf0 request with meta buffer} VerticalPad 0 Wrap NO Bounds {{81.5356, 547.613}, {6.61024, 6.61023}} Class ShapedGraphic ID 68 Shape Circle Style fill Color b 0 g 0 r 0 Text VerticalPad 0 Bounds {{72.7386, 527.31}, {45.7994, 20.3029}} Class ShapedGraphic FontInfo Font Verdana Size 12 ID 67 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fnil\fcharset0 Verdana;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\fs24 \cf0 burg} VerticalPad 0 Wrap NO Bounds {{89.9172, 498.203}, {61.435, 61.4349}} Class ShapedGraphic ID 66 Magnets {1, 1} {1, -1} {-1, 1} {-1, -1} Shape Rectangle Style shadow Draws NO Class LineGraphic ID 65 Points {159.712, 338.489} {191.367, 351.489} {224.935, 340.288} {235.811, 378.777} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Class LineGraphic ID 64 Points {78.4173, 371.942} {108.273, 340.647} {155.169, 338.129} Style stroke HeadArrow 0 TailArrow 0 Width 0.5 Bounds {{169.056, 361.926}, {57.0152, 25.2748}} Class ShapedGraphic FontInfo Font Verdana Size 14 ID 63 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fnil\fcharset0 Verdana;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\fs28 \cf0 Park} VerticalPad 0 Wrap NO Bounds {{169.071, 413.183}, {57, 13}} Class ShapedGraphic FitText YES Flow Resize FontInfo Font BradleyHandITCTT-Bold Size 12 ID 62 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fnil\fcharset0 BradleyHandITCTT-Bold;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\b\fs20 \cf0 right request} VerticalPad 0 Wrap NO Bounds {{92.429, 413.183}, {49, 13}} Class ShapedGraphic FitText YES Flow Resize FontInfo Font BradleyHandITCTT-Bold Size 12 ID 61 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fnil\fcharset0 BradleyHandITCTT-Bold;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\b\fs20 \cf0 left request} VerticalPad 0 Wrap NO Bounds {{159.331, 331.05}, {76.4797, 76.4797}} Class ShapedGraphic ID 60 Magnets {1, 1} {1, -1} {-1, 1} {-1, -1} Shape Rectangle Style shadow Draws NO Bounds {{88.4214, 361.926}, {57.0152, 25.2748}} Class ShapedGraphic FontInfo Font Verdana Size 14 ID 58 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fnil\fcharset0 Verdana;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\fs28 \cf0 Park} VerticalPad 0 Wrap NO Bounds {{78.6892, 331.293}, {76.4797, 76.4797}} Class ShapedGraphic ID 57 Magnets {1, 1} {1, -1} {-1, 1} {-1, -1} Shape Rectangle Style shadow Draws NO Bounds {{108.273, 296.086}, {103, 13}} Class ShapedGraphic FitText YES Flow Resize FontInfo Font BradleyHandITCTT-Bold Size 12 ID 47 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fnil\fcharset0 BradleyHandITCTT-Bold;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\b\fs20 \cf0 two overlapping request} VerticalPad 0 Wrap NO Bounds {{169.071, 187.41}, {57, 13}} Class ShapedGraphic FitText YES Flow Resize FontInfo Font BradleyHandITCTT-Bold Size 12 ID 45 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fnil\fcharset0 BradleyHandITCTT-Bold;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\b\fs20 \cf0 right request} VerticalPad 0 Wrap NO Bounds {{92.429, 187.41}, {49, 13}} Class ShapedGraphic FitText YES Flow Resize FontInfo Font BradleyHandITCTT-Bold Size 12 ID 44 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fnil\fcharset0 BradleyHandITCTT-Bold;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\b\fs20 \cf0 left request} VerticalPad 0 Wrap NO Bounds {{163.006, 268.022}, {8.22901, 8.229}} Class ShapedGraphic ID 43 Shape Circle Style fill Color b 0 g 0 r 0 Text VerticalPad 0 Bounds {{167.919, 242.027}, {57.0152, 25.2748}} Class ShapedGraphic FontInfo Font Verdana Size 14 ID 42 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fnil\fcharset0 Verdana;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\fs28 \cf0 Hamburg} VerticalPad 0 Wrap NO Bounds {{159.331, 211.871}, {76.4797, 76.4797}} Class ShapedGraphic ID 41 Magnets {1, 1} {1, -1} {-1, 1} {-1, -1} Shape Rectangle Style shadow Draws NO Bounds {{144.998, 268.021}, {8.22901, 8.229}} Class ShapedGraphic ID 40 Shape Circle Style fill Color b 0 g 0 r 0 Text VerticalPad 0 Bounds {{88.2872, 241.308}, {57.0152, 25.2748}} Class ShapedGraphic FontInfo Font Verdana Size 14 ID 39 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fnil\fcharset0 Verdana;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\fs28 \cf0 Hamburg} VerticalPad 0 Wrap NO Bounds {{78.6892, 212.114}, {76.4797, 76.4797}} Class ShapedGraphic ID 38 Magnets {1, 1} {1, -1} {-1, 1} {-1, -1} Shape Rectangle Style shadow Draws NO Bounds {{159.331, 105.277}, {76.4797, 76.4797}} Class ShapedGraphic ID 37 Magnets {1, 1} {1, -1} {-1, 1} {-1, -1} Shape Rectangle Style shadow Draws NO Bounds {{144.998, 161.428}, {8.22901, 8.229}} Class ShapedGraphic ID 28 Shape Circle Style fill Color b 0 g 0 r 0 Text VerticalPad 0 Bounds {{104.474, 136.153}, {57.0152, 25.2748}} Class ShapedGraphic FontInfo Font Verdana Size 14 ID 4 Shape Rectangle Style fill Draws NO shadow Draws NO stroke Draws NO Text Pad 0 Text {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 {\fonttbl\f0\fnil\fcharset0 Verdana;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural \f0\fs28 \cf0 Hamb} VerticalPad 0 Wrap NO Bounds {{78.6892, 105.52}, {76.4797, 76.4797}} Class ShapedGraphic ID 3 Magnets {1, 1} {1, -1} {-1, 1} {-1, -1} Shape Rectangle Style shadow Draws NO Bounds {{80.1499, 488.436}, {80.9696, 80.9696}} Class ShapedGraphic ID 69 Magnets {1, 1} {1, -1} {-1, 1} {-1, -1} Shape Rectangle Style shadow Draws NO stroke Pattern 1 Bounds {{325.992, 90.4448}, {113.538, 86.7809}} Class ShapedGraphic ID 91 Magnets {1, 1} {1, -1} {-1, 1} {-1, -1} Shape Rectangle Style fill Draws NO shadow Draws NO Bounds {{309.4, 352.165}, {80.9696, 79.7955}} Class ShapedGraphic ID 134 Line ID 128 Position 0.39680808782577515 RotationType 0 Magnets {1, 1} {1, -1} {-1, 1} {-1, -1} Shape Rectangle Style fill Draws NO shadow Draws NO stroke Pattern 1 Width 2 GridInfo GuidesLocked NO GuidesVisible YES HPages 1 ImageCounter 1 KeepToScale Layers Lock NO Name Layer 1 Print YES View YES LayoutInfo Animate NO circoMinDist 18 circoSeparation 0.0 layoutEngine dot neatoSeparation 0.0 twopiSeparation 0.0 LinksVisible NO MagnetsVisible NO MasterSheets ModificationDate 2011-02-15 15:45:45 +0100 Modifier olt NotesVisible NO Orientation 2 OriginVisible NO PageBreaks YES PrintInfo NSBottomMargin float 41 NSLeftMargin float 18 NSPaperSize size {595, 842} NSRightMargin float 18 NSTopMargin float 18 PrintOnePage QuickLookPreview JVBERi0xLjMKJcTl8uXrp/Og0MTGCjUgMCBvYmoKPDwgL0xlbmd0aCA2IDAgUiAvRmls dGVyIC9GbGF0ZURlY29kZSA+PgpzdHJlYW0KeAHNmktzHLcRx+/zKXCkDhwN3sA1SlKJ Kwc7ZpUPKh8ompLpkKZEUdbXz68xAAa73CUpq0qJWOIum9voB/rfr+EH9YP6oBa+vM8q JqvuLtVP6nf18tVHrS4+Kl2+Pl6o02X2Sv4PH3yrXn5/eXdx+f7+0/n1dHfFUVaX03jR 1s45x6B8jrOPxqmLG/Xynzda/fW2SN3/aFIuL3OOKUzHP+nNvCxBt48+ONTkIp+XiLLW e6+MtrNNIddTzSrfqM+Y9h3/f1OvVeLrZzh/US//fXl9fn/1x+Wr2+vbu6uby/u7qwuF bcUjr36U46dF/fgKY1dbT+VFXIfWdsmzzTkoi6I6OM7FpAAh5jlm76I4+Efs18h//TMn /XL4IOPxnjEKlzjnklEaM7xFTApzTEt27SQkaJezLob6jIgms75OiHyLkY6v1chDqvdj XEqzs37/GAh3l5OoHtMcUtZZhUWrGGYXc4jtTRK1kHZcUGfXi5+9WWxjbWdEJxqLJHxu JAoXRQQOSmtr5px0VMG62Sw5ie//ckZ8Lsvi1dmF0k4uSp3yemqWmc+ZpBA3nRGEf9fz wgWciVNO/nH+gvcnN+X7mxd46Ow79bezVXrBgBxUNNASfUZ7FTjSBvS8UdrbORiuJeg0 x7CEkRJm7b2D0vi0m71LQaGg52I1lxU0tjidJn6YuVdOrxSMCFhn5DMbn+NtBqxBt9M3 yqoBJzW+qqfI66cbDcgcAdU12ChNz2bfBVc5+L3HebdIB4NzTVCDJ4j72VgCtPtGhzAn 58I08BFkntsZPKGB7OIl1JuuA6VapMPKN4lF1RPt9JHSNGie6HoOntCBODAezzdPNMp2 YwPfilpNsBCM2gN0MhgQmI2zPYZTDeY1ho85b+WV+DexRX3YWBuyK1IkMbrZLdsnOuzS iu1DV9SZDeFE8vBNs425yakwIxntwYwQi5kk5k3mZjxoKDAbwWX17IhRwZbasDVVbL1Y 68YJGCPcC8pAjjp580kIRp3c1dd3R4E3FW+XwMzKkwRjtG4LN5QT+0LowINCFvYBnNZw g2+Js2RfCZty2djilzBbctcabjs/S7BZbKpcBT4SbMH2swFZo3T5JdiEr2lZg42ztacG 4aZN+kZZbZsGrmNxU71gQp6NweIOuqQM4EkWz1TQpclESx5PgxdM9PPiPJVq08OQwIM3 hLWAjiKy/Vy90Lg65PDCdnbzwia/eaFr2b1gQpyDzg1w3EGjoM++bRe1TO4BTqAQHBl/ LTyxRfNzAGe0nlOkQDzgfRIJmTJHIvYmAXtPAv8fAUFSJEG4BpiVAIiaBExqqDDgujtl hYGFsnKt4ezVxVTSqCkhXkAQlQ6e6xfsdAI5VOuOAtj4ENl+ca6hgKMrhegtKES8ZP/C 15QUvnr4GvRcqaTxIr9Tdi0j2I6AYP1cDa44eqCG4OCBGqZxqh6oobyaUjWoAS+mrEoO hGrJDtvqgYYucVz1ySa+eaAruXmgBjxJp3mgUbqOK7gj7n60BSI3Z9pcn0jNKecWkWuz cqrpfQxFI9L/tq4HhK1dz8n15dt7Qv7Dp8uP9y/U2W9rvzNUEV169uXx45N0N4eOv7t6 9+sT51M+Sx53tJ1O2w6oQX0v1fnQ8fefb9XtH5d31+fv31/9/u6hIQ+Gl14LLRnQmC1p tOwRnlNIqfezyYBmL30Avt1+9WAhlctyC0FnQ5laaFfHOlpwUJvUvUJ68v3hO6oRsHco gxpNsNtvguNMkxqYEh42wSfnd//ZJDxwXmt1xHcu9pa/O/Ex33VeXLd4rvNPu84GpjhA 82icr45rcT49N861euz0rwlzgVGgO9y7o2MXvzedHLv4I6d+7c3LdP/5YLsPfpyO1D9L bcnOeHUzaWKZ/OqUdcxXkmiuqeKeVixlZS1eM8wy1yWJDZllGCFIAZoJVz7rUjYyTGXN XBXgB/xCg9+QxrJ1jNTIMSkmoVlJ1Yvow8hDC72K6aE71aaB3JjRmyxiiB5aD6YmmW3X F4d+ZViVWfyQip2dfkM6Qtr9Hfb41JyaPbU5AXzjgJ1NHfimTqeGCdVK30ZDQzu5d/9M p1/QKEvznuKs8QcNGoMxQ2XCqTLOh0QXb9jIaEOPN1CAJUuSgY9OP7JvoPokfEy3JM7j BO+mRO+dZdapBJUoLQtuhbBxkSMREpHWzt4oTX7lm5qWIq0fzt3GhY3JJr9T9q2D72GX IHue7gem7dknupjBD95l2kCCofuB0YYtjRv84BnrjfeGPqTrwSjP9I5iTdVOaH5oXGJP s7qdPW2ULr/zNS0HPzCtUG1I5Jv8SpkeWve8XsEssoziyNq9jsUWfLhDxbZ2Cerz1f2v im3YuXrz6e3bS+a21jZ01NFclNmY9ie7zL4lRjZVsgjYy/qP9eqdmaXKHMnLD5lbtS35 ajoEXDIFQWyTCgCWsqe5fiC8kEuAsONNkjSiHSs14w002noGo0nSDe1cyRR0z+QnK+mG 4SRJmgmNARonB8P0yBqItU0oqYpfp+Rk90gz7GOU8zTXGPh1kFsjNSI2uZnsyRgiCwAd hEQWyKwrVUDNZKOcJredAuuQkCXgkue0NdUCTzEsa7Y4PfMdXpQRWJmWXml2PDGQicrV M7zWBMR67EjnIeuxYwVo/1SBHP3MMwrQ0NoUATutx3CVtmANj4udVlcBbKD5OvUla0aw TVJbT7SttyVn/qtsHVgu3LY3d5ft3c3Viwk1+eX7RvrILmIlsQPctn+jMjbO1jCuMnjK VpfevNg7qCOLa0eaMWsKH9XZEV4lfYlwgo9hnwUjKKLRL3tsnD0IJzy+nS+4+7hQ7YOl uC4sIh/4oqvz0BdPXM1j3pkOXg1bsZk1IurQHy4JnB1Xp0cK7cG6n/oKdVqkTPIApT8I oOSS+3joERwpwzKqP0OddSh7PT0VuI95p6nD2npHHfpPTQn/f4kd+iF2A+Rbdk00Ised 801CR8p5pCLQqWSZ646r8w1Cx8ozDXniEwxbV0rWvjbs6aghzF8HUt6fyWoMm2sTffQ2 pN9mXXcoqX0pcnpm3cMLRi+WjVyQxyiBtLJvdRRUa6rgA6ufwEuXuAuJPn9YnnF4mcUN 84eNLtMqd5oFx+yMXC2vA6Q2forCIqvQwk4TAXslVW5NwZYVzkFu7LXMVDvsjdb4j0iX YsgoD5Rw3Ci+0Z7gFx+zF5ZiYmjBxfpV/UrjIW6xXh+RL/xOOpxI+7PxT532HH5EOM8K e+NXkjwLrfE/4j/HfBMtq+yRv9Ea/xH9xX88e5oXuqCRvZKe4C7eY9npl8VOlo1y5qkR l+8CTzYMBajTHrl94S9DocvsURqzz5MMioX2JDNrRzaWeWCmJ600mHdxtkXtKpmxOfHs Y4e50Q5LLkG3MjPVyti3w9xoK7M8XD8Y9WBOB6qimMkDJZoats21jWZeZdxpI/VBdh59 O55vFHZdGnei2DM9U+AKu26zAUOJneRpPVHCN54FkcykZTr24B7oJieTukcGS1UZO3is zRzK8wxaPmOaZuuapEw9658DPHIqKzoGAsvuArizTgV4lTQ5+idXmv+jOYJ53TEZjNyN NHAfu2l61ZDx9SC7kQbuA/lpzS/8/QGDx8jNfryQHudesws9q4vgY7ObZYGQ1DO4mYG8 dLwDdyU9w2s8VGceI0g27k4aZB/xmsl0tfxTLLn4swomMsDJ28j7jXYcIp3fevpkwnzk 77TD/OK5xj/JttXmXfmd9jS/spnKGsqmruvfac/gl41ltOBmtL/Sal4WlAvO1BfhjNk3 S0xLsUqhPIvLQR6N7uPs2QgzYUlsDKljBxKKbrD94b8e9JkQCmVuZHN0cmVhbQplbmRv YmoKNiAwIG9iagoyODY1CmVuZG9iagozIDAgb2JqCjw8IC9UeXBlIC9QYWdlIC9QYXJl bnQgNCAwIFIgL1Jlc291cmNlcyA3IDAgUiAvQ29udGVudHMgNSAwIFIgL01lZGlhQm94 IFswIDAgNTU5IDc4M10KPj4KZW5kb2JqCjcgMCBvYmoKPDwgL1Byb2NTZXQgWyAvUERG IC9UZXh0IC9JbWFnZUIgL0ltYWdlQyAvSW1hZ2VJIF0gL0NvbG9yU3BhY2UgPDwgL0Nz MiAxMyAwIFIKL0NzMyAxNyAwIFIgL0NzMSA4IDAgUiA+PiAvRm9udCA8PCAvRjEuMCAx NCAwIFIgL0YyLjAgMTUgMCBSIC9GMy4wIDE2IDAgUgo+PiAvWE9iamVjdCA8PCAvSW0x IDkgMCBSIC9JbTIgMTEgMCBSID4+ID4+CmVuZG9iago5IDAgb2JqCjw8IC9MZW5ndGgg MTAgMCBSIC9UeXBlIC9YT2JqZWN0IC9TdWJ0eXBlIC9JbWFnZSAvV2lkdGggNjIgL0hl aWdodCA2MiAvSW50ZXJwb2xhdGUKdHJ1ZSAvQ29sb3JTcGFjZSAxOCAwIFIgL0ludGVu dCAvUGVyY2VwdHVhbCAvU01hc2sgMTkgMCBSIC9CaXRzUGVyQ29tcG9uZW50CjggL0Zp bHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCngB7dABDQAAAMKg909tDjeIQGHAgAED BgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgIG/gQEtDAAB CmVuZHN0cmVhbQplbmRvYmoKMTAgMCBvYmoKNzQKZW5kb2JqCjExIDAgb2JqCjw8IC9M ZW5ndGggMTIgMCBSIC9UeXBlIC9YT2JqZWN0IC9TdWJ0eXBlIC9JbWFnZSAvV2lkdGgg NTggL0hlaWdodCA1OCAvSW50ZXJwb2xhdGUKdHJ1ZSAvQ29sb3JTcGFjZSAxOCAwIFIg L0ludGVudCAvUGVyY2VwdHVhbCAvU01hc2sgMjEgMCBSIC9CaXRzUGVyQ29tcG9uZW50 CjggL0ZpbHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCngB7dCBAAAAAMOg+VPf4ASF UGHAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgy8gQEnbAAB CmVuZHN0cmVhbQplbmRvYmoKMTIgMCBvYmoKNjgKZW5kb2JqCjIxIDAgb2JqCjw8IC9M ZW5ndGggMjIgMCBSIC9UeXBlIC9YT2JqZWN0IC9TdWJ0eXBlIC9JbWFnZSAvV2lkdGgg NTggL0hlaWdodCA1OCAvQ29sb3JTcGFjZQovRGV2aWNlR3JheSAvSW50ZXJwb2xhdGUg dHJ1ZSAvQml0c1BlckNvbXBvbmVudCA4IC9GaWx0ZXIgL0ZsYXRlRGVjb2RlID4+CnN0 cmVhbQp4Ae2W2XKiUBCGTVxAVlkUZREExQ0DbsGoGPfE0Xn/15k+B52kKuDkOjV941J+ /t19+vxNJvM/fnoHHiAeb4E+fK/gK5S9Bf6Hb9CYy2Zz+Y/I5bKIvq8cc4AVCIKMgyAK +XwuC/A9FIFIDqgiRcdBFUkSYKybWi8CMQcYw3JxsAwNMIF0QTalVRgsECRFsxwvCCIK QShxLGKxbErCKNV8gSzSDFcSJLlcUSAqsiwKwGI0TfThCrK8IJeVmqppuqapNYBBl6ZA NSVfnCtRpLmSKCuqbpgWCtPQVaUsxSiUmlRpLEmzJbFc1eqW3Wy5EE3Hquu1iiRwdJHI 51LIXJ6kGB5A3bRb7W7f87x+t+M6lgFoiaXJQi6xUlwlzQlyVbcct+f5wXA4DPxBr90E tCzyDIgmpYuSJSm2JCua6bQ9fzh5DsPweTLyvU7T0qsy5EsmpvsAQ0CCZFk1bLfvj8OX xTKKFvNwEngdx1QrIo/T/doiIAtFhpcUzWp1AZxH681ms14tZpOg7zaMqlxiId3Hxy9j 9PCYJyhWkGuG3faG4Xy12R2Oh/32dRmOn7pOXS0LHEVCoV9IKJOgULJms+tPXqLN/vh+ Or0dtqv51O+1LK0iokKTSShTrKhWqx88L9a74+l8Pp/e9utlOPTchq5I90ko04Vkl5vD +6/L5XI+HbfRbOS1G4YiQYvuaF7JaAOSl9+Xy+m4A3LQtmMSZiGpTpTtX/IA5CfNf5MV OBRc5/4N0POv98MmutV5J9uP3s5X28Pb6XR6P+5e59OgD71V0nr7cZ5OZzAKF6/b/fF4 POzW0Qydp4nOM7lD1xmSFd1ye/5ktlytt7vt5jV6mQaea8MMCSyVMkO5QnygdRj4YBLO YWyjaD6bDgcgqcGhMEVobdLc3u6KbjU7nj+ehrPZLJyOg0G3ZSHJ9LsC4wdeIpRrBqD9 J384Go+GwZPXbTUMdFXS7mcGvLaALqhUAdRxuz1vMBiAKbhwsVVFBifCHvY12wy62tjA ANXNhtNy2+226zq2icD7PgSLAdyWE6RyVTXqVsO27QaYn1YF3+SY2DYTJDNYFPKNbbOq arphGLqmVhXk1RhMNjBEgqEgjwerFsGrFaWKTV4SeGTU4JlJ/oUvAFQaowzHlwRRkmVZ gsXCcwwsBwwmnOaVxCgBsrCTOB4Fx6FldtuDiVUiFvKFBsPava5PBq1QtAPx8k10aSyJ ULTP0KYH+BbXpY32/e1nSa/xusdwvoACPS7gXX932eO/wiwow1MGDvSEgPTuCt6S+Pw0 FFPfwm441BzH3y/+v/nZHfgD0o5+9QplbmRzdHJlYW0KZW5kb2JqCjIyIDAgb2JqCjk4 NgplbmRvYmoKMTkgMCBvYmoKPDwgL0xlbmd0aCAyMCAwIFIgL1R5cGUgL1hPYmplY3Qg L1N1YnR5cGUgL0ltYWdlIC9XaWR0aCA2MiAvSGVpZ2h0IDYyIC9Db2xvclNwYWNlCi9E ZXZpY2VHcmF5IC9JbnRlcnBvbGF0ZSB0cnVlIC9CaXRzUGVyQ29tcG9uZW50IDggL0Zp bHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCngB7ZZpU+JaEIZxlCUJWxJIICwBAsi+ aBTIoKBQKIqiKAOKIiM6//8nTPeJy9Q1Ybz34y37g8aynvO+3Wfptli+4n9TgTUS/z4d wL79EbjKZxfRyfX19Q0S8EEW+tQChAXQarXaSMCHdYOs8FceWRAF0u6AoCgKf9ltuAA4 WO0fswUWUIpmGMaJwTA0LPHCr8JRWGcZp8vt9ngxPG63k6GRR3lzHGwj7KAYp9vDcryP BM+xXrcLeLvVugp/gWkGWN4niIGgJAWDAdHv47ygT0H65jj43rDaHbTTzfJ+MRiKRKKy HI1GwlJA8LEeF0M5UN3EO0iDbYR9ohSOxhJKMpVKKkpcjkgBP8iDunXDJHWUfoOjcSWd yeby+Xwuu5lKyOGgwHtdDORuIo7SdopB5ZCspLOFUqUKUSkXc5lkLBIUOI+LdthA3OjQ EmnG7SVwplDeUmv1RqNe21WrxWw6Hgn6OeLdUFyXdnp4UZKVbLG622jutyFae1pNLecB D/hYN+OAzA3qtgYFd4BvfzCiZIrbtWa70zs67h/3ugf72k4ll4qFRN7jpGxWE9rmYEA6 HEsXqrW9w15/cD4cDs9Oj7stTS1nlWjQz0LmULePia99s9ppF+sLRpVsebd5eDQYjq7H 4/HV5Vm/22psFdIgDpkb01BxNC5IIL3VaPcGF9eTm9nsdjoenfc7zR0Qh8w9Tqz6x8R1 GownMiW12ekPr6ez+fxhfnf7YzToterVfFKWfKyLMqc9vkAEjNf2e4PRZDZfPD7+XNzf jIf9A20brEuYuN1Y2+ZweiHtZK5abx+dX9/MF8unp+Xjw2xyedL5rhYhcYF103ajouMx 1el8tdE+Ho5n88enX7+el4v76WjQbaqlzXhY/BzdB/rBgObMtNffteuofTv/uXx+flou 7iaj0w7RXuEcd0yvWqXW6p1dTe8Wj8vlcjGfjS/6HU3FDTfPm9C8GEnAYdnrnl6Ob+8f FguAp1dnR+1GtZCSseaUSc3xtHBiKJYubmsHx2ejHzez+/u72+n18KS7V6vgUfV54aAb 7djLSfVLMmxZbb/bPx+NJ9PpZHw1HPTamlrEkvMek0sGd8xGwZYFIonNoqq1u/3B8HI0 Gl2cn/QOmzW4ZLIEaZuc85cbygkgni3vaK1Or38yGJz2j7rtZg0uCV5wMI5vk9Edg7LR Lnha4IrmK2qj2TrodLudTntP260WM0oUagbShmlbLORF1Z+HeDpX3tqpa9+be02tUVMr hYwiSyLvdZLr/fGKWSzEOu30cEIwEk9lC+XqtqruqOpWpZhLK3JIJA8TNgRjGp9zxuXl AY/Bo5ovFEulUrEAT3I8KhGYPOhGMGhjK6EA5/yBUDSmJNObEOmUkoB2gDD4NpNG69AD Efdw0ExCETkWh4jFotBKBP4NNjQOtN6KACeNLCBJoXA4HJKCooBtELooNGGTRgR7iO3b qvdBaMA+vyCIoij4fTxpwRTCxiXT9/8Vxwbu9rIsy3Ecy0L/J+1bhw1r9o6/Dh44PWC4 cPYgowc27xUwMf8yfMDEQ+PsQuPU8jr2rIZJ6cjwglOTHecmu92GE88nRiZiXx/Y9IkN ZzUkwfPfXOupw88/5kyC6ejKjN9Y/QNH0/f4xz+//vyqwH+pwG+INrz/CmVuZHN0cmVh bQplbmRvYmoKMjAgMCBvYmoKMTIzOAplbmRvYmoKMjMgMCBvYmoKPDwgL0xlbmd0aCAy NCAwIFIgL04gMSAvQWx0ZXJuYXRlIC9EZXZpY2VHcmF5IC9GaWx0ZXIgL0ZsYXRlRGVj b2RlID4+CnN0cmVhbQp4AYVST0gUURz+zTYShIhBhXiIdwoJlSmsrKDadnVZlW1bldKi GGffuqOzM9Ob2TXFkwRdojx1D6JjdOzQoZuXosCsS9cgqSAIPHXo+83s6iiEb3k73/v9 /X7fe0RtnabvOylBVHNDlSulp25OTYuDHylFHdROWKYV+OlicYyx67mSv7vX1mfS2LLe x7V2+/Y9tZVlYCHqLba3EPohkWYAH5mfKGWAs8Adlq/YPgE8WA6sGvAjogMPmrkw09Gc dKWyLZFT5qIoKq9iO0mu+/m5xr6LtYmD/lyPZtaOvbPqqtFM1LT3RKG8D65EGc9fVPZs NRSnDeOcSEMaKfKu1d8rTMcRkSsQSgZSNWS5n2pOnXXgdRi7XbqT4/j2EKU+yWCoibXp spkdhX0AdirL7BDwBejxsmIP54F7Yf9bUcOTwCdhP2SHedatH/YXrlPge4Q9NeDOFK7F 8dqKH14tAUP3VCNojHNNxNPXOXOkiO8x1BmY90Y5pgsxd5aqEzeAO2EfWapmCrFd+67q Je57AnfT4zvRmzkLXKAcSXKxFdkU0DwJWBR9i7BJDjw+zh5V4HeomMAcuYnczSj3HtUR G2ejUoFWeo1Xxk/jufHF+GVsGM+Afqx213t8/+njFXXXtj48+Y163DmuvZ0bVWFWcWUL 3f/HMoSP2Sc5psHToVlYa9h25A+azEywDCjEfwU+l/qSE1Xc1e7tuEUSzFA+LGwluktU binU6j2DSqwcK9gAdnCSxCxaHLhTa7o5eHfYInpt+U1XsuuG/vr2evva8h5tyqgpKBPN s0RmlLFbo+TdeNv9ZpERnzg6vue9ilrJ/klFED+FOVoq8hRV9FZQ1sRvZw5+G7Z+XD+l 5/VB/TwJPa2f0a/ooxG+DHRJz8JzUR+jSfCwaSHiEqCKgzPUTlRjjQPiKfHytFtkkf0P QBn9ZgplbmRzdHJlYW0KZW5kb2JqCjI0IDAgb2JqCjcwNAplbmRvYmoKMTMgMCBvYmoK WyAvSUNDQmFzZWQgMjMgMCBSIF0KZW5kb2JqCjI1IDAgb2JqCjw8IC9MZW5ndGggMjYg MCBSIC9OIDMgL0FsdGVybmF0ZSAvRGV2aWNlUkdCIC9GaWx0ZXIgL0ZsYXRlRGVjb2Rl ID4+CnN0cmVhbQp4AYWSS0sjQRSFT1o0ig9kFAm4qZUvdKaJWQRX0cTHaIT2AdFhNp1O GwNJbDrtayH4K2Y1iyDoyqXgZhA37tyIosFfIQhuVNpTKUJgIHqLpr66nDrVdesCTTHT cfIagELRc5dnp8Ta+i8RrKADAbShBUHTKjmThpGkpEG83FPLuB2TXg1EjdKdLg8EAoKC 3qziiOS04jnJu57jUfNbsrVpZsgOedRdXY6T/5K7sopPJacV/5O8Y2Xl3muyXszkiuQ3 cjRjlyxAk2cdWY5LjVYhxwqFLfo3hchDshacGWXef+GGcFfP/ehk/pi/NF/PDR4APWXg orWee65U6xPoay1tjIelGwLt50DzH99/TgHBYeD9wfdfz33//YRnPwKXL9a2u1PVUq2N A1+t1T2VO/D2VKNGrGpRVenA6SGwdMkSMXHGeeAB+NYPGDFgNQZtorn2qbpRBoQS08mk CEf0aGrGEHEzn0u7pmezfCpCSGAaSQ6BMCI0jyKFGRhcx2EijxzScEkebFS3fWVZs/58 9uw9vicQ33L23Vx20xOT7Exb/Cxa30dFWNejn+9XPSA13eyn8oi+mFi56uZT/RcfjoOG rAplbmRzdHJlYW0KZW5kb2JqCjI2IDAgb2JqCjQ0OQplbmRvYmoKMTggMCBvYmoKWyAv SUNDQmFzZWQgMjUgMCBSIF0KZW5kb2JqCjI3IDAgb2JqCjw8IC9MZW5ndGggMjggMCBS IC9OIDQgL0FsdGVybmF0ZSAvRGV2aWNlQ01ZSyAvRmlsdGVyIC9GbGF0ZURlY29kZSA+ PgpzdHJlYW0KeAHs2/d3XGmaH/ZCYGYzsxmbOWeCEcw5A0wAQRA5FIDK8eb03pzrVs7I iQBz7O7pMD09Mz0zG2Z3Z/Nqtdqg1VqSLUvyemXLsuViz5GO5CMf/wEWfig8dZ+Lqlt1 C6c+9X2fstn+8FSH3+8u32Cz+YPh4MWaxpu3Ojo3zPqVbZ6t0jbfZrPN7+gK+c/fuXOr VNs+7Pvh93/98w9/ZCv7sOX39/63+//13v+Pa/O67aEum61sdmm7r6t0EDbb7/19qT6O hP2lugwt1cvOV104UKoNm61uaamu+i/qg/+pvlB1vrRP/ZDNdv/vS3Vp+3+uS/v/uu7t 8JRucypps+1o7Q51eWy2X+2x2SoW/foYSvdksy2/avfag46uDR+eiw13gr4eh9v+fefX F/9f/f9i1//38mHjow2/7r79G5vty0qbLfbPPD3h0jHbbJUVc0oP6dfd/3z537peVl5R OWPmrNlz5s6b/9GChYsWL1m6bPnHK1auWr1m7Sfr1m/YuGnzlq3btu/YuWv3nr379h84 WHXo8JGjx45Xnzh56vSZs+fOX7h46fKVq9eu37h5q6b29p279+7X1T9oKB1bU3NLa1t7 R2dXt72nt8/hdLk9Xp8/EAyFIRhBMZwgKZoBLMcLoiQrqqYbZsSKxuKJZCqdyebyhWL/ wODQ8Mjo2PjE5OOp6SdPnz1/8fLV6zdv373/9LPPf/DFl199/cNvfvTtj3/y0+9+9vNf /MZv/tZv//J3fvf3fvX7f/CHf/THf/Knf/aP/vwf/8U/+cu/+uu/+ad/+8/+7n/45//i X/6P/9O/+p//9b/5t//L3/+v//Dv/rf//d//H//h//y//qPtvz/+/37+/3/8+v+PAPwV A75jwFMGJBhAMKCDAecYsAKA/0BH/oo2fkZpTyklTsk4JbbTwjmaX0Gz/57M/iWZ+o6M T1NWlDJRSm+j1DOUtJzm/h0x9hfE0I+J4mMya5FJlIy1UpHTlLaUEv4Bf/7nxNS3xNgE MRghCgiRaSbjJ0lzCSn9Pf75n+Gvv8GfjuOTBjEMEcVmMnOSjC4m5X+L/eRP8S9/iL0b w5/r+GMIH2kiCtVkYhGp/mvsd/8Y++4r7KsR7L2GP4fwySZiqJpMLyC1f4P9oz/CfvkF 9tNh7CsVewvhz5qIseNkbgGl/wsV/J4CPpOYAZGRBdov0A08Xc0xS1jm7xTjdyTtU1EZ ECSRF3288IDjjrPsEgD+Vkr9Uoy/E6wibwq87uXUek4+BoRFDPtPxcHfEopv+GyeS/Jc zMNadaxxBCgLaf6vxanf4Mdec0NZrsiyWTebvA+ihxn9I1r4S+HNL/hnL7nHGXYUsANO kLsHkoeYyHxa+ifCVz/j379gX6bZaQaMOcHAXZCtYmLzaPkv+J9/x33znP08CV7RYNoJ xu4y/QeZ5Fxa+cf8r77jfvGM/SYBPqPASyczdZcZOsik59Dqn8SYH1nMtEkndJrS6D6V vivTh0V6Icf8YVT9JqJMG1JCE0lV6FX4OxJ3SGAXsswfWPEfmtZj3YypOqGoPbJyW5Kq eGEBy/5+pPiVkZ3QUpYSx+Voj2TWivoBTp4PuN8zx7/Qh8fV/oiSQ+V0txivEaz9nDaP EX7XfP4DfWpMHTPlIUQqdomZm0JiH2fOZcRfmp99rr8eUZ/q8iQsjXQKxRt8Zi9rzWHk 3za+/Uz7YkR5p0nPIXGyUxi+wRf2svHZjPJbxm9+qv14WPlSld5C4tNOYeImP7CHTc1m 1J/mqZdZqpCi5DgVilJtEeq6Qe1T6PkC8+O8/CIjFZKiHOODFt9qctd0dp8M5vPMtznr RdrMJ3QpqgYiSoshXdXEPRI/jwM/ymafJVO5eFy0LF/EbDH0a6q6W5LmsNw3meGnyf5M LMdbaa+ZaNKjV1Rzl6jOZvmvM1PTyfF0bJiL9HuM3CMtdUmJ7RSN2UD8KvN6KvEsFX0M ImNuY7BRy19UUjvEyCwgfZn+YirxLhl9yUSmXfpYozpwUc7uEGKzgPxF6seP418lo59S kRcufapRHbkoF3cIiZms8naMHB4iI/0kniP70uSDJHk+Su3Q6TkS83pMGh4SIkWeyHK9 KbYuwZ6zwA4dzBGZV6Pm0KBuFlQ8o/SkpPtx8WxE2K5xswXwajg90J/Q81E0E7Enjfsx 7WxE2aZKs3ju5fBAf39ey2WQdLI7EbsXtc6YxlZFmcnxL4bGi/3Daq4fTuW74pm70cQp 09qiaDM44fnQ80JxSsmNQamhznjxjpU5aSQ2K+YMTnw29L5QfKVkn4ZSE53x4TtW8aSR 3ixbMzjp6dDXhcJncvZ1MPWkIz5+xxo8pec2y7FKTp54RianSHaM8A2RrUWyJkdWJ6lN EXqWzIw9ExKPeW6U8w+yrQX2VhYcT4JNEabUGn2qJyY1dkTxDcgtefFmRjiW4Dea3EwJ jEwnYxMxZiTi6Tea8/rNtHosLm8wxBkiNzJdiI5n6eGUuz/elIveSJlHY/p6Xa4U+OGp kej4AD1ccBWzj7LJ66nYkVhkna5VCsLQ1LQ1Nk4NDbuK/Y3Z3PVk6nA0tk43KgRxcOqN NfaMHHrsKI42ZgeuJ3OHreQ6LVIhSANTP4iMvSUHn/cVJh9mRq4l+g9bmU80q0KQs+8J +RUBPSXsk0T9CHF5gKzKkZ/E6Rkqk34vSC95+AlrnwB1w+BSPziYZT6JMaVWqvTf80KF puXucfH+kHixKBzI8GujXKUCUm/j4nMrNGV2jun3BrWLBWV/WlpjCRUym3yTE56lQ1OJ jrHYvcHIhbyxL6WttuQKiU++HhKeFUOPsx2j6bsDifP56L6UuSqilktC4vUk/2wk+Hig Y7RwdyBzPpfYm7RWRfRyUYy/fsE/nQpOjrWPDN3pL5zPZfYm4qtMs1yUYq/f809fBCan 20bG7vQPns/m9yZSq8xIuSTJ3xDYDwjHW6LxBXFjijg1RuwcID9OU+UGI3/DY59zjrds 43Nw/TE4Ocrs7Gc+TjHlOiP/UEU/l/veSA+fidcnhRMj/PYitzzJlmlA/iqKfhrpfW00 PNWuTSjVQ/L2grQsIZSprPRVGvk00fsq1vDEujZuVg/p2/LKsrhUpvDSl/3I+1zvq3TD dPLqeOz4YGRrTl8aV2yKIH05hrwf7H1ZaJjOXh1LHh+Mbc1FlsY0myKKXzxB3o33vhhq mCpeG8tWDyS3ZaNLY3rZh9Yr5O107/Pxhqmha6OFEwOZ7dn4sqhZpkjELwjvt0TbF/jt 98T5l8SRKWLTCLkoR9kigPgF7/mWbf0S3H4Hzr1gDj1mNgzTi7KMzWTwnymeH8ktX4i1 b4Vzz/iqSW7DELs4w9oMQHxneX5otHyu1bxRzz2VD45L6wfFRWnepnPET1Oer+Mtn1k1 r82zT/SD4+r6AWVhSrRpPP7TgufrTMtnydpX8bPT1oExY12/tiAl2zQB/8mw5+tiy6e5 2pfps9OJg2PWun5jQVL90PrxpOfLkdb3/bdf5M5NpatG4xuKkYVJzaaJ+I+feb6caH03 fOdF8fzj3OHR1MZidGFCt2nSv8LwP8SJL3FimCBUgggSZCNJVlPUEob+l4j0+6j4A0wY xngF54I410iw1SRYQjP/HI79HhL5HDUGUU3GFD8mP8TF4wS/iGL/Dir+Es6+h1NFJC6i lg81H+DaUUJeSHJ/G574bWjkLTRQgHMCkvai8XrMOoJrHxHC34Re/Wb4yZvwRB4a5uGi B8nWoYnDuPkRIf118IvfCL17FX6Rg6ZYaNQN999HM4ew6HxC/qvgd78Iff0q9Gkm/BJA Uy549D5SrMIScwnlL4O/+4vQz16GfpgKvWfCz13w43vIUBWWnkuqf8phP+HwZyyeYvES vpyAuAfII4BcBOg/ZoVvWf4p4FKAK31KcjFsHQOOMWApAH8EzB8BfZrREoxC0nIfLd1l hMM0X6LIHzDZr+nUYzoepS2cMnto/TatVtHSApr/FT3yFTUwQeUtKoNRCTsVraHMA5Q6 nxJ+l3ryBTUxTo5EyAGUzHWTqVtkbD9lzKPE3yHf/YB8MUZMG8Q4Qgx2kfmbZGofac2h pN8hv/kB+fko8UYnnsLEeCcxdJPM7SNjcyj5l+QvPie/HSF+oOKvIfxJJzF2k+jfSyZn U8rPdfSdig0pWOlsQRLeKeI3BeIATy5g6e9U7o3CDcqsKgFIBB0CuMWDgxxYwILvFP21 rA5IiiJKIUFs54UbHL+f5T4C7E/k1EspXhCiIm8GeKON066xyl4gzmO4b6WBF2IhL2QE PunnYi2sdZU1dgNlDs3/SJx8Lozm+EGeK/jYTDObuAKiuxh9Ni1+I758JjzJcpMcO+Jl +5tA9jJI7GTMWbT0Q/EHT/m3Ge4Fyz72gJEm0H+ZyexkorNo+YfCj5/wX6W59wx47gaT TWD4ElPYwcRn0sr7JDIeR+NRlIqgLgNr1PBLCrFLIufx1LsEGIuBmAUok3HqzEOVuSgz u0RmHse8jSujlhw1JdIQHZrQoPAXJG6nwM5hwZtobDhiRQwT1/Q+VXsgK+cleTsvzAbs 62h+yMyYehLT4r2KVS+Z50RtGy/PBPxLa2TQHDT0Aqpme+RUnRQ7K0S2ctpMILywngwY k7o2iiiDdjl/X0yfEeJbOGMGI76IvBswXmjaNKyMd0uD98T8aT61hbMqGelF5Kt+41NN ewXJ013S2D1x4DSf3cLGKoE8MQCn84iQQQIptD2O3rawkya+RSVnidR4kUnlGCFNB5J0 e4yujdAnDHqLQs8WmPGClMqKfErwJ/jWKF9jcid0dpPCzuTBaN5KZEw2qftiWoul1Bhy tSZtlIUZpZOUy8TTSZCIe6PRloh5y9CPq+oGSapkueHcYCxVAImsJ5pqNuM3deuYaqyX lEqWH8pOxlKjTHzQbRWazMwNPXlUia4X9UpWGMq+jKae0PEJlzX8yCje0LJHlcR60axg xaHsp9HUazr+1GlNNBpD17XCUTm9XrDKWSkzAakjMDqA9BaQhix6NYkdiuHrDWKmRGXG aWWYRgeo3jz1IENdSVCHotR6g54h0ZkxURkSkH6+J8fVp7nLcbbKYtfpoFIE6VFTHtTh gmbPKnVJ+VJMOhgR12p8hcCmRpLSQBzKR7szkbqkcSmmHTCVNapYznPJ4aI4kIXy6a50 4n4iejEa2W/qaxSlnOcTw6Ni/2A4X+xK5+4lUhei8f1GZLWilfNCYuiJ0D8Zyo12pAbu xfMXrPR+PbZaNsp4MTH0Ruh/Hso9bk+N3I33X7By+/Tkatks5yXtBUROw54JuHkEqRlA z+TQ3Sl8lUVUKrT2nCKnKc841TxM1vRTp3PU7iS1yqLKFVp9JpBTvHuMax5ibxXZUxl2 VwKsjIAymSnxn5jUXCPKowH5Zl46lRZ3xoUVJlcuscqTOD4RdY5EHg0YN3PaybS6IyZ/ bIhlIidP5/CJtHMk8ag/diNnnUwZO2Lax4ZcJvLy9BA2XnQM5xr70zeyiRPJ6I6ouVxX bYIgTU1i46OO4cGHxcL1bOZEMrHdspZruq0UR009R8en+obGGoqD1zOFE4nMDiu+XDPK BJH4HPK+hdpfwHemkQvjyOFBdFMeW5IgynSa+JzyvSHbn5N3p8gLY+ShQXJTjlwSp2wa TXzKe19z7c/YO4/B+RFwqB9szIIlMcamMsR71ftSaXsi3Z4Qzw+LVUVhQ4ZfHOVsCsDf RT0vI61PjNvj2vkhtaogb0hLi6KCTebwt2nPi0TrdOz2uHVu0KgqaOtTyiJLssk8/rbo eZFrnU7fHkucG4xW5SPrU/qiiGKTBeztqOf5YOtUsXY0e24gWZWLbUiaiyLqh9abaffz 8ZapodrRwrmBTFUuuSERXWTqNln0fgt1fQnVfwpfeQVXP0H2jKOrB7D5acJm0r5vya4v yfr3xNWXRPU0sXuMWN1PfpSmbAbt+4br+gFb/w5ceQ6qHzO7R5hVRWZBirHpjO+HStfn Ut0b8fIz4fgkv2uYX1Xg5idYmwZ8X0e6PjPqXmuXn6rHJ+RdQ9LKvDg/wdtUzvdVovPT WN1r68oT8/i4vmtIXZmT58XF71u5zk/Tda+SV6Zjx8ciuwaNlTl1Xky2qbzvy6HO98W6 l7kr06njY/HdA9aqrDE/pthUwfvlRNf74bqX/VemstWjyT0DsdWZyPyoZlPFvwhDP4fh VwicQxEORd0YVofjR0liIUP9eRB8FwYvIZCFGRZhXChThzFHCHohTf9ZQPtJUHkeljOQ xMCiExHuo/xhnF1AgT/xpb4NxJ8ErWTIpMO6E9buIUoVJs4n2T/yDn7jK0wFsvFgigzF +yDrLmIcxJR5BP8HnqmvvWOP/UOxQJEIZXvCydtw9ACqzyXE33e/+cr7fNI3FQ2M4cEB eyhXC6X2o5E5hPQr91dfeT+d8L2K+J9ggTF7aLAGyu1HYnMI+Vfun33p+Wbc+7nhe4UE pu2h0Rqofx+SnE0ov0mFPqPDozRk0jBKI900WktjBxliAaB+QVDvSXqEpE2SRknaTtG3 KfoQzSwCzM9x6R0hDROiTggwwXeSfA3JHaTYBTT7HRp7g1kDmKngehjXOgjlJinvJ8X5 NPdTJP8KzRbRlITFg1i0DTevE/peQplL8T+GR18iQwWkX0RzATTVisWv4ZE9hDaHEr+F n72Ap/LIuIAM+dFCC5q5iiV24+ZsUvoR9Olz+HUOfsbDkz5kuAUtXsEyu/DoLFL+EfSj 5/AXWfgtCz/1whPNyPAVLL8Lj8+ilC/E4LQYyggQx0NeHm7h0KsctpfF57Pk5zz5mCfT PMVylJelmlnqGkvtY+mPWOZzTpjkhCTLl6JrD+CaAHcFsHsAOxeAT9nIODDjQKeA6mKU Rka+xEi7GGEOw70HmTEmFWMSJBN10JGHtHGBVnfQ8iyaf8sMjtL9UTpP0Ok+OvGAip6n ze20OosW3tBTI/S4RQ3jVH8flXtApc5RsW2UMZMW39Cvh+nnEWoKI0d7yYF6Mn+OSm2l IjNo6TX9xTD93qReIuRUDzlaTw6cI7NbqegMWp6OBApmSNPDsAbZFbheRs5J6HYBn8uT UyaeL8U8GgGrRLdM3pfIsyK5XaDmcPRjnctpnKyyYYXtkth7IjgjgG08mMWCSU3PKJoo q0FJ7hDlu4J0khe2cPxMwE6oibQcF6RoQIy0C8YdXjvJKZtYsRJw40ohJed4MR0Qk218 7DYXOcEZm4BSCfgxeSwpDfPigF/It/HpWi5RzVobgVbJiKPys6Q0xYnjXn6olSvUsJlq NrEBmBUfWu8S0itWeOrhx1u4oRq2cJxNbwBWBSPls34zFaTiIVcs/CgC3TSQYxq6UcFn iWQ+jRlJnIzjzijeaBLXdeKYSm6QqZkCnUsCPQGIGHBY4KEBrmngiMKsl5hKnskmVC2m YJbca0oNunhNFQ7L/DqRK+fYTDyqRi00YvYYRoOmXVXUQ7L8iSBWcFw6llWsNGome/TY A9W6ohhVkvaJIFewfDo2JFv9iJm365l6NXlZjlWJ5lperWCFUjokR8ZhY7hb669Tspfl 1EExtobXy1kxFX0pRZ5CxmSXNnxf6b8kZQ+KiTW8WaKIOeQHxUAgF2zPhO8koQsxeH8E XavjMyXSHESZIubPYe1p/E4CPx8l9kWINRpZKdFGP8PkGX+WaUsxt+PMOYvZazCrVaZc ZPSiTGclb1psTQi3o8LZCL9H51YpbJkAtEKEypjelN4S12qjyllT3q1LK2WhjGe1fIrM JDzJWHPcqrHMM4a+W1NXSlKJImquSGby7mSmOZasicROG9Yu1VghKWW8UEqHiPSwKznQ FM3fiqRP6/FdqrVC1D60sk+I9KQrMfooOnDTzJ/W07uU2MeiUc6JzJQvPBawDwfrB0JX 8+HjaXhbAvk4glcoFPMYDY2h9iG0vh+7msOOp/BtceJjkywvtSbo0CjdPUjXF+krWfpY kt4ao5cbdIkizLgYHBK7+4W6PH85zR9LcFssbqnOlkmAHjOCQ3pXUb2fUy6n5KNxaYsl LtV4m8hSo/HAYLSzaN3PmpdS+tGYujkiL1U/KIUayfkHM52F5L1s/FLSOhIzN5vaElW2 iTw5MuQf6O8o5O9mMhcTiSPR6GbDXKKoNlEghyf9A6MdhaG7mcLFROZINLHZsJYoeoki wde+nuf+h9OB6xOhk8PhfUVobRZZEMfLdCrwGul5jjycQm6Mo6eGsH0FbG0GXxAjbBoV eEX1PKUePiavj5KnBqi9eWptiloQpW0qHXjB26f5hgnu2gh7sp/dm2PXJNmPIqBEEf9z 1T6lNIzL14alk0Vxb1ZYk+DnRzibzPqfWd1TkQfjxrUh/URB3ZOR18Sl+eYHpfifprsf Jx+Mxa4NWicK5p60tiauzjc+KMX/tNj9OP9gNHN1IHEiH92TjqyJ6fOND0rxPR3pnhx8 MFq8MpCpziX3pmJrouZHJbNJQs8Xvqb3/prXgbPPQlWT4a3D0LIiMieF20yq5wu46T1S +wo59wQ5NIFuHcKWFfC5ScJmUD0/IJvekTUviXPTZNUYuXWQXJaj5iYom073fMY9esPe eg7OToGqUbBlACzNgtlxUKKI/VP50Wvp1jPxzKRYNSJs6eeXZrhZMc6msvb35qNX+q2n 2tkJtWpY3lKUlqbFWdEPSrG/Szx6Fbv11Do7YVYN6VsL6tK0PMsqKYW3v8s9epm59SR5 ZjxWVcqO8sbSlDbHkm1KqTX46EWxZjp7Zix5sJQd5SPLkvqcktkU4Xd83i8C/vFgwAqH cChsh6HbKFJFYPMZ8rfd+Oc+fNyPW0ECCxHdYeI2QlZh5HyK+i0X/5mHG/WxkQCLBNmu EKiFwQEMzCPpn/eZ71z6sEczfAockLuCUg0k7kf5uQT4WW/mjTM56I5rXgvymR0B/VZY 3YdIcwjupz3Drx0D/a6C4smEfIl2f/RGyNwLq7Nx4Sf26Zd9k0XnqOQeCHpzbf7U9WB8 D2zMxsWf2N+9LIVijieCazzgGWz1Fa4F03sgaxYu/7j765e9n+Udr3nnE797vNU3eC2Q 2w3FZxLKV6j7KebLYQEBD/rxUCsBXSeRfRQ2D5BfwugTBMuiGI/iPgxvwfDrBLGfpD5i 6C/CYAoCaRhwCPCioBllruFgLwHm0eDzoDYZUpNhhYEkNyw1IeIVTNiN83Mo9tNAYjwY j4eidNh0QXojrF1GlV24NJvk3/mLY4F8LJihQklnOPYQilxEjJ2YMpsU3vomRv0j0cAg GSw4QpmGcOICHN2B6bNI8a335aj/qRWYxAPDfcFiQzh7AU7uQCMzSemt9/MR/9uI/zkW mOwNDj8I9Z+HMtvR6AxSfg7cg4zXZPw4E+hlQg0MdIFBdgBsDks8o+B+GjFoFKOxHhp7 QOMXaWInQ84F9FOSLpKMTjIoRfeQdD3FXKCYnTQzi2GmcTmPyyohQYTYTQh1hHCW5LdR 3EyancKiudK7F2aGcL0T1+7hyhlC3kKKM2huEs1m0YyEpUJYvBOz7uLmaVzbTMozKH4C Gc6ggyJaDKLZDjR5B4udwiObCW0GJUzAT9LIpICMBZCBdjR/G02fxOKbCKOSEifgN2nk hYBM+5DRNmTwNpo/iaU2EpEKShpSXHHZy4m+gBhoE4J3+PApHt7Mo7M5YkCCLAkGIuIT 0BYereWwkxy+hSNmcVS/QEYEihYoD081c9Qtlq5m6c0sPYNlirxociLFCS6WbwL8TcAf BdwGwFYCkGdNgzVIoDuB+ggoNxj5KJDWM0IFw+bYlA4SBIg7gNXImNdp/SijrmOkCobP gn4NFHAm18ekHtLxa7R1hDbX0WoFI2SYcZUZxZihHrrQQGWuUcnDVPQTWi+nxTTzTGWm UXrCTg09oIrXqOwhKrGWNkutWMwlWB7I9NkNf50WvKKGD8nwOgmdKRCWFeYiUMiAu3Tk vopcUtAqCf9EJGYIVCRCcAYR1MlOjbynkBcl6oBIrRXocp42DR5ofEDl2hXursRdELn9 AruaB2UcMHSNUTW/orbJ8l1ROi9I+3hhFceXs6yuxRg16pOtVsm8I+rneXUvr6zixHKW 09UcrWS8cqpFit8Woud4cy+nr2LlcsBr6jClDHqlYrOYreWTZ7nYHjayktXKgaAqj0l5 wiONNAv9NXzuLJfaw8ZWAr0MiHzOiaTdjqS3MeG/GQ2cMkO7dHilis6QCC4bQtLhvgTU GINvWMhJA92pYysUvLJ0DtM4nMT74vjDKHHdJE7q5A6VXCFTZQLNJlkozvZE2QcRcN0A JzSwXQHLJWDjAUgo4Zhst+R6U7qmiydUYZvMLxM5G88yCSsUjdgjRr2pX9XUakXZJknL hJJSODqeDkWT3ZF4nRG9qkWOK/o2SV0mSGUcT8cGgtFCl5mr01NX1Phx2domGst4pYwT qNh4IDrSZQ7c1/KXlfRxObFVtJbyWqkFjzhdA+6Worc25zuXDhyKhzZGoSUGWqEQ8HDQ NRBqKYRrs9D5FFwVQzZa6BIdL5dJaBBzFrGWHFabwc8l8KooscEkF2tUmUSH+xlHHjRn mFsp5mycqbLAegMsVBmbyISLkiMnNqXFW0nhbIw/GOHX69xChbWJIFQw+rL6o7R2K6Ge icoHTWm9Ji6UeZvABguJ3mzsUSp6Mx45Y+kHDHW9piyURZvABfP53my2MZW+EYuftqwD hrlO1RdKsk3gg/mh3sxAY6pwI5Y+HUkc0KPrFHOhpJYJgvOJo/Wx686458Kw70h/YHs2 uCIFzYmiZTrZ9yTQOhm8Oxa6OBQ+UoC2Z+AVSXSuhds0sm8aaZlA74ygFwbQI3lsexr/ OE7MjZAlivRO0s1j9O0h6nw/dSRLb0/SH8fo2QZjU5jeCb55hK8d5M4XuMMZdnuC/TjK zjZYmwx6xtXmEaV2QD6flw5nxO0J4WNLmK3zJaX0jFtNw5HaAfNcXj+cVrfH5Y8j0hxN sElcz1i6eThZ2x8/l7MOpcztMf1jU52tSjaJ7xktNA/la4uZs9nEoWR0eyzysaHPVpUS RdreOO6/dF1+6j7+2Ld71L9uILggB1UmUJtJNr8N3HsZvPIkWD0R2jMcXtcPL8gilXHM ZpDNr+F7z5Er00j1OLJnCF1XwBak8Zkx0qZTzS/Ju0/JS4/JY6Pk7gFyXY5akKIrLdqm Mc0v2LtP2EuT7LERsLsfrMuCBUlQaQGbCpqey3enpUsT4rFhYXeRX5flFyS4yhLMFLbp mXF3Sr88rh0bUncX5HUZaUFcrCzBTOGansbvPY5dHrOODRq7S9lRWl0QkysNsUSRpqeZ e5PpK6PJowPR3XmzlB0tiKqVxgelfOvse+F2Fbweye8LBv1toeANGNqHIXNo4pve8HMn lC+9uXqRgB9pDaA3wtg+hJhLkl/bqaelz65OSnBTfi/V4qeuheg9CD2HoL/okqd6pHSf wDsFn4dv9nHXgtxumJ2Ng887opPdkWSvyTp0j0tt8ipXAtIuSJiFs5+15ya6Mgl7iumL u53WI4952a/tDMszMf7T1tHxzqF4dz/Vm3M5Uo3u+CWftSOszcSE963PxjqmYt3jZM+Q s6/w0JW56EvsCJkzMOl9y/uxjlfRrqe4fcLRN/TQVbjoTW8PRWdg8qtg73DYaUFuAvY6 EH8DGryIhXcTyFyGeOkPDgVDkRBEhOE+CH4AIxdRbBdOzKWp51580E+YgZLKyN4wWQ+R FxBqJ07PpuinbqHoFTQfjwQ4e5CrD3PnYXY7ys4iS8McZsFtqF4d8qndAeV+SDoHidtQ YSbBTTnSOVdS8cQhb7TLb94L6mfC6lZEmknwj/sGs86i7M6HPOlOX+JuIHo6ZG6F1RmE 8Lj3cdYxLrmGg+7+Dm/2rj91OhTbAhszcHGy91XG8Ux0Pg64R9o9/Xf8uVPB5BY4MoOQ RvCeFO4UCHeQ8LSTvjtk8DQV3kojswE+ggYSWJDHwgEcasPh2wRymkS3UvhshhyG0TiC cyjuR/E2jLiNkacIcgtJzaTpwTAbhTgAcV6YbUXYWpQ9ibGbCFBJgf6gZoU0Oqy6IbkZ lm4hYjUqbMT5SootBuKRYKw0uuEKm02QfhNWj6PKBlysJLmCv2AGclQw4wwlH4VjN6DI MURfjymVJJ/3jRmBYTIw4AjmG0PpG+HEUdhah2kVpJj3PTX8U0RgvC8w2BjM3whnjsLx dZhZaiW5HoV1YKzLwXoesr7rIHCMDa9nkZksngA+CQQQEOoF4QYAXQPwUYBuAPhMlozT iMig8H9S2VWaOMwQ6xmyEtAxkhFIAJHAToIHJLhKgcMU+IQG5QyIEjJPyGFC6iLEekK8 QgqHSH4txZXTrIVbHB4J4WYnrt/H1UuEcpCU11BCOc1FsAyLpYJYsh2P3cMjF3HjAKGt JuVymjfRQYD2B7BCG5a+iyUuYLH9uLmaVMspwUAmATrmR4db0cIdNHsBS+7Ho6tJvZwS Jd1OqH0exdWieGpl31kxsFcMrRbgCh4XVS+u+N1ysFkK1YjhMwK8V0BW8VglT4oyhMmI S0KaRPSWgJ3h8d08sYojyzlKEClUoB08/Yinazj6DMvsZpmVLGNjmdKAJ8KLfZzQyPG3 WP404HcDbgVgbQBwnAFzei+rPWTVG0A+BaSdQPwY8DbAslwCYuO9bKwBRK4D4ySj7WCU 5YxUBjjAFsJsrgdkHjDJa0zsBB3ZTuvLGaWM4RkwEgZDdtBfz2Sv0akTdHw7HVlOa2WM gCfsvlhfh+W8F3FfMrxHNf9WNbRUhstFHIt5vVFfRyRwzwxe0sNHVWiLgiyVsAqRRK2w NwK3m/BdHbmkokdlbLOELxWJMp5CTNJtkG0adUelLsrUEYneJNJLBNrGMbDBuTW+VeVu K1xp2e6IyG4S2MU8sHEA1lWXprYoSq0sX5Ckw4K4kRcWc7yNYyEt6lStFiVSKxnnRe0Q r2zk5MWcaGO5sJp1KulmOVkjxs4JkUO8sYHTFrFyGcuH1QGHUmyS87fE1Fk+XsVFN7DG IlYpYwVPwd6V632QcV5NuU/EvXst/1oztECDy2Tck/d0ZX0PUv5ricDJaGhvJLzGgBeo aJlMuLOhznS4PgldjcMnLGSvga7R8I8UwiZSrjTekSDqYsTlKHHCJPfo1GqVmi/TNoF2 JkF7nL0fBZctUG2APRpYrYD5JZgJwJmU2+PS/ah0OSJW68JulV8t8/O/h5kjYbbFjHuW fsnUjmvKbkVeLYnzRcHGc454si2auBeJXTIix1Vjt6ytEpX5gmTjeUc83xbN3Y2kL+rx Y2p0t2yuEvX5vGzj+I6x7gcjvdcHHSeLrv05z6aUf0k8NCsC2zSibcz9YNh7fcB3quDf nwluSoaXROFZJmpTibaRYP1g6HoxfCoH7U/Bm+LIEgubZeA2hWodQutKwVEeO5HB9yWJ TVFicYScqVE2mW4doOuK9NUcfSJN70vQmyxmscHMLMFMYlr6hfsF/mqWP5Hi98W5jRa7 WGdnqh9g1tKv3c+rVzNKdVLeF5M2RsTFujBT+RAfNRej93PW1XSkOqHvjaobTWWxJs0s wUzkmgup+7nk1XTieNzaa5kbDX2xqs6UpFJ89OBp983pnjOTfQdHXVsHPCvzvrnpYHkM thlE3TPXjSnPmQlv1bB/a39gVS40NwVVRFGbTtQ9Cdx4HDwzFqoaCm8tQKsy8NwEWvkB ZtT9x/D1ceT0CHpgAN2aw1al8LkxotykSjC7P0leHyNPDZEHitTWLLUqSc2N0qU5nBLM 7k2w10fZ00PsgQK7NQNWJcFcC5TroASze+Py9RHp9KB0IC9uTQurEvzcCF+uf4iP7o0Z 14eN0wP6gZy6JSWviktzTbG8BDOZuzcauzEUO9Nv7c8ZW0pzRzF1nilXqGIpPnpv7xrr 64k5+2iPy+X1NAZ8l0PBXQg0i8TfdnpH7f5YX4B2hlzucKMXuhxEdkHYbIJ43Y6MdKFW D0r2YQ4X1uDBL/qJHRA5C6detrCDHazZBbAe0OsAD9zggg9sD4OZGPO8SetvU/VOBe2W e3qleqd43itsC3EzUPC0MV5siWntUaQrYu/R6xzqOY+yNShWotx0aVmxOa+2ZaHOVLc9 dr8vctZtbA0olSg/3TCRbx5VWofCHYWu7sy9vuQZd3RLQK9ExemGF/mmJ3LrZKh9uLO7 eK83e8aV2ByIVKLShLcz47PLgb5w0NkZ9tyFfGeQ4A4cnk3h4y5P2uMTff6QP9gZDN0N QWdgZBuKzabIMQeUcsGCBwl60XY/dieInw4TWxFyJkEN9zAJB8M5GZ+bafMyt/3MqRCz BWZm4MxQtxzrlYFD8rrEVo9Q6xNOBvnNMDcDZwc6o1G7xfSaHqfR4tZqvEp1QN4EiTMw rr8jZ3Vn6J6Uuy/e7LJueczjfm1TWK7E+P72kUjXIGUvunqzTc7UTXfsuD+yMaxVYGKx fTrSNUnZR529/Y8cuZvu9DFffGPIqMCkDNShw3YS6XOizkeY+wbmO0YEN5LQLAbLBN1a 2EtAPiccaERC19HwMQzeSKAzaSLjD6lBCA/BfWHkIYReh7GjKL4BJyopKuUlZR+F+une INUQoq9D9FGEXo/RFSSTcIuSV4R9gt3PPwjyV0P8YZhbh7IVpWjAaYpuA/Lqdp9WH1Cu BOVDkPgJIlQQbMyRElzJsDve5Y3W+c3LAb0qrK5FpAqCj/YN8M5iyJ3r9KTu++KXA9Gq kLkWVitwIdo7zjtHQ67BDnfhvjdzyZ88GIqugfVyXNSoDoayB6jedtp5l3ZfoL0HmOAa AM0AmIq7aMLjJ3ztpP8uGbxAhg9Q8FoanQEIFQ1SWNj3vcru4MgFAj1AYmsoopyhFBgn EcKDkK0oeQelLmDUfoJeTdJlNC2HORzi3BDXAnO3EfY8yu7H2VUEKKOAFNKwsOoKK82Q XANLZxFxDyasxPlyihWDMSwUc4asR2HzFqSfgdXdqLwCF8tJTgjk0WDWEUo3hhI3w9HT sLkb0VdgcjnJ8/5hJDjoCBYfhrI3w8nTUGw3ElmBqeWkQIkdIaHbLvTWC46rvLua927n A8s5qIzDSN4Z5N3dnLee813lAtVsaDsLLWeRCpYg2UCADXax4ToAXQVwNUC2A+xjgJcB kmBQP4N30ngdTZRUVk2T2xlqOUPZAI1TwEeBDgrUUR9UVk2B7TRYTgMbAzBSLr062knp HileJoVjpLCV4pdSnI1hUSLiIcw2wrhDaBcJtTRCtpmUllCCjeYQPO3Gk2144jYevYib R3B9M6GWBtptNA9jRTdeaMWztViypLIjuLWZMJaQShnNB82OXr37kdZzS3OcUV0HZO86 KbBAhMoELKA7ejXXI9VzS/GdkQMHpNA6EVokIOU84Vd9PUqgUQ7dlMJnROiAgKwT0EU8 XsaRPgmxS+hDEbsh4Gd44iBHrOPIBRxlY2mvSHd/GHFnrvPMaY45wDLrWGbB9zDzCkI3 LzRwwjWOP8Xy+wG3FnALvoeZh9e7OP0B+2GY6hSQ9wFpLRA/AnwZYN1cvJOL1bPRK8A8 yej7GG0No3zEiCWYudhsB5upA6krH1Rm7aPNNbQ2n5FtDN+Tam9OdN2O95yPOo5EXNsN 78daYI4C2STcnuhrijtvR90XLO8Rw79DC36shufKSJmId8e8TVF/bSR43gwd0cM7VHi5 gs6TMJtAdllQo4nUGOhZHT2sYjtkfLlEzBZJG093mkSjQd7SqbMqdVihtkv0cpGeU4IZ z3QaXKPO3dK4swp7SGa3i+zy0kjV9zDr1JWHmnJLlc/I0iFJ3CYIy3h+DseVYNahRR6q kZuKeVrSq0R1G68s46Q5nGDjuHYt2aAmbirxU1KkSjC28foyTpnNSjaWfzjYXlvsupC3 H8n27Uy51sY8Cyx/pQHZFLyhv7e24LiQcx1Ne3YlfJ9EAwvM8Awd/tAqeErB0YWM/0gy uDMW+iQCLTCQGSpmk8kH2VBNOnwuCR2OIzst9BMTW6DhlQppk6j6DHYr9SE4OhQjdkbI T3TyI5WqLMFMZOrTzK0kcy7OHLKYHSazVmc+UkDl9zCrTwm3EuK5mHAowu8w+LUa95HM VYpsKSOqS2o349rZqFplKjt0aa0qfiQJlWIpPuLqEtbNuHU2Gqky9e2atlZRPpJKU9Bi yWy3JtsujnUeG7HvHuxbn3cuTXtmxf1lFmTT8ZsTPRdH+44POff0u9fnvMtS/lmxUNn3 MLs56ro47Dk24NtdCKzPBJclwrOicLmB2VTy5nDgwkDoaDG8KwetT8HLYuisSGmFi7DJ 1I1B5Hw/ejSP7spg6xP4sigxyyTLvofZjQHyfJE6mqN2pal1cWqZRc8qLep9gBm40c+W gqOjOW5nkl0XY5dF2FmlRb3vE7PrxVJwJB/NyjsT4rqosMwUZml8mcyVYHa9qJ/PG0cz +s6Esj4qLzOlWaVFPVkowWy6vSXf3aH0dMOO3m6X877HfS7g3waFZxDY42ZHrt2ldHng Hl93n/++K3jOF94WQmZi+OSjYKY1JHWEw91QZy9814mc8WBbgvgMlBxrIFJNJN9KBjrI 9m7yTh91yk1tDtCVCD1SJyQaBa6Z97XxbV3c7V7upIvd5GcrEDB0z4g1GKBJ97aqrZ1K rV064RQ3+vkKmB28m4o+SILGuKcl2tIeqenWTzjUjT6pHOYHbg9G6/uZh3l3c7qlLVHT Ha3uMzd41XJYGLj9OFo3zjwcdjcVm9uyNV3J6r7YBq9RDouFvpaIq4N2d7u9vc1+x82g uzrs34yEZ5JY3t5r9Dkpp9vt9jZ5/Tf9weOh8CYYmUHguU6/bg8SfSGnM/zIDd3wIscC 6EYIr8DJdBumduGYnejrJR46yese8qif2hCmKjA61cLK7RzaxfXY2YY+9pqLPeID60Og EgWJJk1qU5EOtadbedArXXWKh73CuiBfgbLxxoTYEoPbo/auyIMe44pDO+RR1gXFcoSP PyyKzTmoLdPVmay3x670RQ65jU8CSjkixBpGhebhcGt/Z0eurjt1pS9+yG19EtDKEdH0 N3OBjnCwuyvccx9yXILdVahvLR6qpDDT3cN6HSG/qzPguR/yXQwHDiLhtRhcSeKG0wvc /oA32OEL3QtAF0PwAQhdg2LlBKn1IrQD87mwNg9+x0dcCBAHwuRqhCrDadXOUL3A6wCt LnDHA877wb4QWAWDMoxRumSyR/b0yS1O6bZbPOcT9gb5VRBXjrFyR5Tottw9ZnOfXuvS znqUvQFpZVgoxzipPYt3pV32ZFNvrMZpnfUYe/zayrBcjvFi2yDe2e/qzj/qTdc4Emfd 0b0+c2VYLUcFgDbDaLsD7WrEe27gjlOEexfpW0GFKhgUhHsgqK8Pdj1EPTdQ3ykssIsI rSDhChpngh4o5OsNBxqg4HU4fBKBdmLIxwRWRhG0DwoFEHsQfRBCr4XxkzCxAyWW46SN pCgPFfTR3X66PkCX4sETELMDYZZjjI1gSJcQ8AhdXqHOz18N8tUhbhvMLUNZG1EamDb8 br3To933qVf88vGgtA0SlyF8GcHijqTPlehwx+55I5d9xvGAti2sLEOkMpzD+go+Z67D lbnrSVz2RY8HIttC+jJYKcf5MGh2Mu2tTFct6DkPHIeAayPwLQGhMoCGSLuT6muhnLW0 +zztPUT7NzKhxQAuB3gIdzsIbzPhryED58hQFQltoODFNFbGEAEk1ItCjzD4Foacw9Eq At9AEosp0kZTAQjvgclGhLyFkmdRqgqjNpSmyEjaRjH+MNcDcY0QdxNmzyJsFcpuwNmF BLCV9BJU7WHlYVi5AUlnYPEAIq7DhIUEZ6NYbyDaHbIaQpHrYeM0pB2AlXWovBAXbSTn CWS6gukHoeS1UPRUOHIAMtYh6kJMtpWAJTe3S+33xa7LUs9x0bFLcK0UfPP4kK30PAnd bULvfcFxmXcf5727Of8qLjiPg8o5vI91tXKee5zvEus/zgZ3g/BKFv6IRctYoocJtjDh Owx0kYGPMehuBlvJ4PMAYQNUD4U1U/gd6sMK5nGa3E2TK2lqXglmDG0nmWYS3CbBBRIc I8EuCqykPizdlmBmx6UmQrpNiOcJ4Sgh7CT5FSQ373uYdWHGI9yoxfVzuHoEV3YS8gpC nPsBZmwnGm/E4jVY7CxmHsb0Hbj2MaHMIUUbxbVGmuvMtmtG50nDvl/r26i6Fsu+WVLI JmAtRled3nNN6zupufYrnk2yb7EUnPUBZniz5rivuq+q3pOyf78U3CSGFovQbAG18cQj 2X9PDl6RwtUitE+AN/EllWEzOcLGUY9E5K6IXhaw4wK+j8c3ccRijpzJfoDZI4G6K1CX efo4R+/l6I0svZhlZn0Ps0ccf5fjL3P8cZbby3IbAbsIsLNKMGNBI6veYdVLrHIMyHuA tIERFzHCTOZDYtbARm6zkYvAPAr0PYy6gS59A1GayQg2hruTbbqabjuV6tifsG+O9q2I uOYa3nI1ZJOx2+nOq0n7qUTvgZhzS8S90vDN1YIV38PsdqLvStx5Mubeb3k3G/6VWnCu ClXKqE0kamPey5a/OhLca4Q2a9BKFZkro+USbhOoGit8OQJVG/BeHd2sYitlfK5ElAsf YFZj4pdNolon9qrkJoVcKVFzRbrie5jVGOCSDqo1sEcBm2SwQgRzBVDxPcxu6eIlTTqu intkYaPErxD4uXxp2uoDzG5q2kVNO65quyVloyiv4EsqE8tLYRrLXRp6dGqg9WCxY2uu e1W6d2HcWWl5bUbIpmIXBzpOFbsP5nu2Zhyrk+6FMe+MSMCmf4DZpWLPybzjQNa1NeVZ FfcvtIKVRrhMQ2wycTHnPpHx7k/5NyeCq6LhRSY0Q0dLf2KTyAuZ4Il0aF8yvDkGr4og Cw2sUsVLYLOJ9IUUciKJ7ktgm6P4SpNYqJemoymbVErMmAtJqjpB7YvTpS+srTTohRpd KZcW9T4kZucTXHWc3xfjNpncSp1dqLIzpFJ2xJaWMs/Hpeq4vC8qbzLFlZqwSBEqpQ8q K7UGmx5arc1MR5u3u6ult6fW6Tjp9WwMBcsxtP9BV+RRD93a5+lwtXR7anp9J9zBTQGo AsEK993mQy/Z5HO1BZo6gzd7wtVOeIMfLYeJ3B1Yq4Pxh0hfM9rYjt3oxo85iPVeshyi MjW0eo9GH9C9j+iHrfT1TuZoL7POw5SFmeQNWb4tIXVSz0OxoVm41sEf6eE+8bBlYZC4 Zkm1Efie2fNAb2hSr7Yrh3ukT9xCWZiLX8mJt9Lw3WRPfezBI+tqm3nErn3iksvCfPzy sHizH7qTt9dlHjQmr7XGjnRHPnFpZWEx1vlQsDeHe9u6nV317p4rXsfhgGcdFKzAUau1 k+u0h7r7unuddU73ZbfvkD+wLgyVY1ikycm2uYOd3k67/35v8KIzdNALfxJEy1FCbwgx zZC/DW7rRO7a0QsO7IAHXxsgyhFKqyepRsrbTLW2U3e6qAu99H4XvcZPl8OMck8gGwTP I761lb/TyZ3v4fY52dU+tgwC8h2TqDc8jVpLi3q7XTlnl/Y5xdU+vgzipNspvC7hbog1 N1m1bea5bn2fQ13tlcogXqrtx+/nXQ2ZpkfJ2rbYuS5rX5+xyquWQYLgeIi5ml2etiZf 161Az5mQYw/kWYEGywmUt3egfd1OZ2+T23nL6z4d8O4OBVbC4XIc4zodiN3l6PM0On03 3YFTvtCuILQCQsowArQFoc5wjx1q6IWvO5GTHnSnH/84TNhQimnGQ+2EvZN4YCev95En XdQOH7U8RNsQmn7EBlu57na2vpu91suecILtXrAsCGwIoB6qgWa1q02p75Sv2qVqh7jd IywP8mUISzbEA03RrlbrfodxtVuv7lO3u+VlAbEM5oj6nL8x09mSutceu9JtVfea2936 Mr9cBvNY4KE32NwearsLdV2Ce44iji2YZykRLKcQxNPh9nW3+XvuBh0Xw+4jkHcLEliK h8tJDHb2ud3OVq/7jt93MRA4EgpuhqElKGIjcKjX73QEm12hWg903gcfDqKbwthiBLfh ZKgb7evFmhxYjQs/7yUO+cmNIXIRTNkwOtjJ9NqZR71MjZM552aqfMyGIFgEMTaMCbRL PV3Sox7xVp94ziVUefkNAX4hxNlQ4G+L9HSajd3GzV7trFOp8sgb/NKisFCGcv7WVE9H 8mF3/EaPdcZhVLn1DX51UUgqQ3k39rATa67H264SXSfInj2kYw3l+YgOljGIC2rvgLvq kZ7/m723/o/q6t6/95lMhCgx4u5GPEEiEIEYBIgSd3eZyVgymXgIwd1di1OKFClVSgUp bbHSUpz4ZDLe75Xe9+fn5w947hcsJpmcfZ2199nrfa69J8wsbimfx6r0ZNdYcOp0eE1U a0tFQ2leU3lyc+UiRs08Zp1nS4M5u0mHwyQ8dnltTW59/YqGhqimxjnNzR4MpnlLizab Tbjc0urm7Frm8rqWqAbWnEaWRzPbnMnRYnEJh1dSyc2u5i2r4UXV8eY0tHo0tZoxWrVa po1ZcXlHVmXHsuqOyFpBcL3AvbHdrLldC8aMzS8u7c+s6E+s6ouo7gmq7Xav7zJr6tRi dhB2e1HJ2szytUsr1yyoGgis6Xer7zVt7NFidBGWIJ+fnsbPjOfnhrUX+PJLHPnlhu3V 6u0NhN+Sx8tN4xXG84rDWsv8WisdW6sN2+o0+E0wZrnsklROWRynMpRb7cutdeTVG7Y2 zWhlkDZ2NqM6uaU2pqU+hNXgw25y5DAMuS0aPBZp5WY1NSYxmmOYjPlMpk9LiyOLZcjh qHO5hMfLamQnNXEWN3PmM7g+TK5DC8+AzdPgTBuzrHp+UkP74sb2ec18bwbfnsk3YPHV 2XzC5WfW9qyo71nU0D23sWt2c6c9o9OgpUOdLSDc9pU1q5dP7w0OzGns82rqtW/u0cdL t6xOwhGk9KQt6clc2J0T2F3g2l1s0VWu3Vmt0tlABC3JnbkJnQULO4sDO0vdOiosO6q1 O+rogiYYs+T24nhB6QJBRYCgyrW9xrK9Xru9Ua2dQdrZK9oq49pqwtpq/drqXdoaLdua tflMOp9F+FzsGcbyGsN4Tb6tDJdWpkVri3Yrm97GIW28FZyWWA4rlMvy5bKduRzsHGrz uPTWaWO2nNUay24NZbf5cNqcOW3m3DZtbpsKrw3GbFlLZwyrM4TV6c3qcGILzNkCbY6A zm0nrfxERt/ilr75LX3erB4nVrc5q0uL06nC7YBNjVmbtmBNRtBgjvvqfOtVxQb9ZWq9 VVRPA+lqWTyYE746P3Cg0G1VqVVfuUFvlXpPLe1fY7Z4VWH4qpKA/jLXvkqrnmqD7jr1 rkaVTgbpYC/qLQ/rrfLrqXHurrPsajDobFLvYNA6WETAje6uDe2u9+1qcOpssuhgGHQw 1QUsCsasnRfd2RzayfDtZDp1tFgIWAYCtno7h/rXmEULOCHTe2hcp3auRTtPv52nxm+l +G2E3xbF54e0t/u0tzvy+eZ8vj6fr9bGp/h8vJQZ2dY1n9/tw+9y5Heat3Xot3WotXZQ bdP7h6FbU4M2Z3huyrbdkG+0tkhzsIw2UEX6G0hPS8jm7IBNee4bCm3WlRitKdccqFLp r6V6G0k3K2RDQcD6Yrd1pVZrKgxXV2v216r0NVA9DNLFDllb5r+mwnWwymKgxqC/XrO3 UWX6+eltsfmD1f6ra10G6i36G/Wnt9EYKl0tpJNDOnjzBhr8BppcVjVb9DH1e1o08frm 9PNcImid29/i289y7mNb9LD1uzmaXVxaBw/P49Xbub08375W595Wi+5W/a5Wzc42FcH0 65jwonN6BD69HU69AvPudv3Ods2OdhUBPOr03y1pyb2Z6S05WWX5eelFRXFlpXOqKq3r 6wiTuXF5bndqAXNlcWl2WVp+RUxxVVB5nXVtI8Vo2bC0tCupojmtqjizJjW3blFhfUBZ k1U1k2pir4tr6EhsaExqLEhrTs5iROcz/UtYllVs0shds5jdnsCpX87JT+EkZXCjcrl+ xTyLSh5pbB2I7ODHCeqWCvKTBCvS2yNz+L5FfPMKPmng9y9c1bq4v25JX96K3hVp3ZFZ XT6FneYVHaShvS98I2/R+tr4tfnLBlekDkRl9vsW9JqXd5OGjt7QnbyorbVxm/ITN6xI WRedMeiXP2Be3ksaOvtykrn56TVFWbmlecvKiyKqSn1qK80b66gWZs/KHE52fnVeUU5h aWJJxYLyKu/qOov6RorZ0p1aws4or8yuzMqvXlJUG15a71XVZF7HoBjsjhW1zNT68oyG jOymhPzmsBKmZ0WLWS2bNHPbE1sYyayyNNbKTHZ8Lie0iONRzjWt4ZGmVn4Cv3k5vzSF n76SH5/DDylocy/jm1bzSVNbW2xvU2JPSVJ3enp3XFZXSH6nW2mHSZWANLa3xqxrXLKm eMXqtNRVcZn9IXm97iU9JlVdpFGA34xvSNhcvGxDasra2IzB0NwB9+J+k8oe0tjZWpzc ULqyqDwrtSpvcU3R3LpS18ZKI0YdYTG5eTn1hfmFJUUp5aWLKyvm1FS51NcaNzVSrBZO dnFdXllBYUVySfWi8trgqnrn2kajRgZhslkra2qy6/Ly6pcXNkaVNAVXMBxrWgwaWITB ZaYyqjKYudkty/JZUUWsoDK2QzVHv55LGDxGEq8yvTUns3VZbmtkYWtgaatDVZt+XRtp bmte3lWR2pW9sjMxuzPyXyNqXynQr20nzfymxNUVyQNZ6auWZvVF5PUGFnU7lHcb1HSS ZkHT0o3lSRuy0tYtyRxcmDsQWNTvUN6nX9NNmjqaqlLKalZm1GYlNOSFNxb5NpfaMitn suoIh1lXml1Skb+yqjChpiS8rty3ocqmuVavpZGwW2qLiopLS9PLy+OqqsJqanzq6qwb G/UYDMJi1eZXFxXVppbUxZQ3hFQ1edc0WzYwdZpZpIVTndNUkN+cUsSIKWWGVLTMrmZZ 1rO1m7gwZhWZnLxcTnIBd3Exd34Zb3YlD0ZU+19jVr5SkJstSMoTLCoSzC9t96pot6ht 126cNmblaX25mX1Jub3RBT3zSrq8yrssqjt1GjoIo708dW1uxpoVOYNR+avmFvd5lfVa Vvfo1HcRhqC0KSWreWUiIyuiJS+wpciVVWrCqdTi1hEes6g2K7M+L7GhMKKpJJBR7sqs nMWq1eI0Em5LUVVhRk3J0rqyhQ2VAY3VLs11s5gNmqxmwmEVllemV1YnVNeG19b71zc6 NzYZMxgzWloIm5Nf0pBW3hhf2RRWzfCrZTo3tBg1szWYHMLi5Ra2pJaw4srZYZUcvxqO Ux3XqImn/q8xy8lvSynix5byQyv4flVtTrVtho18DQYfxiwnrzulsDu2pCukrNO3ssOx psOovkPjX2OWkzOQUjAQU7xqfmmfT0WPY3W3UV2XZlMnaWnP5CSv4KQv4mbN5eV58Yps Wkt12yrpbXWkjZnBzFrekhvNKpjLLvHilNlwK/V4taqtDaS1ZWVTwbLm4ihG2RxmhWdL tQ27Vo/ToMZtJjxWWl350oaqyMaaoOY6D0aDTUuTHnYIOS2Ey0mtqVtS1xBR3xjY2Oze zLBmMHVbWHQYMw43tZKxpIa5sLYlsJ7l3si2ambrMLkqLB7htKaU8xKqeAuqeQG1rW4N rVZNrTqMNvq/xiy5rCO+omNBVYd/jcC1TmDV0K7T3E5nThuzpJK++PK+8Eq40G7X2i6r +k6dpk46jBmrPVGQvFiQHiLI8hHkOQoKTQSlGoJKmqCOtDOXtmUuasud31bgwy925JeZ 8Ctn8Gto/xqzpZz8Rdyi+bxSb16FQ2uVSWvtjLYGelszaWMltJRFsSrmsqu8OLX2nHoT buMMXjON10JaOfGMmkhm3ZyWek9Wox27eRaHocFtoXHZhMeNb2qKbG4OZjA8mS12LS3G bJY6h0NNG7PWhAZ2RCMnuInj0cy1ZXKNW6ZfuKf+NWZxdfyIhvagRr57E9+GwTdm8jVY fNq/xiy2pmthXXdgQ5dbY6dNc4cxo0O9pYNiCQinPXJVUkh/um9fpnNvrllvoW5PCb27 knTVkQ7mwt6MeT053j35jt1FZl2lul0Vqp01FIxZR8vCrry5XYWzO4vtO8tNOip1O2pU BfU0QTOM2YL2kjmCci9Bpa2gelZ7nW57gyq/meIzSTtnQVtVML/Gk19ry2+YxW/UbWtW bWNSbWwYs3BefXBro0drk20rY1YrU6e1hd7KJm1cGLNwDjOI2+LBZdlw2cY8tg4Pr2JP bx6StrZwFi+I3erOabXhtBpzW3W4rXRuGzwqjFkIUxDAErixBdbsdmNOuw6nnc5phxHF /uGc9Ul+69Lc1mRYD+boDxSorSqh+iqp3nrS3RK0dqX3mmynwTzzgUK9/lL1vgpabw3V PW3MglbneA8UOK4qMu0v0+2tVOuppnXXU13NpIMV3F88u6/UvrfcuLdKu7tWvaue1tmE ASQCTlBPhVdPlV13jXF3nXZng1pHE236eRZp5wZ21np21dt1NRh3NGl3NKsJmLTp56c3 Cf0FTR4dDNsOprGgRVvAwu/g0fhcPI9Xb/35bI92jq2AY9zO1eZz1fg8Gr8VQwH35dfa 5s7n27Tzjfht2vw2uDJa2/Tm4fTf/73Fn8r/3uLx/8dvcfl/7wmqQtf/33uC/u89Qf/3 nqj/H+8JOxniVCL0d68d9/ZjjHsvYI77JLaM++Sxxnwb2GN+PZwxvz3cMb/PeGP+v7SO +o+2vvYb4/7qM876ZvY486rXePNpz4kmUaxDvnCha+V4sDdjzH8ubzQgtnU0ILNtNLCm fTSoWzASvLdjJPh658icJ10f54x0PQ0abb8Lrc/9xjhnfMdZh3zGmaJUu+zJROfSiUVe jaMhga0jcyM6R+amdA/PK+8dnt/RPxSyf9VQ6O3VH8Nern4VNrTq/tzhnptzRjrPBo0K DgaPtm8NHG2bKrZJn8xxLBSmudeMJfhyhheG9A2FJaz5uKBw7YeI1vUfIg9sfB99Z/Ob 6HebH0d+WHd7wcc1Z0KHVu+bP9y/CXr9yE3cZJkkqrHLFpa5lI/nzmaMJM/t/hC3aMO7 2Mxtb+PYO98kHNzzeum9vX8sfbPvTuy7beei32/ZG/Fhw/rwj2u6oMWBlrjLYqmozTZ9 ku1UNNHkUT9aGST4kBux4XXqin1/pzQdfZl+6MSfGb8ev5/28tiFZa8P7I1/u2tw8btt bVEfNjUt/Li+ElqSzaYxU+utk0Rr7HOEq9yqxjr9eUPc8DWv6xP3Py+suPC09OBnv5X+ duly3h9ndme8PNGX9Oowc8mbvZWx73YULHq/NSPyw0bpMeMI8WHzJVNHbFdOHnAqGd83 mzG8a37/m81L9jztLr10n3vg28+Zv3y5u+rJ5YGqv47VZ/51PC/l76Opy17vS0x4uysW /ZOeMl4oOWUWP3XOJnXyvGPRxCXPxpErc7reXovb8exa8afXLhx83r/m7s9NvEe3yhqf Xcis/utEUuGrw0ty3uyNyXy3LTrt/UbpSeMIyUlonLFOFZ13KJj4zKNu9Hog/8ODiHU/ /JFxfNVPnd/V/fDgRv7F5+fT97w8nrj69f443tud0Y3vtyys/Lg+vOTj4HRfJMegccIm RXTWPlf4uUv5xHOPul+Hgzt3vVpxqOEJ83LuH3fOJT98eSju67c7Iz77sCHs2NDg/O3D PXPWjHQG80c6pYeNFkiOmywWn8OYfGe1TPjGOuW50Kn4k5Hgjra38buy/iw/nfT3J4fj Xr3cEfXL+w1h3w+tCv5ipNP/s7FWn0/GWbP3jjNknxjMk15HPDSYLxkxDHkhNkv4fNKp aHA0kF/xPnrzstf5B2Lfbd+x8P299fP/Hu6b88tIZ8APozzvr8dbPG9ONLhfFtbKv9J2 lT/VcpYItV3/ls0M+lJsnrBr0qkExcbN+LhgzaL3mVsXfOxfN3/48/7Akfftfn+Mcb0f jjM9f5xocPtOWOPytbBC8aeasVSiZvS3XNP+O5l+0FGxxdJe5FE9PpuRODy/N/xj6pp5 w7zeoNFjAt+xhxyP4Yl6j6cT9W4P0f4nYYXT95MlUjml8reSPvMHuabDWanBvA1ii0SW yCE/b8KzftFIcPvckWU9gaNN7b5j2zleEzea3IQfapw/TJY5PZ0scfplstjhZ1HBX0qK 9qNS1eAzuZbzDqlhaAc0akX22alCt+rwMX9OwGhCm89YFdtrYnWTu/B0rcvkL+WOE+MF du9F2bbPRFk2v05liOY45Am9XSom3L3rJjzC6ic8lzRMeGU3TnjVN417dTePe+1qHp99 iTE++yFjbPYo8w2486vnRMM3HhN1V92FtafdhTVT0baZ0yyb8PeoG58dxBj3WQyWpYNj Vewx367/cMz/Gm/U/0nrkP8Y75nvGPuu9zjz89lgmNdE40HoTSVZp4niwbII16rxIF/W aEB462jgcv5oYGn7SJAADNsLht3uGp7zsvtN8Ejn/cBR/g1onfEbYx/0GW/ZAj1xvuVy UYZd1uQK57LxmNnNo6Fz24fnxfYOh+T3DYXwBobC9g9+DP9uzbsFH9Y8Dh1adXv+cN+Z OSPd+4JHOjYGjrb3BYy2SRowz6tQK0WolUzUSmJA21DUwsEPUSs3vI9u2fxu8cGt72Ie 7HgR+3b7naj3G84u+LhuD9izFuzpBMdY4JikY1a0mGexTMS0yxLWu1SMlfqyh1aGrX67 LHHn68SG/SiZQy9Tfj98b8WrgxfAn91gxkDkhw3cBR/XNkCrImR4lXSTUbhknVns1ABq v9excJzv1TTMnNf7tjJu14vs4jPP8w9e+K3w8flLOX+e3A7+dIM/TTHvtpdHv9+cCyam Q0t2VD9YehgcOmC+VLTPNlO427VqdFsg//36xVv+aCs4/wtj/+3PG369tbXs6aX2/L9O Vqb+fSxr2ev9SeBPQsy7bYuiP2ySnZoZKD1tGCY5bRo7dd46dfKiU/H4Z97Mj9fCB/+6 vvLoV58evdW16qcfalt//7yo4cXp1NK/jyXmvt4fm/lu+zR/FiYhj5PQOPWvRszUOavk yc8c8yduYWyf+PO+fxu7re3Xts8rfnh0Jef8i0+W73p1MK7v7a5o9vvNC+s/rgsrH1o9 v2i4T3YMGicNQyWnTBaJz1ouE920XSl6ZZf127hnw+Z3MdsK/6g+n/nndyeWPXi9L/r2 +82hl4YG5hwe6Q7aMiII7Bvl+7eOtsoOzwyQnQA/LhqFS+8aLxB9MI54NmWVfHLCq7Hl Q8T6pFcFh5a8ObUn6vWrjeG/fByY8+1Ip9/NMZ73pXGm1/GJRo89E/XyT3Q85dd1POS/ artKJ7Rd/kDtXpmySlk94dlQNBTav/hd1rbID1s3hny8tyrw79F2/4dj3NnfjzM8vpqo d70hrHL5bLJc8ZW6meK5qqFkStXgBRhyS2oYsmPKOo054V6TMjK3I2woZXD+cE9f0Og1 gc/oB5bXC5z74USd6w/CKudvJ0sdv5osUr6k6GIJpfJCSdf9Wq7leFhqFN4zZZ1aLnSt iB0N4M0ZSewMHGXzfceOsDwnHja4jAjLnZ+DH9Ps+ElUYPe9KGdkgpDncor2nVJ15im5 ttsaqfFCJsYje9K5ZOG4D8N/LJ7nM16P9psb3YTXq50m35fYDYly7J6DH4+mMq3vT6U9 niTkjoJSuahQm7VVrju7TWK8sHLKcnnSpGP+XEBk9vjiZs+JsgZXYX+V8+SpUgfR/Xzb yYlM6/dT6VbPplIsfxUnTfnbZkx6OBVOuniVCV1CKoSuCZVC1+xKoVtdldCtq1rothPx aY3Q7UHNhNtozTs3YfVvrsLKb12EFVdcJstPuUyWTUVYpYjm4N4027VC6O5fO+ERVTfh mQaeVYJnHeDZLvDsCnj2pHnUc6LxOZhzFwz7HPw6Db2DCPEy88SpxcglxKl4wtezYdx7 PliW2DLmWwyW8cGx3dwx/1u8Ef+XvDfwYvfBnRv/ZdgB6G3xmKiX5KJO0qySRUvt84SR btVjwQHs0cDF/JGgXHgxDhi2r3t47nc9H+YO9TwGd74Ac876j3H2wYtt8B5v6Z09zpDW GUeKy8yXTuUhl1SXsvE4n5bhsNCeodDUgaFwxuDHhQfWfYh4uOGviA/rv4MfOwP27J47 0r0Geh3wdyz4MSnfMETCRq00Wa4QVTnkTRR41I8kzen6EBO/6V1MzY638Qd3v1r6dO99 8GfaR+1c+HFdX9h/PFQ9OFYGjsnW6/lL16Bu+81iRR126UKua/lYvT/vQ0HUxr9Tco/+ BS/1OPP5yUupfx/dBv4IwJ96+KgSaGWHDQ2mhAytkh/SdpMdnBkg3W+0QLzHfIloh132 xCbPhqFVoateMtOOPqnaf+nzsieXBguen2evfHmsePnr/enxb3cuW/x+a0zUh42RCz+u lZ/Qcpaf0vWWnTGYJzlrEj11wTp58pJT8dg1n5Z3ny9e99Oti9t4nQ+/qmA/uZxZ8/LE 8vzXB+Oy3u6Y5s+CpI9rQpYhj+PQ+GRaQ3+u9DzG97JlouhLu8yxV/a5tz+E8dhP2o7k f//4dNonL48mbH2zJ7rr/ZYFzI9rQ6qHVs0rGe4Nzh/plB/Rdpaf1JktOzMzaJohki9N oqWjRuH3RbaZg8PzupLBj8TXP+6Lefhue9itocE5F0a6Ag6MtvluGOP4dI+zvLnjTPkh LUf5CW13+SVdL/nPOu5To9quv+PefVTkkNs4GixY/C5za9SHUxtD374ZmPMA/Pl6jOt1 fbzZ/cJEndtRYbXrLmGl4qSGleKGurnyd1UD8SRd94lC0/4i6q5P5JCXPebPDR1KHpg/ vKkvaPhngfcrsOvBRJP7tHe5LSx3uo76vyQqUH5NU1M8o1QmxRTtqVJF63N4iM0Sk+gm kX1O4rh3c9DoEkHAaEebz/iVFvexj3Uuf8K3PJwscbw7WWT/jSjX5quplZOPCXn/kZDH ckLdVtL19st1PNslJotKRLYroyY863zGY1koD4bHxME6F+HDcvuxyTy7F6Jsm0dTGdY/ T6VZ3hUnP3tOyMNRQr5UULQTCjWTAZmeb6NkVvTKKZu0+UK3Co+JqHp3YVWNi3BTuePk 9UI70btsq+GpVKvn4ml2rLC4L1529y9CvsCnsJxR0lQ3KTSsODI9v1KJcUTilFVSwKRz sYtwQYXzZGGp42RPgZ3oRLb11M/pllPCFeYfxInmz8VLzX6TJIi9rZKmXByyRA6eeSKH efkih7iCScdMRG3hpGMHYkfRpONFxAPESNF7x8mi3xwnC791EBVcQ5xGiEPhbf1t0kVu zgWTzr4lk84R4FlKudClHFgWgGs7wbUriCeV42DXH4gfnCfLrjlPlp52niw56DRZIkmA R45EzQbb5Qg93CqF7sE1E+4J4FkhWNbaMOG5q3HC80bjmOffTW/BnPvuE7U3wK7TrsKq /WDiFoQ0C147CT4mxjr9PyzzBssiwbIscIyFteU0x77hffQb4T72HWN9AeacgQ/bC70N YFiP+0SdrFp/jrQYdZIJH7Mca7JIrOuC53BHg1a0jwQ3gmN7wbGHPX/NG+75NmhEcBZr yj1YUw5ifdoOL8aEpoyn6yNjombrTBZNleK+n4maTfBlDYcv6hsKrwDH9q97H/F8/X2s B8+CF9vBsR5wjAWO1cLfFQeMtsrXaDnKBnR9pD3w/G1gK8NmpbDCvXY4bX7vu9iMaY7t errk5Z5PE97s2bL43dZWMLEWWoXwUBnzhntXgGOKPRrm8r1aDvI9ut7SnYYh4i3mCaI1 DrljAvCjPH7ri/x9+68U/nlsTebL443LXx3Mi3u7M3nR+83xER/XR4cPDS4IG16lOKpm rDg+w1p+SstFdkbPT3reKFz8qcUS0RXbzFeXl9T+9MWlGnbX76cK656fSy7++0h8FjzM NH9WfBycnzjcPydhpFtxRH1aw0p+Ggy4oDtbdg3+7EfDkPfvTcJuvFyU1f97Py/7m6fb Eo6DPxvebwnnYw3VhHVM5UhXUNGowD93tFVxGHkcm2GpOKNpL7+Etcz32q6TYnXzb+BD +oWupZEf0laHDt0bWPAA/uX6SJffWaxf9oABa7Ea7pxo8GBP1Cv2qxkpjqubKj6DD3lA 1xufUJlxXzHD5gDqHzfdqrkjid3BI8cFAe/et/reH2N7fjnR4HoV3uM8/MNhMGAH/McJ FU35DZqa5FdKZXiUUA8VKupn5NounWKzuDSha7n/WBzXZ3xNy+zRn5rdXgmrXe6DH99O Ftt/Icqz/VyUafPpVPrULUJG7hPyFz5V6KGYUJ/By6yX6frUQSNW6FzkORHd6D7BrXUV flrlKBwqtnsJ3/FQlGX9w1S65TfiZIsvxcv+/I6QX38l5O47fDIROLQLDOGi/vPEpjFh kw65rv/Wfj1qf1+RvehBjtW4KNXyBdo+Ei83/1mcaPa9JOHnnwj56ikhl0cIOaig6D1g SI1Mzz8ZLAsS2WU4ToYW2otKc+1E68GOz9Mtxe+TzEYlCWZ/SBJMf5XEmdyXxNx4QMgF cOiwiFCDShVNJsazACyLk8yKnD1lk2o3NT/LZipvpfVUV5ql+FiSufj+UtMpYcysIUn0 rOeI3yRREjezJWJbm5Qpa7e0KZu56VM2scBgBqIaIUBsR1xA3F8pshld+dFmKuN3xHeI q4jTCMlcrD28LJZNOdivFNl5Z4nsFmaL7JJyRPZluSJ7PmIH4jLica7QTpTzwl6U+wPi c3tRzmk8HkRIY+CzQ01jxL7wMU5OeZOOgeBffNGkU37xpBO3ZNJpB+JGidD5Zel7sOuB 02TxDfDwNOIAYgtClg6/vhT3/EjzhKkg+Bg3j1KhazjYtxKejvlfT/d1zQj83BN4sNvg FxhWuRdMXO8iLO+Br5OX6XjI8nGvTQWH4rC2m4c12WzfugmvpY3jXnVYl+7CuvQB4zVu Mt96/8uwpl1g2Gr4Oj4YxsD6VNGiaSdvhE6V/hxJPrxDsnXa9BpzLCCcMxpQjL2y3W0j AS/aHsI3ncF+1jb4sW54uxasUavBsUJwTLEKNdczw1YugA5Hf66k3jRGlG+XPR7vwxye n9w3PG9P7x8hb3ovYE9rei+KEzLcXw2O5YFjaeBYIjim3E1TV+5U1Vds17CUb9V2la3X nyPug69qccj7kBXR/qxiH/tS2svBnoQ3u6uwj5QJ77MMeotDh/sjQkZ6QuaNdCkPUXTl ERUN5XHonFK3kJ8BRy7qeosumoQ9vZAUf+fqmSWtgierU0v+PrQk+83eaf6ELf+4et4S +Jf4kY6A2FH+vxrHkMspuo7yoqqR4paaifQxXfvZi1m2135fErrp3prkwpuPGeF7Pw6G DgytmsMd6Qysx/qpdIznmzfGnp01zlAepOiKYzRVxWkVDcUVFU3ZPZray3GKdlWsYdEz 7BKT8CqrKvTtnVrfe2NMn8vjLM+T2DvZIaxxXS3Er5EJK11ahBXKA5SK4jhFl31Ko0/+ SNH+ekPI11IafTv2QwrE5vHuwgjc8w9UOH8cKnO7J6x2vjVZ5nAZ65YzWH8cFGXbbRNl KQ4TauISod58Aw8CL/LNCKGOKFQ0uTJd72VTlkudJ8NR972FDsIf8u3eot19Ubb1t2DH LdT/NdT/RXHi+3OEPL2JT3EDA754QcgnIoq2CmuZcjAkEhr2ovm5tiJmps3UhQxr0VCa 5d/wHL9gpXBXvMT0G9T+bUnMw8uEfIscrv1CyMm3hGxAPxgKdYsMaMxBX2ym5qRbTVWm WIp3J1mI7y83m5iKM/1TEmPySLJ41k+o/e+lUbdvEHLpB3zaGzi0DTxtU6polCs0LBPB EG+x2WJLceAKc3H+UnPx4BIzydV4E8m7RUZj0oXGf0gjjH6VLjS6Jw0//xUhR8ChbeBp N3xdPTiUDZZFy/R8XMF2c4l/gqkkM8ZE0rHYRHJ8kbH0/kJDiTDUYEg63+C5NMTgN+l8 qcOsSImFZbzEzGWJxDxoCU6NWImoQgiWSMy2Iy4mSMzux4vNRuOHzcVLn5iLE+9YiJd9 biFeftZSvELmj3u+M9YeVtaJYgvP5WKLMMRyRAmiFbEdcXmZ2OLxMpHF1LI/LcXJPyKu W02lnLWaSj1kPZUmi9DzlQaDQ26mcWJru+QpK7/UKauY1CnrXDyyEdsR11NEVn+lDllP pT8E+24iziAO2E5lbrGdypIv13aRxWANEwIOeVksnbJzXjllG5I5ZZuWNWXbjNiO+Cpz 3HY08ym4d9telHcGsRexwUGU1+Mgylfk436diXpdDp5FgkN+1mCiZ96kQxyYWFM46bC9 UORwL/89jv0OXu6Ui7BsF5i4GtGGaMZzyib6TEUN6r4E+WTODILHWwR/lyV0mwNfmF8p dNlRMeH8ovThtI8Dv7Z4TtR3gmEMeLtKMCwf8U83paIU0HUUXHUzeaO2k6wCOitNYyfD sM/tmdAwHLCt+pnf69rzWEeuBXNYYE657xgnG2vUJHAsHhyb2kKIcguhlJtoasr1YMjA DBtZh66PuMYk+lVOVO6j+l1JF9L/qG0LHRooge9JDR3qj5830h05Z6QjJGiUHxQwxhNu J0S2jxDFIUIpjlE0xSc0uuIcTf39yVl2944n+946vd+/v+eXwkVp7zdHJn9YH5oIhsRj HQT2+C0e4/osGmON7yBEAg35UULJzxJK/Dmh3mPu//CTgc7Z72OdNn7TH1x/9adE/01j PP9OcIc5xvauHmd4FmIfJHuizj1dWDOKPKagMXWUkMnzWIfcxqcj/k7IideqM7r+dPfM eFywaMmzG3HYay12uYC9j8PYN9kyWeTYP1no2Iponiwc3kbIxF5Cho8T8uIqIT/9SMjF V4SsllP0TJH+bJfRsOV2wp2JlkMfVtjcm0q3uTGVYXlJnGz5iXiF5X5xktUWcfJHjOlf hwl5dBH8+RLtfyNk+xihGhV03cWoXXOx3xJTiSDWdOr7WLP3kjjzB+IlZt9I4k1vov6v ShbNOi9Z9HIr/MtpQm5+TshZjMOuPwnhyyiVPKWa8TzZzABTiU/MLEl99CzJuahZ4uGo Wa+kUbN+wWy8i9r/WrrA6Atp+M/ox5VL4BcYsOshIT0fCVUNL5OsUDP1lc30nyX1jjSS Fi8wku4KN5LeDzcQSeYb/omaf4T4CfV/Rzbv+jHw5zohO78npB8cap6kaPlKih6jUDdz k830M5J6hRtIs+YbyAbmGciuztOXvQ3Wm5D5z3whC5r5qyxI/55szidn0X+Mwyr4OtZr QsqwR5X6D01tATTs5XrehjKvufqylOCZstbAmbKjgXqyn/11ZZPeuiPy2Xp/yHz1fpP5 yiyx9jA2C5caOSECEdGItDCpYSWiPVRquD1EanBxvtTgwVyZwegcoYFs3lNjacT3syTR 100ki8+ZSmLl7vDXNobzpCaWEVJjd0QoYhmieKHUqHWB1Gh7uNTwMvR+D5EYSkL+Rtuf 0faGqSTunJkk4TC4Jp+n6SDz1vWV2hmFSUysF0lMfBZJZi1C5ERLZrGipMbbIqXG1yMk Rn8tGMf5H5lJ4m+Zi5ecAw8PgoVbLcVJijisPRZqOckCsGZwxOs5ZvaxEtO5cRLTZDw2 xkhMti2WzPoqenLWSPQfaPsl+AmGJu8HCzeCgz02UyuV2SraimQ1U3ks8pk3MwBsjRZb uCSKzRcjKsHjbQkS059jh9DvO9MMBQN3gYGr7UTZbfB4zWCbrBr8KFOZocxVM5YnadrL ImcGStzNYsXWfkliy5wkscXWxEmL50sfWU+lgoG58HCFnU6TRc3wdhWIXKxNJzmoezb4 waDRlbV0PUUB+LFMz1/sY5Uw4RiX/iZoU8LvQc+WnXOcLBiEl5v2X6WIDPi7ZW7Cmhg8 vuwiZKiXEPEq1H8/9LqwD8FSNZgoM5z9pDAu5G7NxqCLuQ8Xc3zHGLn+Y9xERBTWqCG+ 4y1BeK3S13u8+Wkfam6QkDcbCPmA2hnehdhPyG87DHVub0+wurBng/W6vm/D4lLfsoPi RgUBMdiDWQTvEoV92IiJeveFwton0HgBjZf4dNW/wKM/DmENAo5cvqpJP3B1gVH/FZ5t 87lbPiHr3uW48oSVrg3Yuy0TVrjkTpY7Z0yWOadOlk5rPIfGC2i82AkGoH6uw0tsQ/2x 7zmarPwx1yvxp7M+Xl+8W2R5SrzCZs/USpv1iB4EZyrDtmEq83eMxdPV8C+bwL+DhFxF DR9A/fHeECplSNvI8014oMP7jf4Wr/8KNv5ZGmFyXbLY5CK4cVyyyHSvJMZskyT+N2g8 WoM9FDDg0zNo/wUhvb+i9lC/4QpVPX2Jq6+ujDNbV/Kdl96QzM/wvjTE8GtpqOF11P5l 1P5ZacijbrTHWJ4BC/ddIWTgLiGNfxMqTUrR/JRqhrpyJy9tebm7tvyUu47so4fea5nf zIeyQP07suCZX8kCZ96SBdzBNT2L67DnAhiKHFoeEVI4RKg47DG7K9WMdOQOHtryLFct +RZnLfmPLlpSmbPOS7mn7iP5bN0fEd/JvT/bjPOfIGTwGiHcO4SUPyMkHd5yIV4vcwAP teX2LlryZCdNRbeDpuKSg6b8laOmUG6v9afcRftXuZv2Pbn7EYzDOuTQhntCzQNCcuCH lk5QtLngoZVC3VhLbueoqVhiP0PBtJ2hOID4wVZDMWmlMSq31vxD7qD1m9xJbqzjJdMz 9pPpOfjKdP185LpR3nLdlNlynXJPuQ7fQ669zU2udcFZrnXPUa456iDUkjs/1ZP53tWX zblhKA05byQNV9hjjW6i6yWbaeov03OB1jxoLYFWgbdch+sl19kGnc9c5Vq/O8m0pE6v dGWz76H9LUNp6AUj6cIjsyRRCh+s051QryZ6PrKZFoGymV4BMr0o6GX6ynVbkNNW6Hzu LtP+y0WoI/f4Fe1vG0nDLhhLIw+bSBZtAxOUkSqaimD1WXJXLQeZ+Uxfmb5NMF76DJLN XAGtBj9oIJ8vPcQ6ox4vZsqCvjGSLjgPHh4wlcRsBAt7wTNlCkVTJtDUFWFqRvLZyMfS wF9m4DhXph8FrfJAmd5W9O0nz1Fdmffd6fZg6R5zccIgfCEfHGyGr5soJESaA9+QCn7E 0XUVwTNsZTYGAVJDr3lS/cw5IqNNgWNmv/r8hv6D4XHbLMXLO8GjZjCwHDzDAjz7RQ0h b6tx366AVhH8RzrWIJHqFmKnWYHvnePnPQ0e9Lk390ffc1aTCYPgJwNtiuHN0h0mC5di XboILLvHQL0wCXmC+AvxoRkeoppQf+UYmPyQucjpRmGv3emcLz3agt+nZsC/xbkJqxa4 C6vnIPw9hFXensLKOyzUKxeeoRW+ox0Brt1HHX7Rpa16tmeh3gH8Z8r+tqtOKZnPU9wX TNS6LcDebTgYEiascA4FP0InS79jE/I9NH5ow/2/E2sI1M8NcODAIRVqzWGfGZwj1fpV B05bRq1+Gm7DmEq3rRJl2RVg7ZOFtUwqIkmU8y007kDjez7aI4eb4MDxA/D/8BJlN81m rLiZZBF7a7eF3+XHHkZHpWGztksjzVZL4s0F4iUWLeJllrXiFd9A4ztofIN+XAGLju4m ZA1YWA0OxTxRpbv9FmTj8KTbyvLJPRvtH+Vueldk/vpnZMH6h2VzDXdJQ4zXSxd+DY2v eOAP+nAIPF13khAmWLgSHAocoSjtCWtTDVGdsfrULSO1UYWx1j2Zk86Xcg+da3Iv3Uuo /dMyn68wptc6sAezBe2PoP4/I6QIOcRjj9hVRFGqShNdujJHm648rE1XvNFRf6sw13yI 2r8jd9L6Uu6sdVPucgt5HAeTN4BDfHiRyluEpILHC/8mxFZMYQFtokNXJkFjUIuu/BYh 11J/qTDXeCS3nvGj3E7zW4X9mR5CdsAbdp4gpP4KIdnfEbLkN3zI91tCWUinNWahXZyW ipI3Q0V5RlNF+UKTLpLpqP0pN4GO5YyfZdZ718MD4TowMI6FYGHyz4REPyHEB75sluxf DbSLQtRBYxfiG8SYBn1Mpq32h3yW+m9yM4Wupq1C09BeMcPOVqHhba2YEWGt0EixVGiU WijUeKYKtc3GSvpZPSX9J22lyqjmBF2p91RDYXlHS+54U1c++6KezF9pgT1HPS1rhaax nWKGk41iRjA0EoDBPAuFOtvsPxqfQuM3LTldpvVKXWF6D+2/0JF7Tbc/Bp4o3cEPS+yB /JuPCXTcbRQaC6Gz0lKh3vxfjSu6CvqfWhOqSr2HMxTWX4FF0+2P6MvmbjeUhsnmo+69 wQ8bVQOFnjbysLBTaPhBZxlyqTX/Tx5f6Erpozov1BXmX2vLXabbH4S32wye9IGFY/Hw +xFYMwRi/8EB/mOmNsbEFmODcVEvwZhsmiVXvas3pqrQ/0FL7jTdfp+BdP4aeLN28IgB f/ZHGiGvVmDfMQ4eJIxSkbuhb4balgpNN1uJ5kqbEb21Fq+Mvjd/pDPlgPZ+07l3gYXT bcvBwRyw7Kd8fHp8Du7ZmYT8mQI/E0coaSBNbdjGyOqZQ4zdz7M7LL7yu2Fx2vqD31qc e7ptEfxgGrzZErAsGiy7CX59UY6aKUXdFhHyMzQfZ4Any3Q1ri8NMz6dwjI4kHTBrGP+ sznYNExZbDuVEYI9ukBHUZ6fsyjPx02UexUcvAIe/l9crkX91WMNVE+ndjX6q69pqdXg NR41Ss98MMd5vjDbfg6YMR1zwZDpwNfTbS9PB9peqsP9uwn1jxpaMwD/vs5WpWJDpkbO +h06kV0/uJjXi6LNSrCGyRYvtUgTL7dcgTVQojjl/9p/hvYXp8/fQsgWeCMG7sPJn2iq RH0SrTP/TK+O18lvTGfunfLU3SDzMeiRzjPiSRfMapZEmVRLFk+f/7P/5j99/s1o37od 927U8LyvKcruW3dd8+8bdIzuXtNV/1ZsovGpwlrzuNxRe5/cVWer3GvmoDRwuv10H06C 6dPt2+Fnyo8RknCdEHcwQO2ViTr9XYGqyoezdNq7STr9Z7mO2i2FqcZlheWMC3IbzZNy h+k+nGvEGgg87oQXqTtISNqnhCz4ihAb1C8R6SFSEYCc8DGhvZLT6fdRs1/Ljaa11D9X WFxCHgdb4aHWYg21j5C802AYcggGy8xeTGvMRExPZpxEdA03n1FC+1OhSn8o01K7IzVU +1pmPD0G+CBLwsVpSo9jHMHCyNuEeN8jxPDVfzWmIqGBCyY6hHhAyCgM3x8KNZVfZFqq d6X628GxLtxPatGHTHBoOoeQb8DSXwjRe0ehLfKYCkGUITZAAzcN0d+Y0FIa7blSTeWR TFM5Q0NfqaZvoKRb6ypVvDSVKuEaStoydSWtUF1BY6nJqPV0KXWSJibfUlPkLRmhlCpP aUq1u6rKmTc1FFYXwRKlIX0GdMAYA2jYzVCqBKB9LCJHXU5rVpVS6+hicpomIj8TIZkg f9OU9J/pSq1b6gqzi5oK+6NgidwO/sNIRU2prq6jpBsjDxe0D0Okqsto9apiaq2KiFyg hOR38pGS0h6pKNW+VFUaXZihsDmiLXfboSvzHfFHN53AIWP4D1V15GGK9t7IIUFNSqtQ FVFrVITkKjVGXlN/IIev6Ertc+DpAbBos67Mp19fFvw0AmuGOVhvuIMfptjDVFVXU9Ks 1OS0MDUJLZ8upAZVRqgvqNfUBO17FcWM8+oKi31acpe1OL8ALGXAm/2wFPuNi7FeCce6 wxdalhRNrq6uKlFx1hiiJ2m8UO9SfzTjqvr36u91LoDH27Xlrl1gGcNAOq/SWLowFyy7 Do7dTCLkNrS+WQR+zAOLnAn1zERf8yfzCN2bdgzti46faB0xf2axVkfuyUDuxfCkaWAZ NhDjo+DLzuVhrucgwLFzmNIXloMlMWBBmIbKiTA/zd1R5TPwlgjanKCf7DPNJxcuNpPE zrcQL/WzEq/wsplKdbGdWvlJEeq9EMwAvw5Bb38uIbuhuRnRk++gwi7OUqku3DgjOeW2 vWfIh2gLX3GihZ840dIP3sMP+ygB4uSTxbhnQ+cIdA4WoD0eN4KLrZjahTx9Ko0fT0/g d6vNZ12bZVX1wdsgXzrXaKU0zDgJ66GlWAfFS2L/1YDOEcQBaG1H++5m1E4PIRE7aFTA jgA1z90NavY7z2nqbX5rpjGgsNJqlzvqMmVeM+ulgYYV0vknS5DHfzV2VSJ/cKwe7ZeD I7NPUcTgspWqztVc+oxrh+gqV1+qq5xWYrGonKm2HQxZp7DW6pc7ncR5j0PnAPg+gPbN 8ITZqMGFhwixu4pfi38wk6IeJFPUvU0Uuf8LRX09hY1zparKeeUM+imFruoRufFJlOPR cowh8ufi/MUb4T/AkSBwxAxegjwGg36PxQtfMI6PAKeHqNk7MDdfKOm0a6jby0rNaY1d jRgDAdZA8CJpezAO4MjsS+DHt9B4gtp/vBBRBy388Dc8+fADzCh0voXObaXqvnqsw3AK 5iAYthP8OELI/LPgBzCh9yM0nkLjCSbeU1ywJ9OL2AvQ+pWQX0YxIWGU7v6jgk+/IW2r CKnAGCYfBMNOwQ8iB3v0Q+sBGPQMGs8CsJBeiWiF1n5o3YbWn+ifCDdaBfUPXV3tH2qm yj/EklIQD0pMQqhJsgR1mkOGSAN5R/rIK7KPvCSXyRPyAwjwijwiMnKHKKkbYNFpNaWh Uoui/UNTVVFS+pSc2FIi4kvGSTQZJuloX4MWPeQPaPxOrpDfyC/4MwQltAfPztCVOgc1 FObjltj71CTUP5QqJSOG1ARxJCNkHvlAlqF9GXlBOsljaDwiN8gD8oz8SETkFo4+R1Oq H1JTzNqiKbd/5gn/YYP1iw72Pyg6+jCLfCSe5DVZTP4keeQpEeDcB8h98g3avyNfEAV1 Fizar6o03ARf1QsW3MWQ3/fFUDsQ8jeG7yOlCvKao//B5Dn68ztpA0mPku+RxRfI4Qyl oO8GjwfB43asUxlg2XUw42Y41vxB8P6uuGSG4AdNDTnYkV+peOpHGpO6rXKMukx7RDtN U6puhbfs1FBYN4NlFWBZHnzd2WXwC7iNnY0GP3Ab+dQbPLLAVNTSVvlcJ1j1lEEp/aDx Tvo2/buaAxoSs2awvAgsSpspC1iCNXLU9BoVl/0g2LMfnmzvEsxZaO2ej+e8KGqrlxN9 lX8GvS1gULXe47phhtl790UGsuB5RtJQfxPpQk8zySIXS0nCviy0yUDNpxOyCVqD4GIP cuMnEtKwzJgqTU6gMpP49Lj4c4Y+8595zfKUhBt7wHt4SiNMvaSRpt6SxfuyoQEOboPO RugMQEcAxtbmEpJYrUoia+fS5tfVq3hXHdG0LHpsoZ0pddZJlnnqLZX56cfhlYoYaeg+ MG83ctkGnY3Q6UcwwJOMBsz5LorY9TnQLAayaYardtBn9P2iqdKh0KSzlTPV6xVmmhVy O50Smee/GtDZjnzW45ELjhQxCFmEOnYEB1SP6FMqRxIp6ijq7egdiuwbpqjtMoq2Dv6h X6mt1qGYtQ8c3o28tyF60L4a66qkTqxf1hJiDo6QTzH5LmCgzzNx4QCF0yjmE6+w+Yva Pyih0fYp6fsKcC0Qa9CeCY5ldxMShfazd4AfaEIuQ+PSPGgBtp+ugfFDUZ//GQEWnZ/C pJBS+4GFTWiP/5dIyrswjmg/H+sqlwPgxzlo3NDFDccfm16YBFdQ+1f2wwTehO4DfP0W i0EZ2Y72eMsB0tgBD4PTRKN9wF6wFDloXYbGbWjcxMS7vhyBgb6+npDPz6Dtd9B/Qcit CbIa7fH/MEnxIPaBthISivbehwmxwmEa16HxjR5efPTCsZjMX5ThEcneQi638MPbv8Ok fyAKQidSMhPMsCDviSv5i8xBrcWg3tPAi3JUGgf1tpp8S/aTr8in5Da+u4UjrpMxEEVO zlL/0CZVsWcBnVGiT94Qa/DGC7W6ALxYjnovJHcJk3xNBlCz+9HuM3INVXwFZLlE4I7A ogNg2R+m2K/QgGcgquRvYgDe2YEXAeQecrlLsnHeetCnn1wFQy6Ah+egcQFnPElk1H6w bAtdqfujC+rdAtjVRM0jn9+IEY5yQcbhOH8K+ZxUowe95Cw5RD5BL46jl8eQwy5KQVun otTsUVOY3ACyb7tj6KyxZtGGn6Ho4K0J+u6N8yegfRk5BS4fw5kPg6SH4Ou2UjJaPzxR q6rCsAksObsA028OpiIu3yV7XDJchptg2Q1ihd4vQJ8L0XaQ7MUo7MSIr4Gva0P7Bvi6 UrAsB77sUBxYEYX7bRgYEoi9Q7DskCF0VdTJMboHtVN1JbVWvZfqVb+iwqe/06inK3Tz 4StTZijs4uELo+CrdiShVhIxV3H510Vj3Q6W9YCvg3aEdFib0Vj28VSNA5tWYHtSPcXk sXGktswxGJ509kyZn6uBNNjZRBqyBbzYkIw2K7D3uhTeBbfnZuRViT5mh2uQpMgQEhdZ QwsL3zNjdtBdM33nKW89J5mPvrPMV99FFmDgJpu3JQ0aKdg7RT7T/OIlgF/QyYPewkyK +Oa4UG652ZRtzga6ccbXWuqpklnqiXKzGbEya61omaNupMxzSzo0kMsgculFLq3QqYDu 0nxC3GpRd2wjagZnCYXPAqNo7OsUYU+guP6haJUKddVi+Uz1PLnFVmhsRJs10OlFPs1g UUY56gblZdmKetmAibMaHVtdj0AxD3yBQXuKg4ex6MKGMRf3i2mOoqzXQqsdHCqtJiQO deiD9kb90NiqBYODC7YxBwGTs+EQ4iq+vot4josxTraCpRvB0l7k3liDfWAWcmgHP/rQ D7CQ7IbGTm8EBmxHE6C5CXEcX18DvO7ghvKYbEPuawoI4aDvRRzk0Al+rAI/gAmtXdA4 gMm7zx2TJw5RgUAu+/YjzuPrW5hMP5AN4Fg72tew4aO6CAlfDQ6ivdV2QmYgbXIULDyC iXcEN9UjeQic6Mhm/DLQCTwCUse+Jj11GEe0z0f7uEGwGPm7bYOf24t1KQ4jn0DjpBNi IQIDdxJje7IfgVxOXMDPb6Hm6eCGHrhhDm44gzkBqPkIMGcZmJENXlSjbnio+dWo212I s6jdW6jB++QEqHUM9PlbFf4DOs/AskfEEh7DHU5jLrkJflyFd7hASskZwsLxA+QI2Yu6 PQ+SfAki/QZn85HshsZ9Y3gPNVg0MOgBWHaH2OAs3qjShTh6OSq+AG0ZOL4Px+9BJp+S HThqGzLfDIKuoSS0L+yBXBPse4JlPxIVnMEQuduj5gNx7jicLwvtGsgWcGgDlNbip2tx tkE4nV4iotrhaS744NbjjFuROZCviQCDToNlh8Hn/WQB2Q42r4O3GwAP+5BRL+jUhRxa yRjFpOQqdSpKrSPzcZkCEO64VNa4VJgOh8GgXcSUbCX+ZA3600NqCZ+sJ1yQmI1xaCKj VCUlUylQUWhlqCqMd0aDH+HY85iL+Yqc1jmg/mbia0oF+dqRDoxtC9VAGqkDpJb6mSqj RLQMmkxtGVi0WE1uumCGwnY9anwwDp4BWl3Qag3CfEFOjehbnZ4OKTUMobKMy6kko20q cXrfaIZpTBoHaMjNPTTlNk7ackc7Pbn7IG6BfYnYtwA3eIvRNgJ1h/6tRP+W+FBkoZ8H mRuQSfn4DdAdva5qGTi8tdS0ldto2cpstexkDtoOMtdpfvUtA/eWwHuAPQ3Ip2QB/AMi KJYiNstMidnyBKK/vI02I/E8XWXJGzVaDPYcIrF+CVfMVAtVmK5BOfYjl070aZphTTGY 93iMTsYeLOqRVo2LXhlKSFUd4IQiKL+MRJ9hsTeGDR8FjVqppE9rrEIu3cilFX2qxffJ aBtYRIhJFeqFCY1GbwRA1cjESdZgwA7BcH2Brx8j8SGyFuwawDm70ZaNwwoKkEMl6q6e EP0WaPBnoJNu+CEGnl0GSPCRMAq77SDM3yUMwh2yFu3+9YIZqP8S5ICU54Bj9ihxbQE0 +qDRg0nYhYHqQIIC5NLZj+e242dHwcdLZB3KuQ8sY+AUeY3wcmz4jzbwoxP8GIDGGk1c fMf/MHUVTriqFtGNE4Nng/vAxXNkENLccgxVEyEruPBR8EPuQJUZ2qtvhMYWLTDTHhGK QKc34uD1gO2GDeDsfvD2FBFUYD2L9lk8jEMXOIg0ncCiWWivuhMau7XBUjvEPPATg76r GIFkd2JsdyKPXSfgaFRRAbogiin8ggPq1gd1HwpuxIAbKVh15IEZdai/VtR8H2poK+rw FP69hlr+HvX/mDyiw3+AQT+BZd/AT92E97gAln0Clh0Dy/b9W/tVOL4Ftd+Hut+GOjyB x5uop59AtxfkawO85qGKgM5tsOwKWPYJWHYIvmwvctlKUlGxxTi2GdWPD1KDRic0unF0 F8jXAZJesoHNNIJlVEOAQRfAskNg2Q4yG+dd8N/az8OxzehNDxzedjDgIhjwHTJ7BsKN kmOeWO9j2E+YANGYkifAoL1g2TqMTR8JQttYHJ8JZtSCZr34dy9i2mE9IJVY35Vgx2sX an36NrbDGbcvCwSmw0bodEKHR9zQdiGpQn/KSSN6tBau6DRG+Q5Wv69IBpmkkik5ff0C 3GtR6wMBuF96YAqib626mHMUDeczQ9u5JB9HZ2JtmI5RToYnWw4Wx4LnETS5aghdrtsf +59p3A4tNqZAoy/mmxNKErzPoamQZLoniVdPJ5Ea3VSo+kWVYPpLNS+aUsMB+2RWWGOb zFCYd4Mb7XEopUVoH4H2IfA/gdiDdce9254iPvZWxM0xgdg6ttBM7Y+q6lo90lI1k+nR TZV6quZKfXVLqem/GvHQmGZYFDTCMWfB1liMkyfy0onQJGrwU/SoakKL2EpREd9SZMEQ TiCnqLkKGi1YqdoD9ggS/sOwJuRSiVyycGsNQx/NwRSShkmYjIFKwTcpxfgetZKyC8/d AuyeosikpBclMM1BHnJhoF05Hpclo3ZRz4aoR1KgSkgu6jYXSeblgF8ASwkKs3Q7Hs9h wfEd6VuOawGdVuTThK9zMuDlCrCWKyFEtwIaFeoQRs2Vh+HrVDART9YATtMbT42bEcdJ H/jVBRZycO7yXOQAjgTUEGINFmk2QaMZE68BF7wBg9SA/jSiP83NYGQ7Fj4ABHcL6U9B X4CWepw7qwp7QQ2EeDAwFizwA10n7TPAQ1uwLxgnQke5OBEXsGrl4/lVaLyR9KLfzEJ0 qxprOcgHswlxBktMcBq1Xmj0aWLyIY9eXPBOXLyuTDwi0a42QLgfTNxAeEitqhb3JCZy QFsfAVjaCU/YRwh9LTTWaWEiWyP8wcUYPEJjFTq8CifDZzOTddvBHjp8jy7WKibkJu6v n2EddQp1f5REkr1kCao0nWyCB1mLe/8gqnYVKrEX5OkhB/F4CY9fkTsq2PeAzhfQuQqW nUG9HgHL9pIQcGcR2JGEdtk4Hm/UgwrGBx2icjYjjiCugAffkqszscTENLoOnYtg2Qn4 sr1g2bRvWE0W4KxLSDtZiWPLkAUTf7pBks2I46jpa6jmu+QTK0JOgWVnVfG7F2DQIbBs K1g2CJZ1oE9c5MJEtdajP7VoWQWNShxRCQaUw3WVgSN7XWF1Mex7jWFbMZ12w3esgY4A a0wGWFZHwtAukRSBGIU4cy5GIBeZZoN4mSBwBli2CcO9ASxb64Bbjik4gunQDQY1g4nl GOMCsCwLtEjDv8l4dgXOsAwjvgRXIAF3hHjshK0KxSXG9OnwwbRxwdSzxFTUxuvQYFAm WJZEPHBcFHa6cnGl2jBCe5HZdYz4M+yiCeH8ZJQgGm0X4ndHwI0GTKMKL5SWHcpSH/VP ocwpGzKPiiX+VD3xonYTN+obypH6QNlQCsqM9o+KsYpSvS0W/ILONH+mbUY+ckoBX6Oc ULtmuO8azySmJiFE36yM0jTdTFOd9aUKzWiUThn+o0IZ/UOnGf+j+q8GuNEYCY1waMyD BtgaNRv3TIRKAI1Qfm6ECkBRB6CY/LcS4neREN97iDeY2JOEH/cfhk2vA6vRp0LkkoYS nYe8ZiE/Ek3DhqgtRNHZxUuxMMhHcPCi+xZ8/ymMwvekfboU0R8GcqmFTiHaJSSAg+DB THCBrMBEXoaBXo5OJgFWKekoLjAkEzoZKKiMY0TwX4YxkU8N9FaCI2EoKzsgSzsXGrmq gDQ0sjEJcnBQLn5YiJorBVzKOzAAG0jHfxnGwGMROJCAVH1KCbHAYTMqoVGuhgttgfDF 14sR0KjED2saAR0e4IePdsVQcZFzdRbWcsXIARxyBQtM68GPZmg0aeCiQ6MeF6sBnW0A Dxuh0QwNJjRY3USQgqeQdz6ejkfbAKToAI4ZseBhpjnWhonbir5wcJFYGHQ2TsguwvfQ 4EGjrZuwkHsZhigFly2MibEEWmzxIwM+rms3NHo0Mfmh0YXJJ1gANqLTgjxEHQIHdnaj auioPh2wYxaqyAZ3VDd4Hn/sU4TBOyxG/U6vG9JxDy9CHTdgtuPjB/AMm2zE6mofHs+T m7h00+y4BJ3TYNkB1NlOsGwj9nH7yQK0jUe7FLAjDwyoRC2zwIxusGADanofHi+Qs3rY P6FjvwM6x8CyPbjXbwTL+rEea0dVsbHH3YxKrUcFVmNtVwmlchCtDBql/6+9c42N6rji +NzXPsBr4wYw7y7gmKfBQAA75mEDBmyCeRgHgwFj1otf67W9NsbGIBCQEJ6hSUugTQWl NFJpm6hqG1GUVm36gSZt1Kopiir1oUZq2ipJFUUpqvqht79ZINqZSyTa0jbBu2hg578z Z86cmfOfM7N3FnZKjbR+CZN/BS57DjmX4KCzcNlxuOwQe8weMRveWUS9VcQqNfSmEW1u c8jn4ZDLSL4qzuYRgo4l1HyA8JOp8Bk46BBydhPftbAfi8Af2/D9GuzyKGyygZ5Uwanr 6PEa2LtS/ECchDeOT2H5CGPi4Zid6bAHOTHkbIPLqrFNJbFmBTVWEM0sxx7LsPQSLFdK TLWYU6zD+NcB/LSfoevJZe80Cg5hSamDgyqRswzbLESXIphrPj16CJachQ4z2aPmE1tO 41StH//cvZBpWMgUns0yj06bWJJWY6MFcFA+XJaHXSbQi7HYYiSrzXDqP8DK9Cn22VnG P0Qffir5p20R/oJOtXNYM6dxdjCe8wtsFAr6RGDwNGGGaoQRepwEd4R+TFDwaw43/oJz p8iAf6LwT+1cZOAaJZPxXext0D+RyzzNQ9FJEMLkNtITpIuk77HRuZ6Mw7qX4oZM450L cHF0WY+cwmJiGPrIEkPCSEX47SL8pYS5XrqTf/ei7BmwF8Re3LmH/nTQnybkbEWflcsR Dw9lwiei3ISvcgDxuQoaWw3hrNsEaUMM1cipejLJYbuR00m9JlI1dFeML4/byDkMlHUz HkNG9XQe2EFGDf2p3QL5N8FFOHkDvzlHe73QUwdtbq+iOSgmfzt7IPzZH0GGjMd2jORD DL29BJKAcBogi0ZktODsbf3JOGw3XWyizQ3ULaaruXw8HD5wMN/NeGwE/DWV9wxeC4Vb Icm2ZhpGRoIfD0S19s3YgTZXAM+Kwx/t2JOPbbgoyWNdyOhk4nRi9ASKJ+hLF40kuWyP 6N7KmMKB62PoAP9N2cW6RN0hfeyP9yFD8tg++tI7mcTg95WTULoP0t3TBZH23+KODE55 huHJY8UFZuU5YpjTzO6jeMVB1uu9eFgfHtrDzqEb1kjACB1wSJwdShvz/kWG7ltwx9fh oIvM4i/AZU/BZU8wvw+IYvY5S6m3Gv7YCAfUkdrgj15Y4DDpaTzoQvL467LF+QlyziPn abjsOFx2AG/qZVWP43Gt7Mka8fgI8UMEj5aMtg0eqiPa2grrPEtXn80i2ZylCIv4KYT+ I6g/gRbz0b6QOksouya5i9mItA1oVo2mVchYR+SUjF3gspPZ8AhT4Qjc0YecOB5aL3KJ xAoou4CyFfSohr+jpF40OwqXnCM9Lx6HNw7lYX78vX8opvczhMiJIKcaTqzAxmWwxhL+ LoHJFsKkD9NKEfFQIf2YBw/sm88w4U7dTKH2CUwlpkL9IKY4HLQCOYVw4hy+85uNbWdi 12lYZzLWyqMfD7KLncDK0oOPdTLsrfhqA261BZ2qxjDfMjmHgYPGI2cMfRoJNw9Ds2x6 mYktQqwLGZz7D4LLunBnyT+NyKlDjoyByplOD4dZM+Gy5Mukow6F/JtYyJnIg07gkJcI DK7i3K+J7iVwB7rIc6Q6+lVNvypwr+JcnoehbyiBEw+DkDDcxDIS83wijjDhGOkC778j JP+0l+BzC7BDEa6NLquRM28e5zDIxJzET8iYg3JFhezjmOsL0WcB+hQfQeEvih5kdNKf FnTZgZxN6FNGftIKaJNmxVKTDQUyluBzMp5aiYxH8N01ONpanHPNfrGbcjKskHvBCGkd RQrX4nf48yCSWGtxUM6gV06iPOS4HqKphmg2IWMrJFGXEL0rkUGbraRaxJc9yjlMLTEM yUcxUWsTc6HHJmRsxvCbkbF1I3y0nYFERmOH6AVKkBoQvY56hXw0sYEYhmTDR6LBRyeR EUFGBHtEULQBGY0QXXNz8tystxIdaHvzNnSg3gzgcdBLdisxDN0VrX6MNZz0IAkjS4Vj 1aQdDCgFOztFJyauR+SaRswO902i3ugOlh/oxexBRk8AzkJGJzI65940XgJDdaFwFw3t YpzTP3qZ/tFLZwD/6OUAn/8f/uanAV8ZcEbq6075AW6vNF+aA5ov3fvi9Tlxh274xHEP StDwjAd0n3/fi6WRe22Bv95B4FCxw4NuYOfqAaHuoR7wrcDomAdMA/faAhV7vRLZoXjA way1OnhOrr866I5nOD2g6758BywN/dsWOOEM8dTdeifLs800zmtFPy0H7rIGviIjqNc1 0HWLx7V7sDRwlxa44i03xjA8YAaWb9HQM3KMRmmgLcPecRooTzTELA10S0xh1upgOn93 FkiM9ZSLm4bwDCejwZd46itHDofGgL/hFM3rnA6osNXa7gUc1gO67uaZR7SC6azXAhe4 VKCjQcZokgbWYHh9jFxpeL5kV4smB874gwr6mQvcHFHBmGNSWMVc94pjWcU6OMDzZ739 HyaM+Rr6hrSxpYFB6UZGj4rePAsIq6Blyfr5KhhJVi9QQdehoFGmgTNMhvOoBrovb/+t Dg2cfDxHX6TcaukJmgU4bQd8QUU5MQfWYn+/wUuYaslSS8rUF8k7ggFLmIYeq1ZLmTop u1m+vMX9akP3ae6Pnn59OdsKaOAP+ebAsH6pogwbthupghbPnnBEr4KjkgQoTivotaTH Gd9UQJyLb6LMn6tgiWUYpnlDBV3HMm193N3hthUw/qaVdI/qdfUCn7x81BvQ52I6rSNz hWVY5jwVZTRsHEwFB+MblrBWKuh+OZqmmaWALsPO16BasDmDuWDqK+c7DJtpFqnV3RCL nCjVwFOO4zd10F39E63YJz5bH9Kcw3XLsEegSu2ZZTj4R1AF/cKWw3FKQUsZdVBHAV2G SPqnCg5hjD3OeRGZwtCqu5ZtWqZPre4uD1i2renkujnTdhy4qpW8L7M/1XtVmpPhN0Iq 6gsYNqPxjoKOxpwYebgCvuQwcLx+oaCSPoHjCridnRwc+FkFdB2GXZi/V8HcAB4f/JEK vpaVmT11kYq57ns6MIDyr37t2G61u+WZpo+wYaGCJizpRbbmsn6Tbwosc65StBj+NA0z UwFdUy5dOleGLHOQY6vV3VNBMxTMKFeru6vOfqAh6axugVf7K2fmZmt+GAg6Jv/lo7oN DpmWxYKkLn1fZXwdVkRVLJxs+4XGgNtMOyhsPYQckvfIsef+pFZP5+7WAr97RS05dpAv 6LOsHAWNOgY7NFPbruOGEGtgv1L0IaJ8Hg7Zp4CumeHkTF6nYq4We2qfprP/mgXePVw1 c0o4PFqtxbjJrfFsBQ1Bqj5h+xXwRWH5/YEMbS82rvQxbYVTKqUz98YCH7z/q7dVSXYg 4LOFo45RnWkNdvyW85ZS1gkNyVuw+YCCuZ5oSf04nftvWWDnmGFZ/iHBQEdqAzdsX8gX HJw5PRV0nYKjavSpfJrO/I8t8N1vPHX08GPPqGF+lh0cPaPu5CFFl/NKLp35OFpgyrCW ax9Hve6JTvJ89v/5uied+A+E0PfuaG+3NMHS9o6+RHNjU3d4dkHB/HBpR0csGi6PR2ZM D9fHYuHkR13hRLQrmuiJ8sB8W2xX5Jbpsvk3IxqXD3LzmL0wz0S7ym69N0401C/jSWwu Bgjj7w3RZTwhzoVJYXx7Z/NyHrrmEia/SLYzsZwHp+V7Y0ZzdznPy8j3Ziweq+RiR/L9 1PiOSp4Gl/INX0f3Evk0uXx/oqunSspM4i31FTxtLp+YN2pa21fJMtwMMDfsadrAw93y vdG9p2kpT3Yn37+Z2LVetptLme93xNbKttDNfJOLDDGuLjTzyH+UFOYawWqwVVyTkBc8 2rmU0cyfGGlFskQUVF4eb/2IsjEuiN2uF6PMiuRPov05WauT62PRW/UqRGWlODg9RUq4 4I2CdwuuF3yp4FLB26dH7Zqc+tnBRF1z5GdPvod0qYHUSkq/ra1sR+p/s50Iestr7jEu dkS58nG7f10pNfJT9dwZPz0q5bOwbCl6pPJGSp+jSnv5XDuLJq0grSEvxMTIRZOt3m7z o+1J6Wt7XxqR2uJ1+8qW1zOu7U3Vil54e3jT/rKHqeMRUUqmjkDUHmfPssvtQrtIhO3F dom9yF5Grtjm4vyH9qvGds1co5HSu0Q9vYlzESWcMgdSrP1PQFeYpgplbmRzdHJlYW0K ZW5kb2JqCjI4IDAgb2JqCjM2NDQ1CmVuZG9iagoxNyAwIG9iagpbIC9JQ0NCYXNlZCAy NyAwIFIgXQplbmRvYmoKMjkgMCBvYmoKPDwgL0xlbmd0aCAzMCAwIFIgL04gMyAvQWx0 ZXJuYXRlIC9EZXZpY2VSR0IgL0ZpbHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCngB hVTPaxNBFP42bqnQIghaaw6yeJAiSVmraEXUNv0RYmsM2x+2RZBkM0nWbjbr7ia1pYjk 4tEq3kXtoQf/gB568GQvSoVaRSjeqyhioRct8c1uTLal6sDOfvPeN+99b3bfAA1y0jT1 gATkDcdSohFpbHxCavyIAI6iCUE0JVXb7E4kBkGDc/l759h6D4FbVsN7+3eyd62a0raa B4T9QOBHmtkqsO8XcQpZEgKIPN+hKcd0CN/j2PLsjzlOeXjBtQ8rPcRZInxANS3Of024 U80l00CDSDiU9XFSPpzXi5TXHQdpbmbGyBC9T5Cmu8zuq2KhnE72DpC9nfR+TrPePsIh wgsZrT9GuI2e9YzVP+Jh4aTmxIY9HBg19PhgFbcaqfg1whRfEE0nolRx2S4N8Ziu/Vby SoJwkDjKZGGAc1pIT9dMbvi6hwV9JtcTr+J3VlHheY8TZ97U3e9F2gKvMA4dDBoMmg1I UBBFBGGYsFBAhjwaMTSycj8jqwYbk3sydSRqu3RiRLFBezbcPbdRpN08/igicZRDtQiS /EH+Kq/JT+V5+ctcsNhW95Stm5q68uA7xeWZuRoe19PI43NNXnyV1HaTV0eWrHl6vJrs Gj/sV5cx5oI1j8RzsPvxLV+VzJcpjBTF41Xz6kuEdVoxN9+fbH87PeIuzy611nOtiYs3 VpuXZ/1qSPvuqryT5lX5T1718fxnzcRj4ikxJnaK5yGJl8Uu8ZLYS6sL4mBtxwidlYYp 0m2R+iTVYGCavPUvXT9beL1Gfwz1UZQZzNJUifd/wipkNJ25Dm/6j9vH/Bfk94rnnygC L2zgyJm6bVNx7xChZaVuc64CF7/RffC2bmujfjj8BFg8qxatUjWfILwBHHaHeh7oKZjT lpbNOVKHLJ+TuunKYlLMUNtDUlLXJddlSxazmVVi6XbYmdMdbhyhOUL3xKdKZZP6r/ER sP2wUvn5rFLZfk4a1oGX+m/AvP1FCmVuZHN0cmVhbQplbmRvYmoKMzAgMCBvYmoKNzM3 CmVuZG9iago4IDAgb2JqClsgL0lDQ0Jhc2VkIDI5IDAgUiBdCmVuZG9iago0IDAgb2Jq Cjw8IC9UeXBlIC9QYWdlcyAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvQ291bnQgMSAv S2lkcyBbIDMgMCBSIF0gPj4KZW5kb2JqCjMxIDAgb2JqCjw8IC9UeXBlIC9DYXRhbG9n IC9PdXRsaW5lcyAyIDAgUiAvUGFnZXMgNCAwIFIgL1ZlcnNpb24gLzEuNCA+PgplbmRv YmoKMiAwIG9iago8PCAvTGFzdCAzMiAwIFIgL0ZpcnN0IDMzIDAgUiA+PgplbmRvYmoK MzMgMCBvYmoKPDwgL1BhcmVudCAzNCAwIFIgL0NvdW50IDAgL0Rlc3QgWyAzIDAgUiAv WFlaIDAgNzgzIDAgXSAvVGl0bGUgKENhbnZhcyAxKQo+PgplbmRvYmoKMzQgMCBvYmoK PDwgPj4KZW5kb2JqCjMyIDAgb2JqCjw8IC9QYXJlbnQgMzQgMCBSIC9Db3VudCAwIC9E ZXN0IFsgMyAwIFIgL1hZWiAwIDc4MyAwIF0gL1RpdGxlIChDYW52YXMgMSkKPj4KZW5k b2JqCjM1IDAgb2JqCjw8IC9MZW5ndGggMzYgMCBSIC9MZW5ndGgxIDU4NjQgL0ZpbHRl ciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCngBbVgLeBPXlT5zZ/SWPJItP5Asj2T5hWVh I1t2HHA02DKYCNYPLGKn0UYCQww1wTEuJYQvcVpeMSk4j6UP0pTtl2yapC2y8BqbKI0D qdf7SArptrvttluSL4+mGwjbJWya2taee0c2lM1839z5z7n3njvzn3PuudLgwFe2gBGG gAd5845YP7BL24aPwc27B52KLHQCcN/a2n/fDkVWfQxAbr+v78Gtiqz9DYAp0bsl1qPI MIvP2l5UKDJXg8+i3h2DexRZW4bPsr6dm9P92jdQztkR25NeH9AeOO+P7diijM+ZoXL/ zl2DaXkEn7f1D2xJj+e68H1mY7ADdsEgPAh9sAUy8JVxlB7uAw18GVRAwAyVsBFH6rkZ /F6O9avIsQT88Jf3iis/BbuWmX/p4eBSCs6df/n3s4a5pwxPaaklHVpQLpS0hvkOAGPF rCGlMTzFLKU72UM/CZ2pKbkgsdRXa044E3KiLdGfGEqcTMQTFxKXEvqpxNUEmcAh/X+f m1crBTlxo7SRtIbvDZOdndz3Ok91kvYNuULHhhxhQ0e2cOfaDmH12jphzVqf0IL3Wn+9 sDLgExoCDcIdAZfQFHAIjYEOYRXeMt4Bv0/wVfcI1f4awV/TKdT4C4QLNZdqrtbwE6kr p8eKW2onUpdOj5nd+Lwim8Z0Yu2YrUXYffrgaXytq6dPsxGfy6nTuqLa09YW4bHDWUJ/ X/8eIj7zu2eJ/N2cJbXyMzn2WvmbuYiO59prDx7IksT94gHxqHhMHJH2S0elY5VHhw4M HT72xMiBkUMjh0X5azpzrTggDRD5AZ2xVtzBOWc45z9wgelPponzp/JPCWziYJN5E5Fj J2NE/BLntVqECmux4LHWC+XWLGGpNVuQrAWCy9kkOK0rhX+0NQs2+xrBblsp2Kw+IRvH ZeHrZlptggXvfisnW1c11YoZ5RKoOdP5kGQ8F5L0UyFJh7cqGZKEV0MSPxmSyNmQxI2H JDgTks6fK5emXiuXXpU3Jl3S2UmXdGbcJZ07/4bptanXTclXf2KcPPuKcfzMhNGcHEoS eXJokojjgfHW8UfGBXG8EuFOhK+N/2w8Na7V6+oEo4moBMITwgFpU3ETXIqLZ4Yg1NkY z+LwuaFxVOfzhOI9HY0HvvENR/x4qKMrPuTontDimK44F+eOdse1oQ1pCB567RrctYuB W5o43xxXN/fG4mp3cBcVMqiQ4Q4iiIsUi+6gh4tbm3vjVkT/z8iuhQu7lE5lIYbhK7cs x0T6LoP4Rh6PukBtVV1VXRT2CRH+t5h1kPow9c78nvme+W7+aZAwQ47DSzAJ0/DWYtIk 4RzDuyEBU/DPi3oKHoWn4e/gX+DX8Mmi/lvwLLwM8UWZghGg2ufgRfgxnIazcB51h+EJ 1D4PP7xp5E44BMfgBJyEn3OOtP48sXLKG3wERnKR28UdBRtUQBDuwU3lYTiI7zXDrUNd A+raUDsAe+BJ1E4C3Z1uvRpwp4nAdrgfRnHE66y7HOd2Qg9qqU65HoC98Bj8LbwAr8BO xIfwfb9zqzGUHyUu4sLN7X2c+U/c35Bp/KIX4IDaijscqC5SVoUI4xZS7wDM96Q+BeA3 kWvk++QJOEW2wzo5O9xZV+urXOatKCmWrFmZFpMRY7LCGeeLm93N7ljvsLO51znsDkaD 3gqMv+ag3eXq9lY4UR10xrmoszm+endv3nAzHRDP9MRJcTO9t8flI1EE7qDL5cKerBs9 uMc9flOXc1tcjsXhiHO0Ymr48QkzbIp6jD3untg9XXE+hmuNAr5ML0Y8Pugd7XXGBTTM Gjtq0q9I+3qj2LqDOOsL9ajWNXUdck3Z45n4bI5bPPE1aGnN3vfs/HBz3jYnFYeHDznj J9u7bu510THd3d15f0HDavfq6PDwardz9XB0ODaRGtrkdprdw6Oh0HB/c9QZhzZMTtSf PWKPr368O26O9nK3I3v0O1Z3dAXsLgu+qstFv/fIhAybUIgPtXcpshM22RMgV3q64yRK e6YWerLDtGdooWdxetTNuG7q4u0EDYc2uEPtd3c5m4ejab+lNbcpXhwl0Djq5g63j8rc 4Q13d01iYjoPd3YlCEeaoo3dlEbS1Nl18yycSSMA6x2WODUPM8LP8X4LeoVZmMS7T0jC 65oZmOS3w6TKjHFNqy6tllgcQQ30fNACy9IapmbNQiW9obkV8SAsqlRppManZlGrFGsd kyV4gVtLQrye/1j4uuoH6lL1r1CPey2dgNN4fFjPqIkA9K5887dvsmZ5lcvishRjg18H nw+p4M/0CQjod2Beq1Zgbhng3QTv19JynSOKJAxas5ZotSq9hudUWh2xmrBrzGTCnonU 52NmMwPXxgwGBj6QDcosnY7Jf5CL2NhMoxFl0WRShyHLaMR2j0k0yaY2E6/lraqJ1NvM FIJZZgrBrxVTuCiaQvkPbFUEV8aoMQaoPQrGqUlVn9F8fWG7nvNFbgieOR8EVgZWZtZX PuC5jEy4kQq3pRrbatWK6bkl09Pkw2nyq7lS1cW5CdKCpx/kQziOfJhxn/mNXHW7tcER soYcbRmd4hZRs6QGNGYN0Wh0eTV6XqcVXZKLWLKdUAUy9KM7J1KLlHwmGxgZRkYBHjgW 2Ps9AiQCv0zOYZTsdImugIss0Vh1GZRY3QIbCNJs6BgbKCskIFBoQaCYQ/A+Y0PX57xB AGXjmkJN5HIEAoHLSEUEmeAinFWtUavdbuTD5cvJzcnR1JSUlpSgotpXW1dbKxxvvmP9 2yenp7mnD55tCUfeqq2reuiv33hhz/HKylJB3PziHevXz/1CddFbVf/SofUDRZJ97kee yqrtyGFv6h3BIeyBEqjmtsndRr3gXqLPdgsejAZ1uIK1XtZ2Z7QX3FOxLSPq2Ol9SL/X 2u94qEJPtGUNVRbZQiwWp7Y1n8vPzws4heWrtHpOKzo4h6XUL1Nm8RB3bWwRUJapRuGU 2MBhADUL08zFGLWwGM1jbrGlY3mWRRV64xKNO/QLezHqSDlbp0NZCd4n/KJf8gf8/LKJ 1GfMkwiuySY6ZZmWftcyuwEzQ66jaxqMdE2Dlq5hyKfrGYwMq+lrGnKoYQNibPU0GAwH am44zXMZPRYxf7AQ0h6Ppd48RyWfr5I5MXC5ujJiYa7MrF9eFcEuLuJy+9XMpYUl/hr0 YFGdX/FooVrj9tfWVjMvZ/MWNirbmoN+FhyvLAmXVe7t+ObbO7Zs5Qqe85aX9TfcOR7T 113YsvuUHGh8ZeNHwfaewa9ufu6rlobMXGnmxCPf9XqdWofcmZdrLi1+TSwqrVz2ZN+8 g6tTWbNyY+FobD3GwCTGwAhW7ixwcpny0hriF1dkVzmDpFkMZcvOuzLvy3xE+1C+MUOn zm20CEauQFbrDVor9SnlhQKMZxK22tEXV8eY5yhIO/OabGAOzEhn18cYCSyplOk453/k pSzPRgqlwkAhybDrjNQROpaPNJ/UDGNrM06k/pPtQwjeVbYfIxuM8hXmbATXZQNdy6im M1G+yt4TwSfjdB3jYdcNF6a9tZh81KU0+1gCUo+ls6+wpFRD9yPqnUy+psSNvrLkMM+M tDateWnrvUebjfFka2Ln9Pvn9j/V8YOWtl1rnxkldY9fWtfa6i2pUVvn/nXVhvkL8x/M /GzNbXNDRflvYk2AvvluoUnYB26o4iplb0PWHeW+iturgrpQ1rryxopQ1Ze4iOrunO1c n2p7zj5Vv9NSqMp0ZZfJBYJmYZ+nQLbT+NZoDDJvWrYqWyOqObWryCdTb2RSV1FGGaDU UCDrqfcybaDOQx9cl9tZCmqZ18zMiYxYKGd+w+DH8mBjeSfSielse9In+QI+4sHEImEP ekwd9tjzzNRK3kRqXi6mlvIwjbBl/sxTU0sUq8MUY3tg+cLWR9mPmN9bzKe/TKfLNJs8 Hro11gfQNcVmcKFf/NXMD7gT+jGv3IXEQpOKZVFdNW/BnbJQnU6ipvn5+WvdL3bol830 RB92uwvCJ/bM/Ff76lVn74l9fS36L/SofCKx/9sdzz8y/9789SW5U5n+ZUtL7w9uDTZx Lk4zcnHdmtbSsqrZX5JYoePCdPJcACsznqeFU5hBBvhcDpaRf+f+Q8frONEkcQ4imbxc panKIBs6DdvIXs5ICM/ZcKhWN0YMel6vJSq9SsNh6TaQqL5fT/R6tqWVMarB5MQaTLQ8 8wXPGOQFGtcUY2vD4qpkBIJ0RqjYYJSv31yHWUFm1R8LMTOBI/6XpQYFLDVUj91UoD1K bkQsSDruYvX1tEIHkHssSofMU1N7P80TphDNaae4yAMuN6dhFZur5oRTn83Le5NJIl2e +xP30eD8EbV11kYq52aRLfytocnFiPdwM3KIL+KXZhVlLQ06gyVnyjXjxVyx5MjX5jaW FQoOFWfO18peTvJWeWVvm7ffqwIlJhkPSCHdSBgPYPPSJMil0eaVaSxyeTSeORbPHC3C tAvBNdlCg5GrYoPyaTCi9o/YrabgquyhVjlCI5OLmYsN+SJbE4MerYpsTYrVYdFmZnsN XcfM1kH5bZmVLnMJHW1mWYPaz5h5Bqh1BH9mxCNIyS66lFmysWVsbBkbW4Ziddhmy19w MYL08S2fDUZZcTECZfejQKmO+Xq6Esq0bCpAzqAr5ccks2weMvPmykj6xIH5pvjavJh5 TLbU1y9mJu2h5c4TwcK2EgNh5ZwPC1t9BBMRT2uLBSwnJxvDYFGkIZGtJCNrszW5SVN2 7sb21mdbeUGB60+sTya7Tm0e+F7pQPLLE6fIvpaDZZ6K1obchoI5P9l354Eyj2fditmf CJF9azui4Wj4d3j4Y5HED+DvzExIylYwmTFXME9EzMYmvajSaTGPPlgsR3IZ28HAKlv7 rcSoYQRqGNsaVigoxh3UhuczJaEQsJMGrUSsHi2wjR1XZAs1pyuiYaNjVKP2M0Y1gj+x bNIdzrpRaG7OJlphkL5K5I9WGDfbolj2pKniB/TlrbV3fT+UTPa/3L28ooIf0evWN8x+ KESevzukwp8dHIxi3b4Lvz6Hs8tWg3qJZq+GJ6psncrSqMLzl/ULD13XWfGlXewIiocv +i0OZetnmz5GHm70hTR8AZMAWyV5WMm+LleznDNRqoCFEzyZJ+VF84iBcYSFFycaWPhS jAcmG/5UeZfVbATp8NWywSgr4Yvgj6x4I/hvJXy1zASVGacIPmWcag/l3sSpEpA3KZQw ZgUcf0uw0GQ1ocSPtdtKD8/0zGypFu5K3vvjbfE3kmabfWPH2h+FkvtCbf92gfxibn/4 QU9F2boVfCP9zYY3Xql1+O/KF134bwd4oBr/g2mGO+Gv8J+YMOC/v3hxGJXKL081/fN3 TWtXS2ijZ+OWgZ7Y/bH/A/BMsmwKZW5kc3RyZWFtCmVuZG9iagozNiAwIG9iago0MDI0 CmVuZG9iagozNyAwIG9iago8PCAvVHlwZSAvRm9udERlc2NyaXB0b3IgL0FzY2VudCAx MDA1IC9DYXBIZWlnaHQgNzM4IC9EZXNjZW50IC0yMTAgL0ZsYWdzCjMyIC9Gb250QkJv eCBbLTQ5NSAtMzAzIDE0NDYgMTAwMV0gL0ZvbnROYW1lIC9HT1hIS1YrVmVyZGFuYSAv SXRhbGljQW5nbGUKMCAvU3RlbVYgMCAvTWF4V2lkdGggMTUyMSAvWEhlaWdodCA3NTAg L0ZvbnRGaWxlMiAzNSAwIFIgPj4KZW5kb2JqCjM4IDAgb2JqClsgNzUxIDAgMCAwIDAg MCAwIDAgNjAzIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgNjAxIDYyMyAw IDAgMCAwIDYyMwowIDAgMCA1OTIgMCA5NzMgMCAwIDAgMCA0MjcgMCAwIDYzMyBdCmVu ZG9iagoxNCAwIG9iago8PCAvVHlwZSAvRm9udCAvU3VidHlwZSAvVHJ1ZVR5cGUgL0Jh c2VGb250IC9HT1hIS1YrVmVyZGFuYSAvRm9udERlc2NyaXB0b3IKMzcgMCBSIC9XaWR0 aHMgMzggMCBSIC9GaXJzdENoYXIgNzIgL0xhc3RDaGFyIDExNyAvRW5jb2RpbmcgL01h Y1JvbWFuRW5jb2RpbmcKPj4KZW5kb2JqCjM5IDAgb2JqCjw8IC9MZW5ndGggNDAgMCBS IC9MZW5ndGgxIDE3NzA0IC9GaWx0ZXIgL0ZsYXRlRGVjb2RlID4+CnN0cmVhbQp4Ab18 CXxU9fXv/f3u8rv7NnNnJskkk2SSTCAhA9mGJZCw74vIJiqIyKYgiOyLLIK4ICiCogIu f1dQEVEL7nWBLmpdqFpbxbW12qpVW60W8r6/mWAjbV/b93nvJbkzc2fuvbOdc77LOTcL 5i+cJljCakEURk2cMm+6kP3pu1oQyF1T50yZl1tXPsD1T6YuWlCcWye/EQRp1/R5M+a0 Pb4C1+EZs5e27c/eEIQe1TOnTTkn97jwN1w3zsQduXVSj+uymXMWLMmtK1fheszsuVPb Hmf3YL16zpQlbc8v4PmE4vOnzJmW277vKFx3njf3wgVt6xKuh82bP61tezIB6+sEgsuA 1gsKzQhMoIIqtAg63tkX4ieChEf541imzdjy68lO058FV/wI9wh7t+et5tdPVdwsHO93 /AL5DcnHdiKOkPvBPuLo4xcIguId79d6jTQme6S2B7NXQUuHaDQSCYJwOBTyfc9zXcex bcsyTcPQdU2TJFGklOBI5MoDpHV9/5n9/o//9ivyAKFqv8pwuU+WcZx9jOHy+P7Wo2Qv Feh8YZQwUBgkNAm9hdFCSjhIVGE6PtObyNu4vJtcReaTT4Wl5w+evGTv4L/d1Nga2tez +I3V9I7uqVc2OJeWXvDNmY9/tmTwbfPeC9btnLryKemKrT3qP71u69Tlbxy56bvuD1X1 fPOtpQW3xF78bNbKd357w85ls/v+6OKPX3m+U2JOr6J31mye0t158JHxlcefmTXAuuuR w5kV171z37OhOb8runBLXt3Ga9jj38399ciJ73f+9aPzz/jDazdlOo2+9ZvGBa8WNhyb 9YRS88ShiSuf15deE6z8cM2H9x+5uPyl5aWnz/2lve7B2ZHX6w68WDL0yh8/uKFyxtWV e166oNfTk29+6bTFfzEq9w4/44PntndMTnj2o3Nr9y54KnruWw/Mf53Nv6yg83W/6DV9 a/f665dt+ku3bUenNr806o7X6nsf/fa8N29NHpv60kOhjWNf/Gblm+8veObYnCuZc8Ud qVG3PmtdWKq9/sKQBbuffeaxGYP+/Msblyz/eXDXs3ZXZ8z76y67Rx9w8wczl1jOXWv7 sed/W/3a3KXk/QcvTfY6+tpF/e7bet9jL2ztcvuk03710bzzPmY/+1Vd8aJVgwdctfnp g7OHB0cf7Z356I31AxvuvPrtc/v/4YNrLl6gFvxt69wK+4GDY8ZfvuGKPWeF9O5XHqkY /9sP1y7rYB++YtjKjWsev3/zmjeOhBtXLSn6/fqfT5/VIf3u5urVZy698fYtf5rxq+GT 3+ty6denDPk8s/oNte/D740/8vby+79OXvVi86TXvx1bdq/+txUv3HLhEXfl/s/H3Ph+ 5v7tZ2/crX3Qa+Ndzsg/31/yaqwu74w/vrlqYeyPW148L/PA/N8P/8l1hYcrZ70dmv3Q n3puf7l+2G17z3rtm+G7Put5+bvRTR8XT3ugYMADP61quGbBk/mJq7S6W37R23m94zkv /3Xu9k+GfXnVl7c8/Xn0memxu5b/9r0bV5927tHynZ+SKrv31ze9fnffc4cOfuH4kwvl sT3eeeOs5dIfNjxqLtnYcfsX8mMd6n/z+rJOty09PCr17qBtn6aW3uxPfPqaHh9rJS3C jf2/e2fbJU74loNDRr31fN8V/1P55s1LKt5/bk0HfcDxj65a5th3rBm/ckPTnlfCE3p+ 99riseM/+WLJ9VZ06/6lPdb3eFBfN/nofdWfL34i79THri6605327eDUobFP3pzYaUy6 pv+23y9e8/jgLV8unfKp9tLXk7Z3dPY95ATV3Z9+bNT4L3+xufPAU5/Y/6fLf/qGTM9Z /sSwLVt+/XGyYdW0EWdcdt9PN81Z5eR/9qNrh8Zf3Hf2rBfYfQf6Nj7/7PwzvG++Kq6c 0s968MnfTFj0ylfdL52c+HbL/EzR9W9/+PaRz8qbLrpw/6nXvnVw4+QFT/7s80tPrx4w 9dv4U0+vGNdz0QedL3ul1P04c82bytDd+1e98uVZ13675Ghm3v7fTdhy+4o/JSpbZ11V FL1nU2PX57cXvjuj7JHFx0b87KqVT+inr35Bmrb3497bvlg4yay54u7acS8fnPhazYRb Pjlj1defzfrF2BF/7Vnyi0GD3ntvTVV0xG1fdV39K815eeRfq1/aMuXxSNfbXq0Z+uye YMvsV/eO/rG7ZPcfq9Y/rS178NPIIzW1Dx0Zv/yL05/8qnbZW2rL9o+dte8ueyw6+vAb U5bnfb379eb5z+e1XPdUXdmjXR+d9+afZl96T9HbTyyu/mrjpltH9H/3qVcFDRVZaCFC 9aL+xfeL5f2TU2bGkjPP6teJfCU0C62CQEUhST4UMmSpUER2tH5CJwgJGghpMkioIQOF WnG90J3uErqTPUKcHGl9lZ4ulJINQi05hIp6VusH9Fzs+2nrd/SO1mOoyLlaLwimoAiP YL1YmPAPNbt9/f7/c5sCU/iPJMh4XQz4xH80YJSBV2oJtuAIruAJvhASwtnHegm9gF4f yf2Ub7SIfrmx0bzEoe5G71Do4UhZdEDstrwXC2ri1YXXAqua8bYfplfjOZgwtiVBVLVF o0xSiKDIikhlSRWYohJKRHwUAxalMzEh3ZRuco/xP5JuOoaVY01Cl85k8MyDeHZ18Mwp 97eMntDVyxwU1Nanup5W55V45XVespksPn7422/p1cfmNlO8UCokWz8lj5DXgMHPkX4t 15JDh1oOC48cvHf1ggsbO1WUP/3jRx/Zd/89e3bffdedd9x+2y0379xx/fbrrt28af3q i5YumTd3znmzZs6Yds7UsyeMPfWUkcOHDBzQUF9X2zldWV5WkigqjOfnxaKRIOx7rmOb uqbIlAhPPmXmxWJ5N+7Qz5k69ZwDB/WumUzXG+VJZ0zcePllbMmmK65kSxZfTNfSFbRP TTUhQ3Jvubmpqbm5OZ0+1sTfNn/fTcc21NjP2n+znz35mtT8P/gR/n7Mto96iHCo/Ud9 iH/UhEUdGg07JMD7ZlH8JggL+A2HRBnuSlCWiUSZ4pAUSzVU9CSpTMoxKlIVqUyaNtP6 hoqG+gyWZtJ2I9OYSWVwXyqTe6yhF8lEm2k0iNZG63FfAqssLSZxuCSeW8HTs6gSsMCh Acu9BNwRRPlxmqXGZlKXyvQizTTVTMkjumrJImVUJEQSVVHRmKzoNtEZIa7FDIU4zPJk PaEoquJpqp3HInJxacjKszpRmRBFoQyBqTCFEFNUZdMiEpGkUlU25JLhsSgjQajMGiWp ibBLVS1fjEqiz5ike9RQVcqplSYxZlBJlmVqiz1wJENRRZtqcp1KGNXINivmBLJsM1Fi iB/JMFWbqa7rqiwq0qjEJJHi4zY1hzFNVsq8sKf06BiL50uRqBmywhWSTvxIB1ExLTso IXhGSaEKc0VZkagomYquq44Ucn2JgOcpkqJXOngtjsRkR5REEpK0vO6M5olURCbKxNFj mm1JIV/3NY3assnkkKKH7JBICTagrDP1qOJ6VDLlfo5kGCgWVMi0/o7sp3cL+4Tfkxkt D5KPP275RHjv3aNv//rN11979JED27ZevHbF8qXW7z/63W8//ODdd15+6cXnf/bg/gf2 3b/3nrvvvCObeVuuvnjtwgUXIOumTJ50xoSxp4waOWLYgD7du3VtqO9SU9WhIqnI+DCI 8NSPzW5du3Z7+hlt5YoVKx98SNt99927Dx2WyzZvkq9fogwfggxNlZeqZYP69W7p1dSj Tm1UZvzqDeUIT+azJrIzPTUyOkLyoiHRJ2GbBCYhI09kYXNTc7SpOe0eI+koyo57LNrE byAvkZYnJ+KJ9f87CdkuA7/PxbZUHCl83D4VP+apWI6sQ+bxlECu1acJSzIkiZIsTUsV aZJJNaR4omXqe/JswGqG35lpaGyox1pjpiLFb/HEw50Z5G2YnchpJLOCFTwBQg/ZjOts 3jcTnmbIsaBZqm+WUs0Ef3UJJQAmhrM7K4wXRF4L0qQhLTokiZB3JH6PQ8PRMC8aDuU5 HUTrMo098IrqGvH0DZkU2a+JUVUJW4Fpq4hapoqqFVii4sZs2WWeH/CYFpGBoqyJMpE0 JiKmVSoj0ZA+susmfeoQpLskMWIhaZGAoiwSWTFwgxAiWhx4JJNKyAxGFI1aKNmOylwv EVI8ooq6wiRTNKksShSZ3osyzYrIkiz5qmVKum4pBJlJNMWkasQK+1LMVVAZpLgWM0W6 Q4vFTVULRUyT+Jolq5Kmq5pJeNkJCOvEVF83DZXixST0HrYS1yKKrRdXipZs2CorZKIo oRTpGtONUpH6gW44KAeKKEmuzxQrXzGVQFVdDe9fDUxFNSTDKRYTAXV8F1KOmhaTVEtR JVGx8aFIlkFiOglUh0nFouwpiiLbakSXRbVKc7v5Xk1JYZLKPjFChmPmeWHNjSTx+eKp fZ+XAyaqtiRqopkf1iPRfJfnelHrYTKDvCBcITwgvNwymezf3/Kg0Iaht26/duuWqzZf fhkHzSlnTR4xvLlXprE+VXHvPbduXLb0wgvOHT9u7Kmja9NVHTukKsrLEkUF+cJspaR4 FsJ2+QoWHuqFfRKNOKJFdI2Q4u/TEknZlEZaZhkBCMH3GfJPbvyzRPrBZm1ZVSzsb59V +7NZFUa8yinkVEWyNJtNqSTAqyFNGxDrCNMMq880IG2QPDy7Ij1INiPqM3XNBNCVXWms q41mopEER0nEu4gcqMqmZ1JxRNwV5r9BGFvQIEGaZWQU9utBIiJP49IkPoxUadJR8Coa cdhopg5/Dc1iI5nhy7pi+CHdMBAWimXa+L41T63QTcVEXMsAFRlpB0blAPjw7SkqVWUx LKkI/AKmMpmZphcVRU0PkeICLSSG3JhPkUcEYUaQAoxF1NKwruhynqSJYdGOSKGpIvKE GwX40zXZM1xNROwBtWxVMWRmidSio03f1UlINH1gp0Z8k0iyEhUVT3Wqy9zAZSbVdDnm WcjPPGCpKcl5niSFZKqKeDeUKhrQVVMsyS/RS5T8wI3anFapSDEvxkTkHp4eKCwq4I1U ZEh2vB9Z8uwI8UWECyGA64gaVzyTMdxhSo6BdxqSQUFbPzn+PvmUPiocIgNadpHDh1t+ Ijz1xMEDDz+07/49u6/bejXo2YZ1F/fv17e5qUdN0nNMAzUIOfXg/nvuuO3WW268YfPF qy9auezCC+bOmT39nLOnTJ542oRxY0cOHzpk8KCBXYsT+Xl+WN973317hw3X1qxevWb3 3fIhYU9ykux7hUXyrp2nj1ZWLF2yeJG8fMQwZeGC+eKqPr16s4b62s5VHSor1PJLL1nP tl376EyyuY0EpznvzaIP54McjXIX/Eb2EVycQKAT1/8FEv3bLMmmTFuqbBYOt0+Vw1ku GOZ4U5FCqccNHrMBL/YiCjynaIjdTLQxU9cZmIF84EtdDhxSChILCZECOHBwyf4CNpTC HNgAiyKFJIcWaTHLGDl+caDiUMWTsDGTTLXBHbAOxwakIJMoxzNcNjZUdAmDRIJn0rra niRTh+xCIkX4y0hI5NOY6oaponiKFTC4YKIOxNPA0kxJIjYlBigiuJ9YCijQTVNTFc0q EzvlG/lIHyNuyyqCW7EALrpIVGzMZEN0UemrVZOGeQUVdeQUGB0qOtUly4l7hbrMqkXZ ipKS7vGYBuUjqsQApTHBQgm1ASlGXLVsJkfyqaJLtmnrjkTHhsOylnJ0hLOqiDrR3ahe oBha2Ac0lHlSDK+YIDEojieKJK4aFtANKcl0gICXX2nbEbxY0iGMfPDCbpdGIDXyFcle QV3mdzV8JLfnGZFmatiWUa+UqQnT6yFJIJIkbHgpRkF/rUIzbKiGWSRbtqaojXJEFj1V K7QkXaIyMlMK9LATh4RLHP+S2uQpfCxzW7aTo2//8sgvHn9s3/333nLzDddfu23WuLGj R40YNqR/n94tzb0aOyRLfe/4d99+8/UnH/3uw5/+5PCh56DGrt224ZJ106ed3dLcs6lH 9261XTqXl4V8aCwD+U6FceO1wni88NBh7ZL16y95/Alt86ZNm/fer91+222333ufdted d951ztRJ4lmKPnOGIpKHHmQ7b+pYmfqU7tpJly9bSci6E7ACUGmG0sriygEipw8Q0gTh 1ZQG7TuRUv/u+r9IuRwIHSBCzQFCsbCa/ygJkYMHCQxjrn/XEa0tEQ8STWjpeoBo0MFE O8EIOQvjBCzgjIskswyOZ6WMnGMBsg7pKkKMgQA21nXm3A4IlQky2QxqBBerjdbW1WYi dbU8YxiHOGQezz0cgh8Fh0Cqp+Us92tsABukjRBcGYacRKaD0fE0T8jAPZKmALwcBRST TjbPIwA8FAO+B+QdR0sxmj0EyGma8kqRECNRpD90Ja8WfKfsFbXBm8BFAAFIL6gZWQZU +V7IMdVwnqJ5ZioW6EVhp1KhoqmranmYmmpNlFFiEt0sU7hyQ8oDxhCtxQ5yXJEBepKq AjYUx8+Px4p1x7Zc1YnEDVlTJT8/BATQKQpEBIc0JVNlpXhaw9RAx/Id39AVW4KIA/oV OjbTZZfkAYgVMERkkuRQBCyEIwEYebJWqURKRdRHlbh6HqRjjJqurAKfRd1l0HcWclKm hoiQ1RhqkbKi3BQ1DSLUHBfi6SjCMGHMNUBJLexBDVBUQD5Q2ledCskNgemCzZq6LTN8 VmDLEp5dikct2zIL8hQzEnG7ePGgGPrPKvQL80KmxixVBV8gJC8vqCwsNRK+5XdQNc+y dCmi4VOGvrB1HFe1qQQeboSlGI6uqg4UdYhoAbODsFmlGhWmqRtUkcA5LB+4rFBOf0sl MRa3jIbAdgxb91XN0iOar5lMcX0wfOBrXgjflQmE5u5YuvU9coTOFnaShpYdZNeulpuE VRfN7FAZL4jdeMMl6yEhlyxaOOmM0yeeOjRXQHo21UV829SEklKTGy3LV5ibrrxy09Jl eqKoKFFSqs09//y5p4zWhgwePOTC+fLmSrnLddu2XrVZvlpRokE4BOfGYviIr71my4xp M5WG2i5i/ZjpynBOXs+cMH6cOlYZ2K9bV2nApXTEcCldQzrBDyBD2+pHM8pHU3MWmyET 2/AZ4NyE8sGVo3vsfyMaT64r/66OfF8q2iB5qLCrPSTvyrFXoDDYKshpTuxlAZMnL4UL k3TEVDItZhEyKwnxaFYopkXIOCReNkN54uV+geMA8YZm1gDmm813Lvcy8G4g2ZCpSHMR 1QNeTJRxshuNkCNVga5Dl9k6kg3hxxAPkSLTFw1DNnxgRRhqwvcc1zIN7o+owD8ZsQ08 QtKgvIuy7Bi6FjFsZAo3XwgqvgyEosgLbMi3oQYkpU5MnRJfphoQmRi+m6+DsuHoPDCx JTEkWa/07JQNu0MMQgEIp6+VVJWYeUyrIIUmWGwyYqsxv1xJ+GHZLWvwwmna3wrEYsOW xTizCwu1ApcVO4W25xo+/BVKS6rjpmfHwlJPm5U4BSrejmqF3DByD0GtUldVJV30kPCw V6hlS4ZiR0cltSFWOBJydEMVPa6halo/Jz3pL4W4sFjY1zKULFnSsrRw0cJ5c8+fM/u8 aeec3TltnDV50plncIY5etTIQQP69u7V1C3TUBiEPRdCU5h3gV5SXFzCVA2NvXDBcDp0 CO1CSMOJwASskf8ywL6PrxxKtUVZg7CkfZQt4VEWAsQojoJgQBw0U44BYiQUhs7JSh0U U5oEymQtPlyAkFWkehHO4xKcqkW5dwh44rVdyZqHYG4k2ixHASB1gBMIodfdkO3oUd3N d0WDOFDdpgQrQO0zXZVhBtiaZbIIZY4rs7DbIUQkE5JXRnlH3QMbEgtMUwwziTKG/RA6 IEgOJDuKMsxARcdmkAXRiGTQ2xBGUdG3FFuNOnk+D1xH0mhYo0rMPfZCVOFVTxNteEXQ TBRfc1GqizlBcmwtHlVYKCpqBiwHTY3BXotIpqFJtTZKUkWelWfrUd+E2EcUwDxDyDKU RJflal1t63ekD50nXCjsbhlJFixoWShcMH0a6o5r44s/dTT3v4YPGzSge7pDZaqiLJmI RgQ53ix3bunRJFdNPF0pHK+UFMXFwiEDeyl14EidqqtYjd8okbK2KEjnVENWNqAQ4S9b kX4ghdutfB8AbV99mbCg/Ve/IFdg8O0qYAGoM5wGUBQaiX/RjVweo9DkNHK2+DQma8Hk 8RmhokDvclMoB+jcWOIWUw7dWQSEAaEEIc0DqjFTn6nNFBHSR4Vbomn4QsJQh5LoFUSZ rxjcpMQXLMMDwdcN7CUgA6KodJRdnTrcW6EySgbRUH9ghKK0oHYQhAbgXrMUTQE1xxtw Zdtz5LAxx2AwTUxJA5+3ZaLD4xEtNayVR1xs1dHwnTzPCdtaknWsUosK9CYdcjaQQgoq imfLhgIHFAGlFQdBqFqOxhXiGbZjyTJeKIIOylaSTHB60eH41r31mLiU7CEDybqWVeSL 1195+UcP77/tfxZcOKlvn8aG6o4VIUP/+i9f/OmzT//4h4/hlL7/3jtH337rhZ//7Kf7 7t191+23XH/dtdu2XrPpsgvmnT7xtPFjxwzj4rO5V5d0eVmytKRY+OJLralHjyYEz3Bp pDxL3n71xnXSFfsfuFO8Tb5hwyVrpfU3qTtW9ttByQ0nYgQhcQLAIDEPkBKQ4PwDJHGA lB8gHQ+Q6gMETBWYlv1px4sPkhjJ23CAxGrsAyTvWfsgsQX9b7htPfvvoKxd0P3jzQNE xRPqWCJY4ljKsFT8K6p8gLh4uCuWbli6Y+lXkyXMg3OE+QYy6HvCPChLmAdxwjwoG80I ZMQzCC0DNpZmo7oCtiXaFbAvEc2ZFLMJQp0hVhHInENndygtJJzYsgpuC7X5rNnrXBJU pBx+WBsGBS+V2KsUT8KfpzSd7XvgwDBgeyLcU1mGzZ8Jd3ESHgnqWQrZwf94uqBKBlzE IjdgAOUusTF2w70cirGTFPCa+n1WcfgGs+fZ1IB0attTLm3ojOJa1zkhcRMKLZRcGWa1 vB5nFXeCQQdkSTm2A7yLS2UEt4wkgm0Eqio7umXbtmuLCHuwWvRRFFHWQ0wmKLFghXya xJL1wIJWMFmsCI+rIN6EyCq4uwiqC2PZMmOUI2SIKNjOtBSdAPxF4tqunK9yezcrZuGE UhN2LTeTwlHQBU+EOUTA7tHKgasLhxNsmFoo/TKeP4QN5TBwnwIxaBidi0CXZFsDnzY4 JHB1DGwo1iWNu7ISCDmcLBUdDAMwoaKh4eN9EeQtKjX3frkjBVfLUBRJ1iD0bckjhqeo mmEpi2xmyDpeoa7BZdVkBiIumppl6HFXKzITeSwv0KAwOPCYqijbetiK2RQaxtFozLCM Qk1PQGKIimSEVF8rDDxNdCXXKDJ8GLSarYRh13pw8VRqhyRXtdSqCEUrKeS6gRygyYQ2 EmFeyCy0I46fwiiPL8aIp0o1KebC81NtvAyq5YVCiuX7Nj4yyYNXETA0UACRzCWKKzGb ooWkOzHPhPxXZJ1zL1u0S6QkdqMogNzqgwXHHNNR7BhaSy48AebaBiJOhr+NRhhcE3jk /FvRoc1SuomvBbRN1Q3ZNk2023J17y90N9kkPCv8qeUC8txzLYeEJx9+aO+996xaMfeM cvRPn+Et2F07r7zisks3XLx21UUrlixeuOD8Oeedc9bkUcPhr/XrU12VKMrPE/Y9oGN8 ya7soK1ds2btlKnygKRy800zzlXGD+srDpg5fZp4toLaOG7M6JFD1QnXXdtMBuYqHdy0 dnWOO2j/hJv/54Xre7DMFbA2yBwoPNceMp/LQaYj5eqMlOJ2GX57ErBwXgnSxJHhDwMb eXXgtYXXjWz3pg1Nswyd14j6Nv+riBQCSPGLfM9WCbQ+uZDmyNuMjECxwHVDWzeVS2ve ooEcDxJilqRDgGdKo7V4IIM+a2OzlOBWG90NbqTBEDIstDu0/FIN3zjaDR7cH43qILOy VoaWqUrQnNFCVDO0CLoDSKs8I2w5HGMNFeRKUkF3gLcclv0EXFsJW0F1o+speyBAooU4 11JynFqag3y0SVjzFQpNGAONZygM6I9AkIk+iBgS0wCREyGHCaIXPCtUoJFbVcsLoyyF bTA+9CiRegzlxA3zxo0filrjPBM2XUVgu6qcgHwPKzQi22EZwkT2UGgieTGLWjF0TKOS zdwinbvJRC023IIi2YtoZlAJcDd8lIZA03TwBHRMQBFMXalWQGkcT5cthL0NBUN8CusZ RQ+CRaSBHonFK8qCfJjiYQM1SiZIZNQ0FBUGmQrVXohaBAJIhXjrG2QjuVM4VbhReL7l DLJjR8tO4VIYXxdPHzYUhL+lR/fqquuv23rNiuXLll4wD27z+DGnju7eDVSvUxnAXpaE q65Gr9ayFy9aKC6Ry5OpuXK6umbIYHXgmtVrWf5AGicF+YSUtkE9BgxyFDA3XZCTqSdE wr+J/JPCvYZDLLejSoUd7QN+RzbgOdcLHD0LeEoS0hNiIItwPI7hCPMmJFwlmEedcc0Z P/xZjn2ZBKmL1kWAZzxR+C5Iloa0VJoC50S8ozeTGzYIWELkYoG3WXgMK5Ah/OBkIzdJ 0E1Q4dWYJGSFJDuqW+jbWbZX5URkB/0CCukISagSkQUgipJD8mx0yjRNNfJtQ3UQXpQF oW5oJuCblSze7dMCVaFoTYo64Mbl/UeRoK1PNFjB+bBmCGQglGg+DRsugYssS8rHfj4E H7ONPLweTzGZLxNwAVi4SIlo1C1F599WQpoeZbYDgSHZTiE4qxfVtACYqVtMNQNZKkjr PWSmyqEQOj+SC0IBKIVBZcj5im06QB0NiWcWyHHMdWKAgOoAOnBjbMDj37DgZJvAOh5z ra8e30/2kseE7cLb5LyWB8jRoy3vCL967ZdHXn35Fy/8/KeHn3t239777r1nD4rxHVdu XLN6xfLFi+ZfMGM62nkQJpyPhICShv7Wb5595umnnnz80Ud+9PBDO3fcyJ1d8NIrL92w etXs86acBemKIgz/ZCxCFi2SwQP7N3XvWl9dBZ5awudmfA+N/ExXvVN1dadZcumg5lql plONckpjwwFMBaA3WFqkFSvcKB7Qu0e3TJ3ed0Fngcw6QVpPVHLu1eZi+j+M43+km//8 nn+I93++2Q/ubUuJWcLR9ilxNJcS0awD61D0SqKAAZYq5dMyXBHxWo7qzDkYF0Vgm7yW Z6M/U0MQ1Dywc6vZS66weAuk7Zc1cCKaYtmtuJvaDOs0E+XZhAkalknIAdqMdWjRR9Bz 51qM75hWsUs6u2MacJL1fEAZ215ElPF8jNZmYAW3NW7Qzkxyz4jsNVUJzUTq247ioNUt elI8Tw/ARrL8Dg+JCgWJomJcKYQyIpKqgcdQA1xQEZkGLDTQnSPoKWAoBvIM1k8ZM1Bc 5S4aieexYhaXtZir+RaqOBxCEjFCVoyEVElFElGdGjpkVT9M54DsqRJ4pua64CiSrau6 5KMTYRvAH/QSebNAJIHENGQy+pcgsCrT1LCr0RCAi20tCiuujduWStA4hbgHw4HuRytI C+kRI69CR9/FUUQN5kDlALWo2o8UWVEz7Cs6Sr/IrEC3NKOwgxUyQrIXSJIhuZ6FcQdD Z66CVo2kF3qm6rgE7RhKk3I+hgI08COFmYoLZxUEUjbCYLFRl+muH3gYNNBU3TXydU8K qEstzpyZaIYMVIuiEPr54JQakMviLjZ6S4ouhmzYsTlPofT483Qm2UnKyKst15M/f/XH T159BXPCj2TbnPfsyU63bd2wZvW8ubNmTp40dkzvlp7or3Tt0rkUVZN++9dvPv3jW7/5 9Zu/PPKTQ5s3bTzv3Fkzc3kMNjV8SCKeHw181xGOvv320USxdv8+beCAAQPff7eosCCi xJQvf/fhe0dLMGXw1Z/33q8MHnTfvcrQm27YtkXaxT5fv3zp4oXKgovWSicG3sDHeDsz CjDK9TPByKJNB4gC/UnRi4EEhLQ7QPwDJHyABG36k29zItUPIoYYBKjCBSj7+6TcQQBs VoiS/1yI5noyXOlKWBgWA4uJxcJiY3GweFhCWPKw5GMpwJITqP91vYDCxs7FWEpzcrUi J1eHkPLv5Wp5Vq6Wc7lazqsIyQ7VJdMY76E8h/lFFiaz3gu331QMDaC2YLsKOL0KHs+W Dz7oU1cLAom6ATYIHQnLjjCYdyKGcOqy6FsX4TfRo82gdGTRuB54y426bHsXR0Vpakgq gQMvBPhLwCBRa6LYpRn2U0NFAGzHLTwHN3Gyz8RtoMY6Xpl4KYGFzBs7HOizx+WI3lb/ TlTBbC3kUxGoh+AC/PXQmaYdsu1CUiTqDH1ZmypowYBvghKiePBWEDQYn4cJPOp46EIw G11UWcMMieFaKiDTwwgNJiPA2Lj2QQcIIAnpB7cHGhaiT/XRA9FkX5bjKqggfDy4S2i3 OKaF+5lJVC3KZ38MAzOC6NbA81UxoaRZ5VBGUIm4Hy0fA8yCoP0bkZH3yFN0y2QVVjaz dBFdGAhjPHN2gg3cWKRayIExaSoYYMKD8AdBZfEoSCQqBWAc9AQdaTOEuuiHj/9ORYHQ KPhKyGFGZ2poXlGBpoYUDAqWVKpWT1/mxYT5happ2OAoEh95MlAiKPxySQLRMGzUyXBc hxpXKbx0MHMMRoXzQauJDO0Jgo8WD0aVHHwAqJaQiszB/BPmPDBpBYotivCxJIht6HW0 tM3OiaSvlaJ0QhLyVJR0PiiFVhHkX1g0MNZh+ZRZ2AOTGvDdIG91/j2hRaWp6EhbnstC YYxzYODFzXMK0KmOyp5ve+h8S1I45poU9E0KQwJzDVmLs0juIjcIlwk/bRlHLr+85Qrh krVrVi5aeLahS+KaVSvBOk4fN3bUCExkDBrYv2+2wYz+cbcMRixqYI6mypN8ajcfM7sq E8KB3tLc3KJq2uJFixaP7kjIqO/ZxYkKc+L635DjH7CA3Er7etDGC0YJl7fnBZfnMhp9 GYpebRKeOQIGnkx2egI4DIMmQcCd29wlyD+eR3x0L/fLc5p3b2BU8Qf4zATMI26qOyAA SO9GPlfEopiJrQt3KQ24sYN0ipC70Bm1YK4HkD4MjQ7EBkaEkEcO1aH6CfUsM16OeEXr BWY510YcaaHGNP7tV+fJYJiYwwPGwtMBDCFoJRsDrxaf7xHhdWP+DAM7NgINZq0m6flk QYmMqTS4sbrvSVGqurIJSelgoE+xPAtSL5sRflSznajmuNCKYOmAaswgiXD6ozo8Fx0M yrdcJYZhDNg7IasAIQKpyMIRuLZMBU6j92k6GFiDMIN5C0dXgZjEq4AgzeGj0PoZuZX8 SNhBrJY1ZOfOll3C5vkXzMvaqAP79G7u1bOpvq5Lxw6VhZq644brL1m/du75M2dMH9u7 pXtVRw+NKWHJUn3SpMlnnjkJHZizz546ZcrZl1+hjRwxYmS6Rq4o76KM2baVMaVvfzYV 097mVSxkLFk8m02tZYmC/MCzFHMum3o2jeDDKf9elqWPcSTkPDbnSuAWH/ABMPKrE0G4 oUZtw7jc9b8IyvaR98PAbIvDcmFn+zjcmeWnjsw5ZMCRo7FOATNkUF5V2Rm4NEFLETOk KPW8KCMqOYRwWwGjPxiOy2SRATewklZ58ecNIZkHY9BGaBvwGCvFL4YAFFZE8ABM/4QE qKF8WIAABsitfLojbIVNwyoHn9MVtRIDoIobAQlEvbUxDloeiqFBo6iWuDohUpsGPuiQ bMGdgwnPLEtD6RfthF4hxR2UQGo7jmQ6zUa4pBmGhQsrI4Les20xy0CjH+VO0dC9gTsI meSqYSsKY0KyIj514zC+RDbahsaiqN+YHQd7xZMhJ0BtMaWGot1ds9GEkEyCaRp0pZAq mow6CCaKkQBAlIZZbkwolHj1ShRNA3icZgx1X5Ix7okp6zwPTglgyEShxXCPreAFabKM D4hblmhLiMy1cKUb2dht/aD1r2QKTQsXCx+2zCLr1rWsF9auWb1qZVZqjT6luVfnirLS kOdYgByFewaYNVu4YN75c6bxZvYk3s0eMQxWWv+6Wn7ighCNaf379esPUSeGlVEjhw+Q ho451cQnueKimuqOrFvX85rIhFyMpttG0XJ0LcvaTkTlv4jCH0Tevw7JEzbCBGFd+5hc l62NXO3DO1cQWXDakwqDD8+lSoDx/3ACZY3PkeASzIWPrUA/wXFIMq6gkmmZV8mcjOLR lyzlZyJEMZuSs9FQMhHCOAKXPOhzZ4kH1BK2gKtApvgYblQCN8/UCjygloEzVSAh0AVE 09GEzAA/sABi4ApaNLBjWqFpReEcaFZSC8wIH7kE6KE+yizki0VmKM8KBZYlc0kiqgVa MRpWQEmY1aipqprIVGPmNEFmaOiHY4QCE8UwpalYEU9W+HYRkBpDyTAgMOCrElizrBQW gaFQKSTGrdo0ED8CV913ZVeUrGIx5GMkKxxnTgKuBhwIVS6E14xyyCx4suGY5nMqxM9F yuDMpIlkq7Bb+KJlAdmzp+Ue4e5dO6/eeMWsylQcsA9hD1W/ft3KFYsWnjl2zCk8fgb0 6dUVOV7XJV6A01+CkHDZ5frWa67ZOvksrU/v3n1u2rVdvE4+f05C7jZ13GjxVGVItqk5 6fQJ47XTlH69m7pL3W5n1ohOtkW6toVXdgLrhKYHw3d54fs70f9vYq3mpD5RW9HrKuxp H2B7skWPd6wRLKAfVSQtZQGVgyqQOMkNWZp0pBQaNt97r9kTVBBTuS3BrdHESaDHw6Mn 203hUZQV8rjgsZlpqOVcO8uIuT0rdea3OelOoXwC3vn4BCI6q855XUVE0okYjAEV5Swq klDzvArVRfnB9LdDVcOwMARYxk/PiJaEw6pGXHA9kDU+asuJmphSbdipoJG8SsmIA5i5 8F4NILuHghJiviFZKDtQxYrIfVtcG0ohENaBvYlJdirji2E6+JhqFnU3PRQ1DGbIanEB dkMTAJS0KJ8GUszBdFB5REMx1X1UO5zfosQRtKAEhp4Puwyi3yxpcROIdRrOx9yF6qKQ KhhwdExbAYXmLRicj8L5NMP7k+yQwps80O8awXSTq5YiTqkfJiG3KpxSHMNnxZ3yPTSb YJNZONWatH7X+jnw/VNhmfBwywiyfHnLCmHaOeNxstbQIU3dMxjgVhZceO6sGdMnTxo5 on8/Du8NmDyvTJbmxzwUV9AClN5ly4SlS5e19NbSNTXpZQsX0OL2gxTt5yj+t8D795J3 cr1ri8EGYXn7GFyeLXKgayInfxF+PlMYHWk+Lo6gjKYlVKp01tYBQDZmuHcjBQmJdwJ5 pzCKJQDOosihwxflp1AhwOD3N9Tx058wpy1ijgH6CCBF3GjIReHCyLThK+FEaUFETIK5 Gfn89AnmA1zzqjLgbjr6OwAx1aCupeOr5LQQnTCEl2OpsB8xcoZGoGSgHWhhAhxiwXQl OU5e8kKYBMM0TbGtMLshoWPWC3QfXTPN0IPGOKszpZiR1123DebbJdBVJXGvvDyK86NM 14whDryUU5zSgfEKRs5NNRQGyKOLLqHlT0ULBnuEJkxHjcOILRExRENaj7V+Qe8gTwq3 kGJwuVtvbfkfYd2CSWeOh3mYh1nT7dduu2bL5k2XX7bqopVAw3nnn3cumNy0qWePHZMd rEDnvHf3bihk9Z3T0AYVpSVBGPGw/hKMlpjWhkvxwnXj6i1whVUNA2SNDQ2NY8dptV26 oF2qrbv44nW+6u7asZP2py4hvdu4HM7g4/30EwXrP73+T0D07+H1g1t/j7W2KOst3No+ ym49EWUWJqlxQg7OmxMznODxMxq4LY8eVAYdb1zykpQbAUOoiZznidmzGlDAsgYkrnMu OyIvIaHSoaUkNUvw8vnwB+WefnbkBxAKRkfvkKmJE8gMN1QoO16R4hZ5aNHIHaHKo64W oQ5mmSM4VQ4SGr1MDHRRHTOGcNVhE3rQqwFCTIXQ0NGERSsKvWY7Cl9f4W2gbFMYOpdi NAcSP6TDtmOeFCZ6VPYNRKquwON3EK6+ZhQXSJiFJltDesp2Qm6yuMoqSDbGorqeLtXj ygTQ8mIvv0BL4D8jiFEKRQ7ZUOBbUlUBXDctEoqwfFfX9SofJxPois9cXXOdTkZBcYUZ C1St3MOApI/mN145KCaIJFwE3YTSIZ4Tc42IViSKnTFZrZVrgQxXAH0iU0RXwDYikSJ8 hhA0jJO8tv/r0DpR6Mkp3z/8BLhHFPoJ/fE/FAYLQ4ShwjBhJP6nwin4bwqnCmOEscI4 YTzOKD5NmIgtCc7XzcoenNMbF4QhI0YPHDmkqs/8KefMnrZ00JTzzxk8pu+YMZ36zJ3N /ztGbkv+lGdgmYmF/0+MS7Fsx3IXlgNYfoLlDSwfYfkGO6lYYlgqsXTFMgjLBCwzsSzB cimW7a1tP9hH+P42EYpPWp9y0vrZJ61n/+dGu/2z/+Oj3fqMk7bHa/jB8806aX32SevZ /yHS7njnn/T43JPWs//jo932+D8cP3i++SetX3jSevZ/hrTbf+FJjy86aX0xX/9f05S/ +QplbmRzdHJlYW0KZW5kb2JqCjQwIDAgb2JqCjEyMzQ3CmVuZG9iago0MSAwIG9iago8 PCAvVHlwZSAvRm9udERlc2NyaXB0b3IgL0FzY2VudCA4NTAgL0NhcEhlaWdodCA2NTcg L0Rlc2NlbnQgLTM5OSAvRmxhZ3MgMzIKL0ZvbnRCQm94IFstNDQ0IC0zOTkgMTQ5NiAx MDQyXSAvRm9udE5hbWUgL0pOUkdPSitCcmFkbGV5SGFuZElUQ1RULUJvbGQgL0l0YWxp Y0FuZ2xlCjAgL1N0ZW1WIDAgL01heFdpZHRoIDE1NTMgL1hIZWlnaHQgNTYwIC9Gb250 RmlsZTIgMzkgMCBSID4+CmVuZG9iago0MiAwIG9iagpbIDI1NSAwIDAgMCAwIDAgMCAw IDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAg MCAwCjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAw IDAgMCAwIDAgMCAwIDUxNSA0ODUgMCAwIDM3Nwo0MTIgNjAwIDUzMyAzMjggMCAwIDMy NyA5MDEgNjcwIDQyNyA0NjggNjAzIDM5MSA0NTQgMzUyIDYxOCA0OTQgNjc4IF0KZW5k b2JqCjE1IDAgb2JqCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBlIC9UcnVlVHlwZSAvQmFz ZUZvbnQgL0pOUkdPSitCcmFkbGV5SGFuZElUQ1RULUJvbGQKL0ZvbnREZXNjcmlwdG9y IDQxIDAgUiAvV2lkdGhzIDQyIDAgUiAvRmlyc3RDaGFyIDMyIC9MYXN0Q2hhciAxMTkg L0VuY29kaW5nCi9NYWNSb21hbkVuY29kaW5nID4+CmVuZG9iago0MyAwIG9iago8PCAv TGVuZ3RoIDQ0IDAgUiAvTGVuZ3RoMSA3NDIwIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlID4+ CnN0cmVhbQp4Ab1ZCVBUV9Y+9y39Ht3QdENDL3TT3Tx6YQdRFCHyxG5AWURQQxsxNAqC y4QokpDFn2R0JqIxiwlR45/JMnHBMT6B0lYTf5My0dRkJmYZEzOZmWSiSWYqlPPnT+ZP RqH/814DUWuS8q9K5d06955zz13O/e655/W73blmXQvEQA/QULso2NEKymPNxeKNpauD HRE57kMsVy3t6nREZNYLQK9q7Vi+OiLzjwGobctXdY/1j28C4DvbWoLLInq4gmVBG1ZE ZDIZy9S21Z13RuS4ISy9q25bOqaPr0A5cXXwzrH5QZ7f8bPg6pZIe2s0lqkdt63tjMhJ 72I5rWNNy1h70oD2vQkEa9VwG0TBSuCAAh2mRgDuc7UNGNTKenyWZWq23Rpb/DXoeUW+ tfohpXzN+eK5b1queDSP8N9iRdR4e7lUpY2mAUQT1A9rHpnQKP0wU4egPiMEs5FKkKYg ZWTMNEEP2Q0PIz2NREM72QzdSJuQdiAxE9w+lI6SzQMMLx4j3WAhc0QNY59vMNtNao39 7RBRDT1lP2/65Dgx4+59TMwDMRA1U02eJr+CZWAnz4OL3AUV4CU7B9NW2ZtQtQ86kHqQ aCUnZN9A8iT7CZIJLoZgHzckM+Sw/bO8LPvFvBBFBuyveEIMFi8noyTG2k/anrL/l225 /QTS/oiqPw1bHLbvs62yb0sOkZ0D9kdtIYJ9HokU62zY9bB9dVqffVmeoq/qC1H7B+yF qF8oauwF05z2KbYL9hxPiCcoZ9mq7Ol5v7OnYkds5sBBXaLebrVts09HVbLN75mOdJz0 kychnTw54JpjP4YsLndwdtq0vhC5e7DCm+cKkbvEggpvX1qFx5VWZXellXk8yC88w23g buFmcpO4DM7LuTknl8QZ+Dhex2v5aF7N8zwXIr8ZKLGrjpP9UIKw7B/kVTwbIi9gJXOc HFAqDxzhGZ7igTeEwh+h8xIwhMj+IZ3MIXNYpXCqEDkwGKk6INoZmWMUhY6SecwwB4rw FMwBiTwYUsHGxK4SU0ncDH1hme/7siZFM55nfP9jIjapr7K+Qeq3BaRJMhO2Bcabm8aZ 7y0716GqpTQjo7Kue7CrY0Wrv0XwNwn+FqQmaXNXm0nqaXY4Dq3okBUOiXY3NS9tk8tg i9QhtPikFYLPcahL6XedulVWdwm+Q9Dqn99wqFVs8Q10iV1+IegLDDaXrmm8Zq5NE3Ot Kf03c5XKg62R52pW+l03V6OsbpbnapTnapTnahablbnkxfvb60vXdqJ3OvztlQ7JWy/N nreoQXIEA74Q2Y2VvnXAngQd+xJ42R6wMDlgBwifR/pALkcXhD9lT4NudHX4v+ki3NSj MlGjJcVwEh6EJ+EgqGAv8l5YAtvhdbICz/ZiGIJzJBmyMfYyEIIqeIOEw29BK/wa23fC K/A4HIJo7LMaElC7lbjCd6EsIt8MG8LPQipMg1/AS1CIo26F4fC+8CBq62AB9MN+7P9b IlCHmPjwC+ELwMM8HHMDat4KV4UPQhxkQinUYu0GOEFc9AfhNjBBEVq3C34Fz8DL8AW5 nwyF28Jd4bPhj9FVTWCFekz3kiHyMX2Q+UV4V/jv4VFEwgvpOGsTbIPncPyDmE5iaPWT laSTbCOPUyJ1PzXEbGSNoyOIQxqUY6rAqPwAInAUTsGX8C25RJloHd1JvxqeEv4f0EAl rlJeSQt0Yfolpq24puNERXLJLFJL7iWPkcfJO1Q6tYBqoO6g7qQ+pWvoxXQ3/Q6zlhlg t7DbVZrRr8PHw6fDfwAj2OAWWAPrcXWvwFn4Cv5FaBzLSlykiJSSJZh6yJPUUfIMOUrV kpPkLNVP/kI+IZfIZYqloqkEKoPqpLZR+6lXqN/T7fTj9A76L/TXzAyWYp9hL6pc3B9H m0c3jf4+XBT+OPwNhlgenLgzpVADt0IQV9sBk+E/cBUHMB3EXTsFr8LrSvqEWGEYvkEU gMQRC5lEqjHVkLmklbSTp8gxTCcUW/5J4UZQUZSeMlJWqp5qplZTPdQfqB46iU6n59CL 6IOYztDn6Mv0ZYZl4pkEppyZDVuY1cxOTLuZvcwA8yZbyM5ga9iFbA+7id1CL2XfYs+p 1qu2qgZUl1T/wLBYxd3GbcHdeR199mX05e8ehqSi9ZPgZ7CU+Egz9OFuPEOC0IvetYw8 gHh1gDfcSK+ny6lc9IYTcDd66064FzbRi+GZ8Pt0P7yHnrIKh+yBPUwp2NgncHfuh1z0 orEkpqWneT1uV6qQ4nRgyLcmWcwmY2KCIT5Or4uJ1qijeE7FMjRFINMvlDU5JHeTxLiF ioosWRaCWBG8qqIJj7JDKru2jeSQ+wVRdU1LEVu2XtdSjLQUJ1oSnaMYirMyHX7BIf3O JzhCZNG8BuQf9AkBhzSs8NUK/7DCxyDvdGIHh9/U5nNIpMnhl8q62nr9Tb6sTHJURDjU WZly4BBBIw8swazgvRhgYZbcwi9ZBJ9fMgvIo452+YPLpNp5DX5fktMZwDqsqmvAObIy 2yW0EzZHLxOWbQ6J0Nwkc8HFDRIdDEhUkzyWPkMyCj7JeNdF03fiOOffcpVSolxlwZbe Mkls2ozgymKTLAW3oFRZ78BhqY2BBolsHDNCtnEFWiqbG3knuJpWOKQooVRo613RhOBC XcOARbQowVeC2oYBs2hWhKzMo6b1RU5c/dGsmVkz5bLIaVofKT/7eaT+7ZNyaVp/6iMs K+smACAyAsJstFNyLFUmEdDYaXLWMg16l05DnPAJEFxmO9ozS6LQZ2iXxLpmB6We+nEz 2nwR45pW+AaizBblJVQawPZNvbrpuFPYXic4er/Gt3WTMPzFtTXBsRqVS/c1yEp5oyd8 RSLBcb5Lflm6cNVtJqFN3t8uZU9RFkz+qypQlqGRbZYM+AKvbXBKjgBW4K/JzMoQRNU2 HCJkayBEwhtD4LMdxd+o9K1LUJ0pu1q7D+dHISsTK9KdyGVnOspw5jLZVxy9jt7Zy3od ZY42dCbGpZSoaOkN5CCC9Q2IE8zHGcVA0gTbEghMx3Fy5HGwCzbvDeAIK8ZGwFKpyhnB RrmZ+DKl3bUN8xqkHl+SJPoCuAvovidrG6ST6LmBALbKm7AULb633TRm8yS0OS8d9fmR UfC3Sw8OEejtlcesbxCc0sne3qRe+bxF5BCB6yvEsYoQyE1kyEOkpxb7YiE4k5Q9cApO NCsgYzoZXXrco/A3+w8jXDBhN/acitYWKAhP+5EQLrwRhKffEMJFE5Zeg3Ax2lwkI3zT T4fwjGsQLvlhhMUJu9HImWitqCBc+iMhPOtGEPbdEML+CUuvQbgMbfbLCJf/dAhXXIPw 7B9GeM6E3WhkJVo7R0G46kdCuPpGEK65IYTnTlh6DcK1aPNcGeF5Px3CddcgXP/DCM+f sBuNXIDWzlcQXvgjIXzzjSDccEMIByYsvQbhRWhzQEb4lgmExSQJro7DPdeFXfjRA/Pi qyDHX0psHJQya2EXUgU5DRtU/bAB+SKkLqofNuGHtqzvwabjdzzR+OWxBOUq/I2Jn9z/ z4caa0+PlTgBsDgiXiXh73z5Zkh+JsNi4ibz8duhgjpGA/0RMx1rKfwGAOYsfjvS2Lok ci/E5+DLG4nXhQDOIsky8vSHIWCQAHnuQziGPQAWZhzDUVgsc/Py9U69B6mU2Rq68lf2 pX/NCjHVl/GOAVe1C+epxHlYyBPjgaKpZIblaQtHKBcLZhXea9QPOruW4Hd/zVfF1SPF NbpPoURmSvJy43HgBOeu09TnV+bhqF8eRGsrwh8wFvx6sOKXpotEi91P8Dsse+w0q6Vi WUOCNi42wSBGiwY+zUIqNYfp0+Q1+nTS+/z5qHP294XPjZ8LmtP603HUYp51psbuTLSl Fqo4LtFps3JqW6LGxT1h3WM9Yn3PyrgSY11W1qyO5vRaT6zNw1o8qdmcx2x2e9517m5U LK4euVCj+2f18LsjhXGFhXqkuMKcRigpGS4ZRq54pFg3jLV5ubO6xTIQGJbGTzPCMiq7 W6+L08XrDDpGFe1KSUp1gwNsbpJsizJybtAkaN0kRitYnFjFYsab1G6I0WGGV3YZGURX nJEhU0Z6Rvp95PZGuL2xERKNmBKcySR/0tSCqflawqk4lZACeh3kE7fHLaSoEPehc9MK 4nRXLrEPP/Hg/FzDIW5uXl33zLozo38npr8Su8Y758A9e1kiMOUrF8xbNefZ515tLCgv eiS71qojAn6fUqR01L2u7P7BXiJfgKIXbMCNLhrzpQLRyl1kcONVtDrKaLRg+zSOBjMf 1e9sLh3b5lMjxadq/C0+3OqS4pLqYWWrBX1+grDhCD5M+uVz7EtvKP6DY6tcTDm4YaNY xPGcVhVr5I1aY6yH9+C2VJgXapZrogWX2mITzGqKMbqcNqMtRsWBKsnqouPVXjRCn4YX YWTAkibf/4lqINmuNDeYPd4QiRn8zqyRC7qvhr8aGbPMWIzGVQ/j/hkLCW6ivI3QGJ+f UDC1IH8SQj0DcUa89YJ+sgytwiggI7dhQJwcuL2nJjO1+NmW92vSj6+sXrHjiCWto3XP EJOzfW7qTSWpZQvrd83fOjKV+nxl7dbdI49Qx1dPqnzqzZEzeE0MReHzjJOpwbsVE5jh YTF/O9+n25H4PLOX363blxjiz/DvMRe1fzNET+dVNhMXbYvTmDmzOYHyxFqSojwJZktS iEQNOteMeeqw4qjolBHvVFwyE4yMWxMfhV6lp9yEMyLHxiCnNkS7gegw4xNVbkJrMVP8 Ts4y7oPG1LgpkyNrNSTmx6GHUc4UmIJ+xlEfbcytOvZ8X99zeJF0ZfR//zR6hcR9puok sbv7ljx2ZWD/BfqD0S9GvxodGX2BZFwhWiKy6ENdowsYF55rLaRAp5i5j99jpLy8w6rX qmwJXKxKa7NqUrSUx2RJVWfrsp1pKbFmIfWXzpciy8OIcSFyFJXllQzrlf3CY2dNTALW 4mbckIQLYxMxI2atG2ijsiZlWffJKyqIrIhTJRiMiflkbJ/x414+Qx63Ry9Qr+1xlR07 7ndhPpp9sEC85e7Do0c6d3bX5RYNdb/zds/iQ8eX7bzn5t30oa2zvcWjf8M1Ptt365Tk 2SN/kmPhJjwsj6Iv62Gu6PbQ7pipdDnDaHkdpY3SR0V7eBa9Vq/mLfEkW5emB3NcfIj4 0UHXj4dHXKJ8ZqpLTo2ckqON7JdjXil7Y6IxIZtEXHHT/oRfr2RNNl2S7oFH0eWOFjxJ 0Sdo6uCake1K/A+/Rx9mKvGmL4dkiw9Ni9rO9sXtMGxP2J6u8qa6PAXOMmd5arlnYerN ntbU5e7u6O6Ybm2X0Jna6ep0707emxlPY5his5jseLAkJBmtpoQsQ7Y3VtPOu10FLsqV EqNmMuJNr1lt8Rxjy96ZocnhorQ6ioMcZ47Fbko0eYwzvG7O47Xkae0e3QzwZJtz8wYm YisexULZaUcKdcjJyy3MwVxfWCjvMUZY5WjernhyFcmi3Akui9uptTshCq/QCZ2Jl11s OnK2OKxLMpicxBGb4gRnijaG96idxO2KUpMsxon/m2CWrLc6iTkRMyXE6ooxviqZ4iLj jt9IGuMxyuZPUtwlRw6rUyYr8YATIiFWdh87kSOxAR3H7SGXeJdv77LtN3nWPrRpZucf j365chbVz7pn7Ght93tr7niltP38ny+d5sgRUrso9+abb/Gn4lspJX32fdtf3Lqo7aZJ 5TViWbo53paT6X/sobPnn6a+xfPSE/6E/jOeFyPGhyXi9JDhjIGKiucN5nizwau6g36P 43hgtWpQxahZjA0mzmTSJMZkq9OiNRYLSUs0my1vj4e+ajk4yEcI4Y3Eh5JiGXDZtUgj 0csHAl8pU/T5emGq8k7BVetdZJol9+cv+lxD/ZQwefm2i/VZ5CCTM1JYN7lp76L/pLSX 33rqpvT5O+o2Ue9bZJ9TnnAL3p/+u0eNlTl4B1uFN8B1eKe7ABZCg9KQ4M1w5PeRCv8r g/K5DRWVCzMqWlZ1tXS2Lw1im4hWbpyLVIxUhbQYSb7TuwdpC9IupN8gvRgee5CHCZ4o fa6WFTuv0rdf1175n/Eq/W3X6Tuuk9dcJ6+9Tl4ny/8HFKOfyQplbmRzdHJlYW0KZW5k b2JqCjQ0IDAgb2JqCjQ1NjgKZW5kb2JqCjQ1IDAgb2JqCjw8IC9UeXBlIC9Gb250RGVz Y3JpcHRvciAvQXNjZW50IDc3MCAvQ2FwSGVpZ2h0IDcxNyAvRGVzY2VudCAtMjMwIC9G bGFncyAzMgovRm9udEJCb3ggWy05NTEgLTQ4MSAxNDQ1IDExMjJdIC9Gb250TmFtZSAv R09YSEtWK0hlbHZldGljYSAvSXRhbGljQW5nbGUgMAovU3RlbVYgMCAvTWF4V2lkdGgg MTUwMCAvWEhlaWdodCA2MzcgL0ZvbnRGaWxlMiA0MyAwIFIgPj4KZW5kb2JqCjQ2IDAg b2JqClsgNTU2IDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAw IDAgMCAwIDU1NiAwIDAgMCAyMjIgMCAwIDAKODMzIDAgNTU2IDU1NiAwIDMzMyA1MDAg MCA1NTYgXQplbmRvYmoKMTYgMCBvYmoKPDwgL1R5cGUgL0ZvbnQgL1N1YnR5cGUgL1Ry dWVUeXBlIC9CYXNlRm9udCAvR09YSEtWK0hlbHZldGljYSAvRm9udERlc2NyaXB0b3IK NDUgMCBSIC9XaWR0aHMgNDYgMCBSIC9GaXJzdENoYXIgNzYgL0xhc3RDaGFyIDExNyAv RW5jb2RpbmcgL01hY1JvbWFuRW5jb2RpbmcKPj4KZW5kb2JqCjQ3IDAgb2JqCihNYWMg T1MgWCAxMC42LjYgUXVhcnR6IFBERkNvbnRleHQpCmVuZG9iago0OCAwIG9iagooRDoy MDExMDIxNTE1MTMwNVowMCcwMCcpCmVuZG9iagoxIDAgb2JqCjw8IC9Qcm9kdWNlciA0 NyAwIFIgL0NyZWF0aW9uRGF0ZSA0OCAwIFIgL01vZERhdGUgNDggMCBSID4+CmVuZG9i agp4cmVmCjAgNDkKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDY5MDMyIDAwMDAwIG4g CjAwMDAwNDU2OTkgMDAwMDAgbiAKMDAwMDAwMjk4MSAwMDAwMCBuIAowMDAwMDQ1NTM2 IDAwMDAwIG4gCjAwMDAwMDAwMjIgMDAwMDAgbiAKMDAwMDAwMjk2MSAwMDAwMCBuIAow MDAwMDAzMDg1IDAwMDAwIG4gCjAwMDAwNDU1MDAgMDAwMDAgbiAKMDAwMDAwMzI5NiAw MDAwMCBuIAowMDAwMDAzNTg4IDAwMDAwIG4gCjAwMDAwMDM2MDcgMDAwMDAgbiAKMDAw MDAwMzg5NCAwMDAwMCBuIAowMDAwMDA3Mzg2IDAwMDAwIG4gCjAwMDAwNTA0NDQgMDAw MDAgbiAKMDAwMDA2MzU1MCAwMDAwMCBuIAowMDAwMDY4NzYzIDAwMDAwIG4gCjAwMDAw NDQ2MDMgMDAwMDAgbiAKMDAwMDAwNzk5NSAwMDAwMCBuIAowMDAwMDA1MTA5IDAwMDAw IG4gCjAwMDAwMDY1MzcgMDAwMDAgbiAKMDAwMDAwMzkxMyAwMDAwMCBuIAowMDAwMDA1 MDg5IDAwMDAwIG4gCjAwMDAwMDY1NTggMDAwMDAgbiAKMDAwMDAwNzM2NiAwMDAwMCBu IAowMDAwMDA3NDIzIDAwMDAwIG4gCjAwMDAwMDc5NzUgMDAwMDAgbiAKMDAwMDAwODAz MiAwMDAwMCBuIAowMDAwMDQ0NTgxIDAwMDAwIG4gCjAwMDAwNDQ2NDAgMDAwMDAgbiAK MDAwMDA0NTQ4MCAwMDAwMCBuIAowMDAwMDQ1NjE5IDAwMDAwIG4gCjAwMDAwNDU4NjIg MDAwMDAgbiAKMDAwMDA0NTc0NyAwMDAwMCBuIAowMDAwMDQ1ODQwIDAwMDAwIG4gCjAw MDAwNDU5NTUgMDAwMDAgbiAKMDAwMDA1MDA2OSAwMDAwMCBuIAowMDAwMDUwMDkwIDAw MDAwIG4gCjAwMDAwNTAzMTQgMDAwMDAgbiAKMDAwMDA1MDYxNyAwMDAwMCBuIAowMDAw MDYzMDU1IDAwMDAwIG4gCjAwMDAwNjMwNzcgMDAwMDAgbiAKMDAwMDA2MzMxNCAwMDAw MCBuIAowMDAwMDYzNzM3IDAwMDAwIG4gCjAwMDAwNjgzOTUgMDAwMDAgbiAKMDAwMDA2 ODQxNiAwMDAwMCBuIAowMDAwMDY4NjQxIDAwMDAwIG4gCjAwMDAwNjg5MzggMDAwMDAg biAKMDAwMDA2ODk5MCAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDQ5IC9Sb290IDMx IDAgUiAvSW5mbyAxIDAgUiAvSUQgWyA8MTM2ZTNjMTFkM2FhOWZjYTc3Y2ZjZDBiYWFl ZDUwMGE+CjwxMzZlM2MxMWQzYWE5ZmNhNzdjZmNkMGJhYWVkNTAwYT4gXSA+PgpzdGFy dHhyZWYKNjkxMDcKJSVFT0YKMSAwIG9iago8PC9BdXRob3IgKG9sdCkvQ3JlYXRpb25E YXRlIChEOjIwMTEwMjE1MDkyNzAwWikvQ3JlYXRvciAoT21uaUdyYWZmbGUgNS4yLjMp L01vZERhdGUgKEQ6MjAxMTAyMTUxNDQ1MDBaKS9Qcm9kdWNlciA0NyAwIFIgL1RpdGxl IChsYWJlbGluZyk+PgplbmRvYmoKeHJlZgoxIDEKMDAwMDA3MDI0NSAwMDAwMCBuIAp0 cmFpbGVyCjw8L0lEIFs8MTM2ZTNjMTFkM2FhOWZjYTc3Y2ZjZDBiYWFlZDUwMGE+IDwx MzZlM2MxMWQzYWE5ZmNhNzdjZmNkMGJhYWVkNTAwYT5dIC9JbmZvIDEgMCBSIC9QcmV2 IDY5MTA3IC9Sb290IDMxIDAgUiAvU2l6ZSA0OT4+CnN0YXJ0eHJlZgo3MDQwMQolJUVP Rgo= QuickLookThumbnail TU0AKgAAD26AP+BP8AQWDQeEQmFQuGQ2HQ+IRGJROKRWLReMRmLQOCRqPR+QSGRSMANi TACBySVQsAy0ACiYQiOSuaTWbTeItedAAUz2cSudNeeT6DzONSZsSiBT+DS0AzKl0yEU 6XzGpQug0MU1eRVme1uiymNV6iUyswev1yDWSwWqD2y3R+4WGoxm52ad2iy1y73EAX2/ RbAUa7Tu01KzwbD2rAW7G4GJ4OxYWhYuf4mC5ar4++Ya95CKZK6xjOTTMVq46XEZ626D Q6yoaOL6p6NwAMNqPcAB8RBkAAR+QQEgB7AB5AAFAB+uh0AAGCoWgB2t+hXrW5vYZDVa 6GaKO5TUQx3s4AKBevAAC8Rv0AMJhO4ADQWgMAOF3BIACAG/BzAobAAFYKHqhDNNWyrP sc7LuMjBSCsI0kGpw08CrxA7rwTC0FtfDK6O/CEOJsckROkdp2gAA0UPCxkIwxFUNIg7 yPO2kAexqAAOxwAA+x3FzsRAv0ZxetcIwe2cWJUY8kuIezig1J0ewNKEVx/ISGxiscjp rCcEQrKSuKQpUPSqhiqJgFDYzEi0wMmfgAHcdx8gACQJAWkaqLouMyqtMc+IvIqNNqaI AGccZ8ROAwGAAC4HUMcZ1ni5AGgJNx3n8AABgUBoAAqDARAAFwSAiiB6HGadBm0cDpHo AVFBUGtPgQcgAGSaTigUB6CH05TfuCAAHBMHIABaDQET7YyST/Y9lWXZiJ2TZtoWjaNn 2latrTHaiJTW2SbzugzJq5PUz2vZtspzLLTLyxUuMvdFySrc0YXclUtwvLsKXfbFwSNK ib3q1N53zBd4ofIKu3UzN2QlgKaSZWwFOSg+HORiCEYniDk4viuJSZimIoNjWM47jGLZ HjeQY7giHGgYpegABAJg+AAKAu5J/ASBwAHwcjmgIA6CHaes6hKED8ILNs3noAB+H5Vg NA8CwAG2nZ+HrSAOBGE4AASCNRWKgp8bDo7dHcfbhg0COvoNsM2oOBAEUmt+GJEe+6gA Tu8AABe9oQem/OcBlEoPv2lcDRPCcBwSDGLxgAB3x++7/wwAcRyfB8lwKEHrzcwzSipm F6WIAHuBYO70eBpAAbwIBnRR9vQAoAoIeB3TaBIEV0AQJ6iBwBaUaprzaJItCEAB0J2e JxGaAB1HzmQOAMavVAg/4GHVWQDAQA/KHkggHgmAwAAOB06nyejinWddJiGKgegBUSC4 MlRQfoAAwfuhESxMCf+fzEoAH+ATRI/t/pBw2QHAAJSBT/oCQCf1ACApBoHwBIQ/QUDn UZNzIo0obA0z4AkBaB5qTCDUD4HeOp0YCT8HDba0s4zZTkAJH2cSFQAALAEPgL4Xg3gA A6CQDhl6Q1+lMgs/Z/BBxhDSGsc4BqmiDjzigACJqmooDzilE0hAnItAADJF0hEVYrqa HpFEHwLgVQMghAIg8RWVJWg0SBf6LV8E0bqbqLQnAABZj0QgZA2RugABwCcEZCE3nwTm fiQqck5kIDvI0AAiZISETfIo/EfY/yBkGQeRMhyECnk9BhLEQySB1lIooC4FwABLlUAA S8rQABClgAAKEs2FyiJxEV+4YCEDBiUAAH8Zo0QUgnBEgsBw2QJgWQeYcApeRLl/GeZT /4KRrfrG07sbyMSkDqAACE3QABSnBKyVzjwdgACvOeWqXpbv1lzLuXsz5gv9mWQiY0yB KTxmZO+YE0YGwVmqvswU2IMnVXWvZdstiVx1ABHePMeyDyWkBIKSUhpFybkWQeRod5Hy Rk1JOTlEJMUTkpJ2T81iFrbc8TVbyDluFMXEXGXERyDTNl9PuCU0p5U4jUQaesCp7z8j SACmk8KgTTINGygBXG2EIbgm0fg+E2qGbicBXlTgEAQOc3Fo7Oh+KTOAm0AjcGdVQaWP dQwDAIuKWVQqhkegsx8j9RGTJBqLSIo9Rcg1GaNiJpFR+uNIaO0UaMQaTwp5QE/HeOYo Q2R0KWA0A5nIAB8xWHePaKwCqKjkHOzof6bR8j6U0B0DEAh+DyKEPACYJmXjuQGPUdsh gVG+H4Ok+A+wHOmBfCGIK0KYy6IPUOmxBZ5zzgNAin0+KhT6mhTefs1IL0mYFdEh9vSE RJiWAyLET4oxTABGC7hB6GRdDJF+7cWIxxWjLcu4VOp/XPqTdK+BEaFN4E63pvjl3CuZ vw4lyjmK1AAcYMVxzkL9uTcrfog2B7/ubQHdC+OD2UMPY+QVkLJcJJLwuxzDOEWPYYw7 hrD+HMHYQxJiUhWIyD0oKvStMKeSXJmxMhrFEQp1EpH3jcAA/sdYtJTj0pZKRs5BAAAX IhVUzgHyQSPHSliDgCycX+gWMSa4zfiugeGVyEAEy0AAqmXSXZeKfkEbKlwBn0A5mc5Q /T2NvWLmwAGTlWEHyXZIfKcc6pxaYm0qkFH5Ekzu+LJJB8/5Ie3oPQJBtDaFzroB7egt F6EIRokjGVMoJUHjpfN+T7vyhQIT7NR7GwqG1Cod8On8c47RQ+HSGRACpukmA/WAABta znUTe4lzKg63vXP3XUA6g1FmIRTShcB5bFy4S7TccISRzIPqPMp9NWKjb/s8AA4Nra1J UMzbRJSThc28AAVu4VNgVAqjdHIwd0TfnCM3dgANLqQlg8UV285fA/B/f1pSTgNa/1xU Yiuwyd5nA5i2yFkV6bLYUSIfXC3Rt2HNw/bBGaFYMAANDi23Ckt7TrgVzOCnI35cO3+7 hKZyUjsDSOTmcCIaU20MwAAJeYAA1gA8AA3BfCmAAMceZvgSgbgEAYfA3zbjclQGcMEQ G1FY4Qdfm3OBlD9K2CIBqrAGAGisOceSkwXg4iABbpJCYHjq7FxEkYs+za+nbEiXt2Iq XlicQa70WIwDKGIMMAAQwkhKu727LhBgPAOOTv7YV70GJUHMNg2wBgCDiAAMAbL2wdAz gELoXA5gABUDA8W/5CY4kG8ObYBwEjjiyF4OwAAMQgAydGNLlwDgeBIAACTzZB937V2u C/3EdG7cUIPYWhtb6H1/olXWvtFZJ4B1IAAGvy6RjVHKOkAAGQE6tk5ynJ5D+AUI4PQR hNBiaamGr+FT4LgXFX3CK3vcVu00zuVr7wVw3/jA/kbsD7MgQ/3qCM4b44zd+A343+8I Ikz6Lk6WYA+0JIGnASAA3YeW/WIKuAjO16/gRM/kGAAABzAw0Y/y/2/68C2CWdACXPAO JG86M7BGIu4mc4HDBWAAGNBc9+rgku+GrusGIK+IkS+QIO5Kk4+c+g+k+okW+sziIc+y 7IYO+5CM+3CSJA/O/SiMt8/YmcmBAkvYgfAq3q3uz2f6/0/4A+/88FBApaQ3CXAJCQ2Y JtAGI8jBCbAcuTClAivY2Agc/i/mAzDsAABPDzA3C7C/A+Ik0oTAHwHoPQq6IKHIHAOK BGaKskUub0AObiH4zxEazUIKyQq0IKxYH+H2aUQGTqAIHyQGAGb4Ums9ELEeIUaqAAHq H6TqAW0aIOpeoS92c4GJFqAAHHFxBg+DBkkG+I5Ok5Bwca40AABhGK+a+e+i+m5QkW5U +xBCg2HMekGq8shsAeU0Z8QGHWHGPgUwe2HaHkTiduWKAoAyU8BKA8fgIcHwHQG2L+Gq G0AAHIH65oBABSBW5eAS9MGcGsUgASAOVYTaaUH8H+OGAmBEOiBI68Q0F/IYPrBZDaus iY7ais2TCcu47m7ql8CKCM4aN0u4KeIK7/A8p3ABDEykXzBSQGcQ7MFnF0rpBo1csE+K rsPhByINB2rxBtJhCE5XGfJOulCa48u1Io7k75KGjDCc4KQABXHuApKc/+/dD8Ii0pJ+ WYG/KvFvFyfSHWAACnK8AAFJLCyMZebeAAzFCwAAGVLUzIPoBbLc5ySUBZLkZnKe/fDi I3J9Kqvg0k0cTi0hL40Q0eyTMAILMIzpL80PJKpTL1MYxLKpMbMgWVMfMjMoX1JNMrMw WtMnMzM4MhM3M7NALVM/NDNIJ+0pBWHCv4zmcQ0gzdKOKpGGIOoUzyv5KcApNKxi0oHT N2lMlQxUJWKo3I3LDBNwvg0pKu6E/uBC0rDI8Km4m9DsN9OKxI0oHPOs+jDvDTAEJ2zd OVOnOpLyIannO1BEpGlOlRO+we0oHZPZLpNvPIXkKFPO5NPTONPCISqfLMHAOaBOBIBB OZDOPBNs3G3LPrPtMuIcHoKSEoFKPIBqBUdMHOHahQAiAU1aH6BEeKCE9lDKa2ASOGBB RDQNQPMWIgUMG6G6hQAuAkU0G4OYAAA0ACqcBAOiWI2UKE5nRgSfRGui0pK2N+y2OYOb QCQ/N7PpR4Xe0o3mFcAAELSdLBLFSIX5SMk5SQXy0oG5SzI6yGyLPuIoKpQ+OG/qZlSt STS8IVF8LVFw/4RwdNTKXI0ogez+z/GG2pNoYmzc1SxacQy0UnTCIROJTeXK8IHwhQG2 G9K4bCUsAeA9OWH6HNHaqgVYAHH+u6H0IJRYN0G+HkaiA8AUPYAGH6hmHiH0OOH8Am32 H0HQ9MAWAUe2eS8YH4AaaiBMBi9UA09nUEWOWpPyHorIHwHcOaHsAU4GAcAGaUAIAKza y2rIN+cEH4aUN+hcHwHuhnWSKeH3LKASaYdGhma2H2PgHgAMPwAoAgUTEvV0WZNHXTXY sPXbXeI9XXXhTfXlXnStXkJSjA2o5VX4yfX6VZEyKW1MznYIx3YGx2zm1MKo5VNozdIr XtKnTOIa1M2KOPT+JTYKUtYy1OyYW+KWKpX3X9ZFUmzK0yVY5U2oIecQ1NRzYhJ7QQIg xuhnYqvsTrNcxLZoKpYfZdY9ZgxSJO1GoUcnT6TsxeT2ReyudgyLNjZ4pZZ8IM5aAABH am5k1jGbCVSlNEKXaTLIWLT/ZcWeJTZo1nHgBpbM2PJBDQyiK4JTa4cm2jXgQfYo2NT0 HFbtOa05ayNcoU1MctXeI5No9rGGzdPhZU5sAAFwG+TqH4Ha8sBYBM3K6w6064hs6+NB bmOOa7HTXaI4kSu5bgxpb1GgNsHkH6OOFKFQPICYC2Cg9W9a9e9jVyNckSm6qxavUFc7 Jg6VBOJDcKMDT4y3ZvdwIHPY9NQHd3bwPBdEQXZlS3KVXSI5bcczaIbld5RveSNBa4zd eFXoIGcQ2pa/dC4TaxfGJIoUIPfCIa1G1HdrXmI4z+1G2i5VLOBVfrZNbRfvCPewIez+ 4paIKpebGG0gzmHfgLOeqxZTb+I4KWjBaIzmTA5gBLY4xazmybZHfvYXgvbIABfqjPX/ fwIfea94INaGy2IPhEc4IW0hfTXmICAADwEAAAMAAAABAEUAAAEBAAMAAAABAGUAAAEC AAMAAAAEAAAQKAEDAAMAAAABAAUAAAEGAAMAAAABAAIAAAERAAQAAAABAAAACAESAAMA AAABAAEAAAEVAAMAAAABAAQAAAEWAAMAAAABAGUAAAEXAAQAAAABAAAPZgEcAAMAAAAB AAEAAAE9AAMAAAABAAIAAAFSAAMAAAABAAEAAAFTAAMAAAAEAAAQMIdzAAcAAANAAAAQ OAAAAAAACAAIAAgACAABAAEAAQABAAADQGFwcGwCAAAAbW50clJHQiBYWVogB9gADAAB AAkABgAHYWNzcEFQUEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1h cHBsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN clhZWgAAASAAAAAUZ1hZWgAAATQAAAAUYlhZWgAAAUgAAAAUd3RwdAAAAVwAAAAUY2hh ZAAAAXAAAAAsclRSQwAAAZwAAAAOZ1RSQwAAAawAAAAOYlRSQwAAAbwAAAAOdmNndAAA AcwAAAAwbmRpbgAAAfwAAAA4ZGVzYwAAAjQAAACiY3BydAAAAtgAAABAbW1vZAAAAxgA AAAoWFlaIAAAAAAAAKBMAABL0QAAANRYWVogAAAAAAAALw0AAKClAAAUSlhZWiAAAAAA AAAnfQAAE6AAAL4IWFlaIAAAAAAAAPPYAAEAAAABFghzZjMyAAAAAAABC7cAAAWW///z VwAABykAAP3X///7t////aYAAAPaAADA9mN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAA AAECMwAAY3VydgAAAAAAAAABAjMAAHZjZ3QAAAAAAAAAAQAA/PAAAAAAAAEAAAAA/PAA AAAAAAEAAAAA/PAAAAAAAAEAAG5kaW4AAAAAAAAAMAAArIAAAFHAAAAwAAAAtMAAACbX AAASGwAAUEAAAFRAAAI6BQACOgUAAjoFZGVzYwAAAAAAAAAYREVMTCAyNDA4V0ZQIENh bGlicmF0ZWQAAAAAAAAAABgARABFAEwATAAgADIANAAwADgAVwBGAFAAIABDAGEAbABp AGIAcgBhAHQAZQBkAAAAABhERUxMIDI0MDhXRlAgQ2FsaWJyYXRlZAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB0ZXh0AAAAAENvcHlyaWdo dCBBcHBsZSBJbmMuLCAyMDA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbW1v ZAAAAAAAABCsAACgKjBNRFPFEFcAAAAAAAAAAAAAAAAAAAAAAA== ReadOnly NO RowAlign 1 RowSpacing 36 SheetTitle Canvas 1 SmartAlignmentGuidesActive YES SmartDistanceGuidesActive YES UniqueID 1 UseEntirePage VPages 1 WindowInfo CurrentSheet 0 ExpandedCanvases name Canvas 1 Frame {{614, 113}, {1033, 1065}} ListView OutlineWidth 142 RightSidebar ShowRuler Sidebar SidebarWidth 120 VisibleRegion {{0, 422.481}, {342.636, 347.287}} Zoom 2.5799999237060547 ZoomValues Canvas 1 2.5799999237060547 2.7300000190734863 saveQuickLookFiles YES mapproxy-1.11.0/doc/imgs/mapnik-webmerc-hq.png000066400000000000000000000764311320454472400212120ustar00rootroot00000000000000PNG  IHDRæ$PLTE!`cXae% ] \+R!Nd_QnY]$#'.b"!#/QŚ&/I.`*9F"P)Xڷ 41:.!\vl^ċ'Hu3Wbps_ppoyκ}ܶһ.~ͻ!]-.8/~AŠ.~/~/~/~/~.~GHL~̻!]ggk44:%/L٧!]uuyVUZ66:EH!]-.8-/9-/9!]R-/9!]-/9ͻ"]ͻͻ!^ͻͻl~̻Z[c{|-/9A-/93+eRC;5*w0AÓ-\9CM/½1y$i@w*TAk}KyUAV**NBJ<7%g>)%e'W7w'Hk,I+Xl$XER4iw (u&.4'-Lt`^aŏ!Fd-Z]^go;HoG}O; ,e٥$euxNvH$JYYc)[B are ٞ xDt elv9B6dm-@(ħDd5MRXfJvu-o;Xoy! RJ6$ 2.K+ +++ RIm~(G$KٞdaI60da9I}dpy"BGrv`myu!IVeewYK$ +Hdpy=VVWٵBvdaeu).c9]ٕo-ʧp,d7.eKفKKف5`%]$Xf+ ΃~~ "$i5{$ U,$XN=+ =$^$Gr``-a){J6dX"!J(rtP$ֲDze{[JpI ~~Oh0- " J',g, wdpSuі($"'H%YMVp版'8.}#QD9=즥R <z8"'/2_$" a8"?t,/eOhi!eR~yy 'Q"D"" ȧu f'ݴ{H#AD!v@P!$Ч'/ad){b+MK Oď<"/$&BAJі0r${6G0(Ѕ@$0pdB hCGgc5ŏ=5r񩑧p 񜏔 GE~@B. 69=Sxŧ>v)l.)RCpD Ν \$B)_hCjeimu-{K+ ISŋ{bSAj 5m$<'0r Λ8|"IH)h;XɞZͮ%8ﻘžp /lHptAR.E> 4̓BM~ V]Kpt##.>5rqq(:GDQ$8o?MR8arF>z7{F$р!mrAP* BcrSGGp&DG-CQh CÖ)%8^xŋFw#8#"E:y 6s$ m6R.^?p੏]S "s/ ΋x 6Ah aSZZF#.ŋ0σ8O|x! 8h@ia){Jk .|c/^|SO<Q_HHD. $d 쩭&HpTO}⦏`H $ )qy/" }RVG32!{ }#Iធ ^À#p.р BJm @= $8|ŋ8ѧ72Ps=zDAH) m d5{FG522w1UD}!M. }= x("mqYI$8.^.^8ŋy '{J[|Oy( 0$]H[/J%B#FF152y xDdp2$|G`J/$/^D< $KKٳd>pcŋOxŋ7epO |zDPz|@uI=lv)Y1bj.=%N'{ zS8k""FQ(8/;"DϋۈAA/BD#1$ I`5{jk8=#F0r)=|JE"s R =lA8!$ 38wŏa`dxT() z""u"`D4$Ħdeuumm){>z}8HDDQ 8dpGDϧa`Z -ep< p>GgOB"rD" ID (A$h>[Vx>p)lIID$8Ѻ/.Q( ~yGD $G"<lJi-$Hq\P@)ND<Qy"ǒ="y!_@""'H| IגC|GPp_#C9!Q_H#r *{ C $S)zG䖳Ǵ"HK'L p>%H@Ǒ,di!IqD$8x`NR&dpħM$^ P ǒ,eg)H(x$"v>Idpģ-a'ȉ ?(YXR6MpN<È<E$9'8 VD["l P*PH舜HOHpd9{tr6sx"$G TaF'"R'"'8 Δ6x 9" E/Jy#"G=!=嵕sΏ;GAD!EBG#_gʧMQa@9HDD Q$!aBhV$8/<Fh !Q3 hQJ Q8" =z|D| YXJ]/!%NN&JBD28KBQB #QH)#?Q_$,,,efyay9"<Q|xt, s#"CHQ 8 R@|6QD~HD>q4 >9A*ROD`_rVMD v ?G N9/RǓYh68"|"rHID=>rq z0B" d9{dk2΋QO!< T0rN0D|ꋜ C?]gHh> s)q ᑏD QOH#(d-{ K IPO| "TB lк0"Hpd%$"=|!%M$}$K٥Vײ=K ΉT]BJ90(@#@'h(<YgJ"8Q*DO/#/iHaOrP+r6sQሼ 4Q$\ @$z|e#3"(@Bߏh;}<#)G}> 0${IVWde-{l6!GD! )/P嚾Ŵ!;%+ #XYX..'8B0uڼU=I 64 p%rt| }~yF| ţOpѺ~s'WuDb" W.Az/_y5}jQDֲ+++Kكf{pRTc 8"|lh zMçuHDQq;]jUʛ>!*| O@Mb*Dlv-Y] N05J5Եo }Op~7D."2؛( g.r(W˜YiE*q~z `曕v]d&o11J.2\jv-YXZ*yP/؋'&T ȅ!OB2؛OsDNȧ"JUkif}m*vt_\|j1_'uS[\W2vHVd-{'TlC5z*uz;x Bц =H!BȑsZWJCbѵ2_涮RֺUb~XlwtWxjoqbѵ2_V[JpBԬRYP1.\Ժȴ՘[< LsK+z];>\ӷ\jKVWYYXHp Sfg 5P" a(!xtGpu_ Unk]U,2s[~ n63*mmUpM**mma kk +$8 jVmhucHHq@D8dh掾UxyB&ҖۺU*tyB&Җ53`[Z7疶\H0,Y.-e+ϪM n D$C@oAI\DrKZwJ=Ufu2s[JTn3ԺDumcc&!ML\ז`R62N054iH t" !>n<ѵ jRoj]j':2s[JTn3Ժ*Ote暾<\M\DG[ 6%+Kٵpz^(凴) a"GoEEE?GUuYf)2K%-3uDUn62K%-3ҺE- eJZ[RMJv)ZTC2I=<ѡ0 [C{xQDs*;ZZ3׭mm[\%rKmuu[[NjTӷxZVI붶\ iai)lve%v^ǑLmjDx ):{59dDD#Gp*Ok:iT.u[*QUJGk]mWhmeJGk]m'r$+k=e×~«䗾3qԼjQO(EB"=t8dOGhOUf/qWC\ s9rOptDyd`\  WqBC5ԦԵkj`|IH;Uy]J=/}DtQrOzQR"rZeNӜᙙ+?ZT5ӗD0da-B$ig/̅u_$WXT6]0ƨ/@_@DR_r\!]O "H)J`d5e- 6act hŚ]*tM] M]uUnWyCIWE:'ţ8: " z$69[2 "sD0?T 㜣 769H@I֖qb`bcVm7[yjpM,^+xt(hLDU.j]dZG"ssŖ1;׋q\li3uѵb|.qD(HH)9C2IBQӐ!<OyֹT`_NúY-|`Pf HID̔b37uOE^WduEfnW]*'X9^a DO|rAJH#"G'a7 -8% NAnjV 4j*8:Ƿ\TnM0sVebf+zeSq޺ZL0W{\rn79U*JQOu"~`SaQfjbI:j6XMH}/VS/ҰjN:s!D# ~<|JEO({(N$=S?O܁Qm H T<0Ux3km-aODep  #aO~hn OEl75_(YLxW|o.gkFo]$.>2&r%t|""}.\q;ڡ#$uǖbd;wn~;w`eӲ?kkao =ĩKH=ĩD0p{xʕ+a;ڦ=t4.Nmb=OlTlkm.׍Z{ݘmҏ1M~tyyGD<$n ᚮexQ85]2s ynsC3E"zZLQ 8;eLZ4fcḋmŘ-cjQ3jSIUؤ{&/:_3\ufn_-VwZ]4͕M^իW[;Z*eR˕ecc |ݱ-c֖F) {iٚ1nӲcJiL-6b3jӲ1lL^5:1^= u"!a3\ue`k%ݮ._nsQtt.E._պS1ӱ NmQchL֯ƴl͘iي1%4ƦlKf^7euce3ڲWdllR G& a/N8@f|:37ovtg|Uu{fGיuy ($ #'uǶ@3j׍iٚ1nӲcJiLh̨51-k*m٫t$wccwRct:_*Q(a< p pM2R3\uf.isOsKnsOsS'0W\:f:6/xt'uǖݛbdlkqn[nֺz-z[nVkv[ndm-үvŘ'=E{r>9$D; 6e0Df|:3tg.]]n _muj.3Qoi3[ |mrCf7]SWiO8y"XA@f|:3tn _m+֝@IK\%" n?B.=xOw_}ӣwmqo\߼׿7?w_bRN~Q]"{ A t/#55]g撮<-|Թ[\%".k3H@\ xo.]hؤJ}R3FԾ&i/|k~6 |Qygi-&(hOJ14C̥^}On8]'8Hæ. K\ֺ3\0wtLDUE@䰝x8T.j*vjZ4t]in6OsSzVuOź`nj]'1 zP<= oZ79F)G{saL6|3m^'?~׿!U!a"8\<̗+|`"OsK2[:a=Й`:fr#CgdoL5&ц1uB_r sww &Gcplw9nJVe!Ls+s4+7VRar^*2<|^*cT&ᜣD^H)}R/8= hO5TP}80N_sG0y0Lpo| WpS3pC/] R#pΑs9""AD NOǤi(J}z^]ц1u2|_d(y~7}4 mDppm623_4K\rrC4é+\.]|RTSt\S"rsa(8=/|&URFR/rwis6Mh~2ws_@$&8 vGnTCh9GF<_" hȤRjvj{6 hcR_3wT- y"8 y%;zu/sWkfwf:G{pDR_ B qRRP} \D6cjPPG+u?;G`h22ҺRD3՚yzzK\a˗piZ3OOO?) RaD}Γ *59j)lK1 R}4L iu_~Q(蓐z|1eݥ囕VGk]j 2s^Yk`j 0k.U.sKV3f*-! !ι JF? ?6c¢Rwƈ RE$10 Pѧޙ_wW^k?R"'ߙ/$~a@BJEc`9UnvZ3qt:fj͌KaCnr?__FcC$ 'Ǘn2sWkf`ZhL~QP*̫L'h`rRNccJ}fcc+uln̙]3?/"|deN]~ࡇpu5+Z1sWk+Wf[W.OsLZehU:(5h`Ôj U(̫"ޛ{P Pħ=WTsN$mf@]yB{:©uS̆D2 sܬJV;fKJe\-*Jeb(V*.O3R3s|~Td QhI+kRFcP;AOnrTϷXk=j^x4)7+s#a 17(1ܬ4cc̍/cJ5 &|H\2Ûpiy4.MfxeDDP(`O.mSav~~ZԼV(j=N&3)9(`OS 9!%^gUZ<gQ(~DIu i]3e;͙N1-j5ssSEchKy%pkzu||R>N&3)9\ z$paR)U@j *Γ9>zhO*P^4][͙:6}ϙ]37gZu6&7=_1%AJx'8 ΞO?gZFczGkǼ0t䇾N*{@ 鄜{O9RjC}s*>G{rwo%c7ֱ̙LQܘ3]fn4͍|>?gZƜ1-i t2D$ qZh*5[!ƿ[>EaR KdXkmL>9:67)jsuֱ͙LQ0u&Uֺi&y<Jst\(\zʕ.IH TK\eqq ;L5jIvWmKDt\>9:67)jsuֱ͙LQ̙QKf ֺiEu^ 9:{IӗB4faJ5*5LsjB)8UwRiahIeTns}7LOSsuln̙]37gZF>3Ec3gb/oM7(^'rR9:\]pR`GvQ 50uRԬ R/?gZfZjMg!{F>mr'FaH}%%\1;qvecJ0z>7p&Um0MO5:67)jsuֱ͙LQTQM epGmrQ(H|+\ԩ.qY)* VD4ԇ~>oa.'CJ}G'i "z܆[wRMeL[J:67)jsuֱ͙LQx4ֵzuQy g"{CHH8'$Bgm[^m.qY똉^-@u- ROHyܖ? 3jMjj|~͍9պkLSsuln0jMD %>(B"g!-a?ec*sz}fDzwe~f\^or9qz^陵sM>3ƌv+vX6Ƽ2gFecn93Z.s-G͍93Z.s#ϙr٘9cnWbcLoyJoa_NG@h;Gc>K5fny[Zo޼k*Y[ś͚L0ZtlymRټu73 9M7^y啹YGw[?̧nx'ѯ7RԍT~F*>؛oYvN"{IGn` ΅K@$83|u\WJ\5fnu9ֺ?\:uZO0_:q[Z?-. ӵQӓO~%M.A v8`<3r]u]c4k.U1sIoBc!ubJֺC?I-E\D=_o)DZ1uc8G͍|)|_٘m)_Ѷ}L; ΔO@"^Dxq+\zS]cV.k3wtO]⊮1sGNJt9%R\: YL=x0?~c+fXiZ{rw~aJS'?/˴6nLQؼίP*˿-R>Okwnb}[Ha5Iq;>rY똹.8Wt6k=J).k3w.U7o"S7O[1ucL>6UW~;j_55mS:6ﱶfSQ[k6e>%=mb{>G^gH1>>zK\1շ]5f o`P\:fR{.2ޕ\VLuݔMצJÿck}=-k?-ZlZS7ulneՇdkzɖGm[[KR~Z[7{Z>3~gh v+,7fg ]4i]d.k3t'> .3téX KJqY똹oUu 3؊)Z[b=c5[7G1mcFGMicQ[bRM[ؤFkn*1\Odpv*^+\UR8K5]uWuu\:TQWuZrOkUEߗۋX51)_뭮)hkmL ^æo45|j+6kR*Ջ5[7q5M8NT3"p)&|  k1I|S|enj\.NsbO0tȩ[יc'9u\םZE{s{k]7h֕1&6l}3ZژxvMncc-2fҐ1>q*$8qlzWn_fT+՛eџЗv<+ss؊ecFe5Mbk01QS:6flh^:ZwM 3-1q:~XJpL;&1>>ü:8 .|=x]?}'r۽.cJ5 <=950_m&ֺn⢩hͦemee6Ɣ>lZ=uczK';OWc>q4z?x!I,|B//?ҏO_9#cGR 55dv &ֺnkTfӲMS.ֺlöK66u0EcSgԺ.qԺh^O?zG>'Wn?ӏ/.~ӟ&?cݗ.㟸0Ͽݹ]ng)X@gMu|MSњ3MMS:6nfԘ1S:65ZFM_Win#'8D2ͼR]p?|Op^a?z#jalTA/`&ֺn>hee)j]4ﰥvӘ1S:6oE-/Ժhso|v>0#.kj~D/I0Sc'~°\.|.\.s>LnԬRjvq 5u7MEk6/x4. k]7_Y66ֱyg="cZZJ)"Hp$qep ߤzP $8N sjgsiLjƈfզ1&ոMB&ֺn>hee15ټ?`L[km 3Ec)j]4okuQ'8@c+̫TPµy$I N}9{}G>w!7Ԇ$m3PhG>3P(PVϿi*Zycuټ)j]5NLQF`Xo3>vόz/?j\.R4lR)P <7I[]S?SWBco> EؒxFCooCh?L/|o)ճ?ȥU ET/|f8#Jذ5S $ 9$HJ$FF`K'uw>}4I=?V^ M=}=rGT_V;Z7I#_m7I[TǺkJMa€D v "@!6%`]-Iep_u)&߹P8 =@D }ɍ 5-OX[qdĝ;wni&Ν|D)H~ȷ}᫖MlK܅lbGƉ}_.RL.@@I5|ng_x9h]TC@}" DD< 6$8H! FCKpܱk>lڦ~ڲO_[j%&ﵶlv;8zAl(y5Hه>=ïSʥ99x#5# G0 OQ*NIpOmm٘_iZ6-1M?1Ɣ\l 4kMl7?\)Swp־Gy#>C;9qq8'E^@ QQ$ x8+,Ipغ1EpǶiZ[6M2imٌZ65k ۚ1Mff+-R]1męJO^H}0{\yڛu jLhq("' À68:OO$ؐAjmӵyڶ1ukmc5]k)Z6Slb[4}ڗ> R_C4R (z "R/]~(@ N#I!֎S=1溵7lӶ1MlZkLѲ)[2m-cWT8K \jvphCCy8#"R. ?{@ N%I.x2ۭt+%cʥl%cʥ֨)[6k1E\/Ƽf׍ۢ1GDp?xц-q(q DhR{"x'6%>cm}VL_˲m,Qӵ֘e3jR]kĥQcFGO.yOrN.цFg+G)s!AHDB"Dl)$~xz\+5krX٭X[Œnbbm۶n5k݊m_}ۭ۟#"L~vTho1DHAHB"O$9_* U(.Id%yҮ{dvX(a 8#^_}.G4_( ΜPOE8" zS@_a $ '}ǟO<>yG}ի׿៻~owoׯ_yC~o^~7>D=RE .w$mpDNp|q  >40 P6U(epDm0 8?s=vTcQ{B$TuHx'OH "S (plI_c 3}_߾_hX$K|" 1L$ Rߘi*P@_-ģ-83t8Fpd.B$ #1w Ե)P@8NKpjEO#$@ED|"SbC/#[hpjS㴋!Np #8($=A`;<)Ժ6L]!2xKt|ެ#S{BOND#)`TC)u [_~Fdp2'Bpt"0#R}cJ)i t( 80lFq B"8kjm{G&9/=/ O䛿+b)f f*B=2228qLx'nckj{{^5^"o}8PV@ϬRP/:>dlIۍ^hsRcWtTDZML՚}D5 =,`}P SlOod868766#tǖݒmՖI{R?g}D5ep`O"  )`6MP(`8qF[sі13FGGO>y'}6G?=#ϼSzl\o?~Rj8µ٩P(`@MaXP8DZKo9!mJp\eeko߱]v|\Ga3%G0>>kpj#8RR8q2I"6ut؞uS,} jJbfֶ͖mUk[bdkMsZZX>f7(ͫuSW%-o !mR&@!q5]5Eh*&UbcFMt]c~-٦jmŘQ3z5Mj 0Uu*u K;emRC@!>s5vwi_鶵QhJ6U1]kGuӵiڒTlz"ŔGc^ ` +'6#dC@p\Oغ)ھۢ)֎6Mf룦kmTMڦjᚵmcz)k0`˫gǑJ`.q=mcSOܹ󄭛-mhlm-6f4MrMZ4W-Mlz s1XN $ c<H{N87J5ԆF_8n[[lk͊Yb)ښ݊{}KJ\Œq S u) ` ?ڃ׸ 8K.WS%}ίoq3 \mWr{ȱؑ72"|#~BrZH oLBm%d -{-,u|uI(鱒- xg[{;;[mmakogg_VʙKxo{ů>Ƃ>Ľ{yAkYX 9\plrak%=s;A" 7ʁDJ:ൖ>k¿,,kauI߽Tھ}ђyܠ]b1#Ѳ[kk:&(X華2bap--Ĵ%8ζ'JyswnߞMڕ#xo;sWu}$` [E waZ:Ol;g=~/;xc+x-c˘5;XpM 7ZKؗk gޱGީ%Ȓέe[$-wŋloj 7^Ǒ:nPKsRItNc2x;x9vW:XZLzŃ/KKnWR]-K9:+<&d=O9wKiL̖Z_뷭J(WR)X#%;y䧞8ڑJ\̪r߶*qCD0-Aw9:WڞsabyG>_aDb-s><|O>=|!7$=|çO<W%nȦ/wJû ;)i휵GsΉ&w缥ۘ})?D$z*↼UrlWl[({@%msΉ&so[] rHA$IEV_ϲUH~hdYIHRIT ZJJ""Q%kI* 3kM9>?x_,9o|8\S% I܊FekpI..w!K1Ɂ "Kr 19lo;m?~8sLӞ|fM"il_H*7d8b$"7Hↈ )F"RqCX)̬5sm;' Y_y'3z(FJ×կeMnȱ!r r 'b!"qCN$q9!fTI/9l{?qݺz.&e9'qncKP>s3ilD[DP$c"!"qCr.0]^ip \=ݾi\TT/]?˹$" _V 7)HܐH"9x!E7\H!RtCŮ.0U]r4ՖscﯨXBf"LL9oӵ/eݚn"|7!cDr\*nED"!"qCVD$r]\Na3qe}׹oK!WD[9]trH6݊gH(")5DjnYrDrBnM$]WDↈ [I}M\jL]A?^?B;Ŝ?<SU~Ou~4۽Ac~nG}3n0e[6"[[[;DK D2Ǯ[)T| "7wEp9iR\9>Х7ЉEYBҥ:#`ԡ~Owܝ>}VSw_(K|&WgUZ-$U$JEHR%gT$Kj$-&"xc7O!s\2uIF~m5jqdz*娘Ŵ萟diR^)"WO~!IF& h%j娜|J4|_J>r䂇?SZZNJnWr{;rjgpowgooo g ɼI$Pʸi֚UNCV*Gդ)1QUZ276O)*e\()sFW+s0B JR{c SԣDTᔨ;9)&*EPSY2U5?$t$*e̫k2H}c:R"a5} p1\5 (cޤވ j2( I8"ʘ}ZtE3 J4)*e>F*sz=:ro*×S՘}E\2fn5H3񑐫D3,M!W\%mpfV)ef"#gZ3s Ӧ"G4!WcSBgnRhSD>AqJH.SB JDzT2*|*.fM*\^ p {GR\RڔqJgnRhSD5)SZtEp:rPU"8У (a};1u1kꘔZ^Zzky Sӧ߈(:RF#C|qJ*\\%r2Ne"rBc1ua\3/ 0-Em'8@Tڣ>5DT儨Hcx=prPU"8У > FJc,rAkx^"0-C:Q Q)u6Uڤs:TjE:qJУ M1\%S=p(fޤQSVkz3M4ڴUS#jV9"WHmjV9ݤ\e-ejSy1\%S=pRN}"?b昺0L h*W{9DNaJDU.Rc1qJmUy,) s5MBqJG.SCtFz#tkW|={ 2h۽uc.t$jL]ꏚ[׎,._wv tvn{AuvnCD^ua֥u|y Qf96`UΩIZ׭eGxM!7{գ" S-wzGφ8S3(\j]D zC0m:`,X_z9v r zvoő`}M%g| y q,j,kB?G^%RArs:u̼'<g)̠`}YsvχC0^rN܂uSHaڊvN݂5)C(̠ʲv`݌!ަ0Z++Z;Vc)̠+/] 3UZYr]1PAAk#yX0`yy{q3fP8aݜoSAAUێ/`0Z+KN/~10rIk]k/~_C0`eW`ݬ!SIAky}e]c&Vtk]jD)̦`e}}}M/9uXLaFZkeeqz nx. *h,]q \GuX Zkk+Z59 3,ZIRmAkSuXסúUj [uXWSxoa]MUu5VWuXנ`M2e4h ,)XFS`M2e4h ,)XFS`M2e4h ,)XFS`M2e4h ,)XFS`M2e4h ,)XFS`M2e4h ,)XFS`M2e4h ,)XFS`M2e4h ,)XFS`M2e4h ,)XFS`M2e4h ,)XFS`M2e4h ,)XFS`M2e4h ,)XFS`M2e4h ,)XFS`M2e4h ,)XFS`M2e4h ,)XFS`M2e4h ,)XFS`M2e4h ,)XFS`M2e4h ,)XFS`M2e4h ,S^~IENDB`mapproxy-1.11.0/doc/imgs/mapnik-webmerc.png000066400000000000000000000305511320454472400205750ustar00rootroot00000000000000PNG  IHDRkXTPLTEh&#[(Yj[fQW)Xj&1j(Y\!#+)+NU(T/Z6A02\_˛Ǧ ,g8(.P._VËͩ&ks86!S1YSH`Mz`x_׹פ.~ͻ!]/.~+,3A/.~.~/~/~~̻!]GGM449ggl%/L!]69ETTZ6tty-.9-/9Hͻ[\c!]!]-/9-/9!]ͻ!^!]{{Aͻ-/9ͻ4-/9r-/9ͻPͻ*.\R⡝5B;@Jx$dFn*eý8;A@'Dk<*w/B#c8V ,%gAÀ~R75Fv@‚(,K,u>_`h[0~768Vx)T$S5G[D#'k$Y"c"b?[ljyv͎06KUU(/E,4N(Ks'A([v#`74&f#RpH:%L]}6ngϽ3g}, Gؒ:`SκAdϤ1f۸Y>SDX1tIR#q 5,`aa))a2WSZmܬ̹"PLPXcQ(86 t"$i,Ij#mX<{߁#a"i( 0RHnlZ!57fI?{AФ` P4 Mu`(aVWm;yܳxq)8fXd`4ÄCj,$Ɩm̹s1]l#Pp`l` 524 `'N3kc'gϽ38(FgiSp`: 0 M$#<;lƎ|b [0Ld}1QpPI3M5-4ݠAdÞMcֶmE$E bl hLKg;Qp@$KפdeilfYSTjv6SStSR1n4`6t&Mc]7ba0:s)۶gS;eHXDc34L6Hlli.&dKfI56ԠYl[DRM4JIJi0cg A'CRBL2M "0jj7qxYMXX$"5R Mg]Z&1RY]qxLD-̴dl2M ;Pp0 ]7 ô,le/v16][3cFcG "24`cǩ٩]:ccWE"3`lk؝ѨCG3`ZDCc+{,N kI[醥50]ǮIK lD$"Ia3{53c8]L,i;D!dZ͐(8$5&CЈ$[dDS͞9uh$}An>S$0-IDK!ɔDfY6[N`Ԭ#0DII{da)M3bQA&",,ɰ ɩN8<6DlaI{3,FD$ CNf"`ih&"2Պd ΌR6O'2geIS  "NhѧzQ@c$`D)Tɐ6,ZKm::uEu]l:IFFD:I 2$Cc"h`V` "i؆U'?xb(3X(3MJd2jJL7 N-Im:kΠXܙ [AL"2t0 ڏA[X1M&la-a28Q32ʼDqB28QTj,3gם(MĂYӐЈЩCtPE7HihH0[tWZu3Ws]jՙoybWU0zScfSU{,R43cKg$"à[`04nꀂRfQgʨb^ `E-TU_̫jF*NR؝;19)(RLѧk`&aѭ0$آ ֱEH""IDexynfd$J0Mc65I$M`ݔt :LtN҆_!:jXP|[p>ΨaA~WWTFߙclΝ,J"Ѡ.`K$IiDul66T85̫j԰9:_VZ.{Lje{rg&KBuc0 :mItC% ;8^Pj&*| Q]%ѱKm}s"Q !5D&AL6d 606I&L%PLF=O e*f(TTVUXYgND"ˢ&x~BngL4i6t OO#!)d0ljتww7&dIl "MCE|.w2!%eS"sΏhL=M$OZR5ld4tIYS{{sر԰+J"QB4 &Lx(W(WT3f2t`vD'$uXl2T)l'u۸k ؤT}/SΝZEZEQ&3qnI2t06-Ӡ$eI.M:LF}f6ecK|͕D׌[B4HѧBɔsiQ UUd8&(Cg 6i3I$M2 `HdgR=6n}{O ۸d$ H4(aцѧf2y/_^d"XDq)|gXAuI[Hڟ dfh $˒>{,qqݒ(Ą;!hɄJ&Ν'GbjT 6h}錄e55)-n63FLIbuEgh󑓡er& zdTWX-g|9CeJꑦ`vc0:4d`c6vf) !JB $:~+q>V 8g8SAI]Rg@:6h4C~tJ 06gRc3؍97L(mw!T+AU+lV3f5sgƛ]y:d:$#)GZdE$inЩd0cm[̔D*NQZ0M"2 ѡMԩK3 !iO13u ]&0$(ݹ$d&ft)蛞XR`֦ )iw1,갘-˗0#N 1QBLXzZo4]g] Dc 8 "S^8|rx %OUh;XgQe.9WQr]uȋ_sj>nЉ`ܠrN P qrEUEGmMUQFUB71.I4\e#O # r5(V0:mc06$"ɸAAZiB،cWy/8Pډ!Et>elFH˖aDav4rnqF qbK-~SmԦ.:Nh4t3n)`:=k9ZNk.{WΓWh7&Lb\6^ہuS٨4^6T95vxt FNDSг˷atZp+ITM_ݘh3)M^|UWN>z>8y'f 1:JNJL1-tTXVV<[t^i5V U ИѰ˗ߚ~+[x!}?ޠ9=$AC0J{?E,Bnj=A!# C/|:7+^. ވ_]LhRuJRQB<"%ubm6ڠP6뤅 fVk^9+'hru)@}}>t*+iaoKKf{s?MІ B4ѠdA74W&Ph&=l2`릆 ֆOOM^[kZ'dy:i%t64M!' "'%u5bkхjP#~կ~uIu|1a6 OֆONgj4zز :2q.!hHARbTu7;~.hוB=B3S7,WWӊ+cӤH)i ,$c?4KRI@DǩGRbTxh "IDfDAtVS jkXUՓSja\mTuxzzXU3ڙ I'̿jY&æzWcTtq*u5*v544v_ꉯ.xwrj5Q[~ HmEB I$5Q|-k؟FT*}BDj"4Ai7?uH[ q3=A>}"V :ㅊZWU' *jn$I"2 dqxto)? Sj:uI 3 BnTl=Nt׿}M|CN(D ؉J ja|e-_+y^͊]IJHJai88I$Jw/(Ă dMR $k糪dϞn'HεߛN2$ hYcHW SrP-Hb^m:Ѯ$Иui:IK~֝ sW(Jb]I`R Dm_ˆ?~P:U_}o:;H";S3=<<5'NV ɠORڍ: NRJ }Fx[BL] N.PA !Dkg|6ۧOV^}_RCCC'N>}bh۾Kg%5FAϊ޸SVj~rw9e]dh7 q`3A3%J_ M%Y }FŅ5Dc~աlස\>}"[*j6ȅCY5̅jХk])虮DN!?_9Ns1'iVu'qa2aNPfp'^ 3,ޫBa$:7ĉӁB!{ZӁB!{#ǯ~.lZ'W05<<|R['[ᵵV&C{[-` b,m-h8:qlϪz:ٶζlEI])8 a؇9A ]&g^+IN.4DM|vD':YWO~pb+ٶ煾t;Ē&saW%`M?71Mp\WAGAD=BQ!Di]%uꫧ}bl9>-N70 SpP&q++ MR!VVvVg*%~Q_J8CgYWO~rmگɪ~`M'"'qíiy,u5Ĥ y9jL a$:F헳O8f}bl9>o:[qxcޠIIc 5Iq0ka=(>Q'Nb]퟉R$mxx?eO r~6'Ζ㳪8vȲL؃c]Jڙ԰ζ/d2DTJԊ/Zk~-SBҺ׉eN5'Fj^='F/#H0X4ЊW(drN93DAJw-ibAf fO͞:=;4t:=q";&7::{Pp!IR$ t2*Nndgr|Am~:q%s8B"ϧG+A=q:Po~C'N=(88[A$:`p0j!T(V!o|SU+~T'ՠ:^ϐ|]7K"fۯߡn١o|CCC?0KgH0c 88I3GJ:)q0Sl.Ū_%/~!'F|9CkkKAjy"աS_CCC%C=?x)ǐ`F3v 8( % lj8r\.JYP?J;\e~+?8ޏhSpm/`0vQQհ?><<(+{^FN%|?dW͊W(g$HXk_]Ye٠e ^;UC{8xa{SqlF]6%f^tsK+u'T BFѓOJX($t-wkն7 dٜϗvq;.6eϋW`lŸM}1C}䂋Z}5JWQ9CV~ݗ+/?TT5C _HvTz~9Ujl6sWf|^pI;8qa _mï/~oՙ/KW;$(K@A:v䴃jϵ_ٶZը1ν|sEԠ> /]{WUo7v9ʖc/x8ϵ_  ~0a3l6ۮ8ESc 0͠S7~ ?tUq] qboP?6rU\]sWߘ+٠G=mlB_ؗ.Mǎc]_j}66[B\ _Xx6}?O/~ ?|b6)Gq^vXp]k~ė⾟{ 1J$=2!f\ul k7fv6d_UlBDflPp1}/?/;vH_S6ks3M ){1 sI.fM75즈"E(eniEbA4 2,-HƇKG"FQz$uka$xdž&0ҘM"Qpɿ|`N.DŽ(}A]JBy+_wm'p~뗱}"~”D !tU%[jI=1-fF]E8N˅u'GKƑ|e49{O<B}K"%BaZ [a"`Ot 6:lE)e{~8[m8[mYHSH~FǯqRDR/v"i{m$le8{sϋv= v= 7gGNA]ÿnBL&b `ID ؓm#Ee8|6 qv$zHv})ۼs:a:NlɌȱcޗ@}2A vPaf$:ul1pU (q$ׯ\x+w>8yG~DGwW5.$X#LfID:ܥ&| ʶ 6sAcO~_:=J}QJHq`l uÄnIkΖ!htrθ.Sa۸5֠]X:F 6n(bl.[8YDB,;7KlN`et…Q9Qd줈ei f hT1931԰Yp`&Iڝ͊p%_I! _(;_Ɔ"vƦe&ؒba4D Ţb}ElR᠘v#;o7\ŋoy{}ၟ >_#bCa-k+_b4@Z 7l(EtذSÁԸРI Ÿsr\Fb֘5.p\ZZ l(bcLn^ /~:j9Wjaq]6:j5566(DI$J .^壜t:}BREDŽ!QAf.ٶ FRll(qC*%!J O/.bC=>eSpZ }5j8W?B'u̵t:}-ƻAbF0㌈ :OE>ۆ.;հY qFKB&ޚNB Q¡y.{?{?twzXxul`EE+FkTI=NSQC 4ws;r0j7/0 N]Qtf u91J]B4!N?qJ?\8j:w{Zv罺ZW^f vS.\tMao4$QS8k9hUn/VZU]Wj=z:6S#ޛ6QZTS^Sby2GrAPDAj zU'p:E)K8++SSSS+Әovn$ LOMM+SS؏A];b=O|pR%'V:պ#l)8V>+SwyW `5^*J0W+^.o^3>4qDW=s#0mrȥRK:6Sp4SB,b);~PqB #\>_.D0 ԣ._?t+C_

p'+W.^f f8՜ZVZ9?ʪ*B%S܈zH+W W}5PU57&")- bF199 bXB# jNV93E_͍d(+׏r#Ӂ_ "'_pMg*wbr7M9l% d ?jí|sj|LTr ~%72V< կj%߁DWV/<(83ybA͕ARʢ~+C+NsX6 [(V<ߛ^ a<^j!կx~~+C+Q>n"]L&oM_-a?V% u-hFjϰ {ͦ{'.j3jz0{qtq(8驩iS xN<=+S+XJLOMMcejez4n([M`ǗxPp˦r>UV0Q;+Znj:L|7u-lB$` ;Ss.6[z"}m XΈDd/-CmND:v+{!6s%ADnF_Xm,KmN:%4e%(Yob)m&&I7WEFK nw8Nm3p$:>;)m܉ 3N;PzuKQp0nV,⎥6r!qVr]lvrg& /bwEy| (V*⎣[;obwNS0 8NS0 8NS0 8NS0 8NS0 8NS0 8NS0 8NS0 8NS0 8NS0 8NS0 8NS0 8NS0 8NS0 8NS0 8NS0 8NS0 8NS0 8NS0VKIENDB`mapproxy-1.11.0/doc/imgs/mapproxy-demo.png000066400000000000000000000443341320454472400205010ustar00rootroot00000000000000PNG  IHDRw`ɦHIDATx\T>%$7nrs$M7uwQ`/;v\޻b۵b/(***E&3<3 `<^rΜ:9=555 ""zp'Ä&DDɃBlJx4lyIKDWJ3϶k1L&DD\ {0!" M0!"" 1L&DD  ÄaBDD ""b0Lޫ0<52Kyk~v {&m ",L;ֿ4|^G1 D7^|<0 >jzعJrj1L/LǿiÇ,AyQ<& GatTGG0yOä˪6>,|cއ5B0y/Dr#ӗ?gIGeo "ɋ763(U5^%Cm&iದ<&Ka17KZ5P%4 v$FCOaƅ7W}i]L܍p{Y"|(JSU=ry LD٭YlIT#&:ApZvm0Lݏ^} @l9A?>@_6r6*_AhcPFuaR +ת$ צZT2 p[&/-\6q`Ah)TeaX˂8 ].ĦcWq7:rFɡgnx@}y2ukn^<޼3w``k#uҗ lly5a.ƝNg1 jue1C얇iáɩ#j.>C4[Ѿ{^WEe6g^ܡ\XuZoB7:L$ehm&]0 ,ew&6od0QD^`oHlMXcҭ pzR'^Ѡc0QCUY DNF׮2lL}7B4)Y9HOI@d-,Ҙ49ҕQMNE$=Ҫ>Dž%S:+ȮObHE)EB'MS"Mn>JzAoUN6oZi:^RM 4L4Fi_$LԨy{WcP7X>G ¤ ; U잗=+7LHKD:a4T(2OQQSc"aar6X0yD:9e2统#LT\֣ qG9ªDENb?,Fm^Nny\>LLܟ 2N+B ki(3lw#;] A&oy)b2xJäTϜkdUe^4W\C/I,[S^4-^3H7GNErqJbE w!Ljji'W=6LKr6Fm&jȂV=f3%3iꀿ1Uac#)Q |E쀗!MƁuʿQ[W}5 N w%LjP˖`"e[p)a"ǽ6ߋ=꾍DsG1> f. *K`\̣UlDpcA3m[&n ;މa.IZ& -r=5nGkHo s %W( IÃ+{1 |휖~aMXxjʐnFӵ<\=/v_ᔑtCB5Q]q9*zOä-[a4pEzc?ŊÍ78aLmAt_,]#orh:K"w` l{cu.yًNc37ȠC9=0=0@%k>Gxv5*z¤c}gkyDsiv jIaKIK7Cv%RP3ya3#kG9:H{}VnIͅ.FCafddL]oT;&U/W=)LtG2DڀNp4|#8Mի耿X2i;ѩ'MeO^ƄI~Ae^?cMpqFs/R8l}ۙaRQU]CӼ WcU&[wK燖"&W0S[S&~nbO?0a0LEe]*~zϡ;&  iOISgWqÄa0T!Ӌ-fH}D  ÄZR by&  |sI'AlD  ÄaBDD,LV=m]my|ki֗o,&DDD ""bÄ&DDD ""bÄ&DDD ""bÄ&DDD ""z]abƍi0L&DD  ÄaBD0aÄaBD0a1L&DD  1L&DD  & DzeH{tW{aÄaJWPo i =@52ރkf|U(KIBd:d´j9iqj,xjO.qV*5V+PfhדaBDf!цC SWjJ"i#iW>>M8ԅ jY&䠺ԙZ8U~/S45}ntÊvh2ʵdܲIn9B@]•WPcs[A "zdN$jjmi[J{M)ӗ?K7>Q3]]d䂉7a.2u:9MpI=a')Փ原m kJb85UwvlXo&OƌxmeQ b]7 Of47{v1< d&㽰>LNhd cDH -r[ż0k)۳v:b̭RaxzY7 ZnBDԆa.Yu ;87Ūw˹!(Qv& /ө oai|Xj &*i &Z6ݱ.0mp]6&bʺ=6z?$syPiQIqmf}`s]#]35Ex0~^]V!8=9-,ӇA~<:4gPTPsP -Tox}E`:3e\HᶘrCV7?uq0l8/{0a &VߐwٚS_WGA5J<ԣ:laX_U,?:\k0!"zk¤*z03Z&^׵~QP a2gVׯJf<әÄ %݊ '-Ie v3(or3nA]w+Os#rA_ʀVU&HܞTʮ:DUC@DԶaRq9mv zStބr,4%T zt;{aW`"#wH =EXj!d0aG}ݸ.(ߎC"W&U S 9銕"1GmQ(3XU%MyuW]U˖*a.μ45IBIUbw?Rfq8 w{tWP2!aGok,:9ʺS_:Ր>:oY]Oӏ°H) }akUN23U@T&DDa^- geBDD;2sB!"Eg+aXm"*@wUO* ]tŴLl:0j_on9¤A {PF]|=irzVE0i.L>Hy&Em"VՇpRW"v;L5ZڛaN\LA {SP)&Dž0i03C޳{2(;yr9ClBF]u`2;βpsB#_um@zU}`8'N)%; 6G NeHԜ~HNJʞc;6Ĕ붵:? ۀ.q2Ar 92 cۥ6lxҞ,!6/&0J{;fKj޳3a:AXD,L(}N=`hw̺S yA Jil{EOW'XX[;|kC^X[ 0L=N#0L9"Z$(LWţep uJqOjy |ypػ|EIԄIɕ]p!: vT3m!.^i5paw@肮B8 Ey9lbÍgVTav5yAeR{^1rhXUnx\+68vu,LܾEPD fD;W8ruہxQSiXJccXԠ$ލ$ym9:h]:25ߘ} C@my__EQ2&}Ѱ1TA8=|%0xm< ]u1v뫚⠉ºZzV^hEֹvF_gsa2zRʡ6f<5!/&Zx;BeD ioOMY¾:HV@Yk˺;"}&äi 0z<bIބS㑑ko]׫4N :G1CJe¤F%AȾ: 7L,27,5p4U5"Wst'3t1kZGY8TW0Of]Z&-aCwa}MGT:Td–ao95g 'Y"&2=,̄ך9ǴpWs\bUaTؘ[/͝Tox9Dߧ.nNVv6KģTUʭ5vNfy ҽ"bfO^eKm "|㉈&/t ӝm[""b9wPR bQ8Lʣ hGbőOB7ͼK oʠ* ~^awJXi BQߩo&}~Zӑ8u,bF[溿EcO>DB7 u'[56KAWieNmW<26V3qH ,lj;̻ qia~c?A>`0nqopVu[npe{}?K1P2&1e?aN 0х\ut n+("V7荸i* R**! zߣi&L*T(<٠3DQSf,#B,Umpx;º؊BQ<=IE叱U?\lWD\FPzG>Lb 2e1j<^E0ݣj:-Ua1uoë~y5S*ԯg0bp9ͽ$T2v*,|N}TfCQn[0in:oLT#,ax'C^cYAW"\D%Gf3dU pX#h8 FJP!/B^z ZVV56L*_Fś3Llu-~Wˮl--VD䗩&ʋVa.:alT7R*PY~u3 ܴc@\xӫ[ lYqBB0@D/o+KٓJ Pwr_XZy0* U "Eg+aZ XM[M6L*5aWʚ0lxkaǛ0ix(Ku9W;oT*4۱v ,EOv`dDDa=n"@-&55*do;}@RIEtKR#/G ~SpM5Vfp#SwTb:ipq b.aawjP0#j ;#PXB3LprdžrT<݈.q2Ar 9MhDsD]}u=jca#'#;dD>1qEoH.*C{8{qVoloEa&GH+3t7ctz[.}̶zf OB%Ѐ<]CZ+5T"P2|O5\ZUp3h\_ ƥQȮ_+pN5boU'2˰t5T2jAHTTRiςpwsQn=4UQZvRuIsEU5lDg6dR E\$V|#+Q韝C0}Qc bSB1}"K+dWmB 1NRF#X+6-(&n"; 6 4Hs V6"C,z7K9ȊOA던Sk]0cä,X8.FY*`Xtup_Mt=`q9.ä*,߅#St`"aR,l[&T&[d .H?<٧0!@rqxhGI0i/6]RˇIqDt0#,mЈVl.\FB@H0x w+Kmچ>86a.•ѶU"yw?XN f~.U:DhF  ~,?qʦ q)~Y9b 9BCi8߳|qcl zR!bv XT;Ӌ~[$LB&U}gؘJY4EN?+BUVZ3c^mC0jf>S`W461-\_?d+uڡ;~tD09lUN0La~q s'1;hw۠-`[1Bo؟7aM]0kg>|k/"e^xmAܢ=W > &#=*M#~7B 7 U1U%V6}_D$;l}ֆI527!D-Gp-XtN߅ Fg5*AS`skK $0.@t2yk$˻r$r{mBLGx*TGo#L_Dݺ>œ-jUPh+3LV&K``C/*_I4-~l0)B0)),4rMJATp,Ow_(0äf¤e.~rF[mC063)GN< =eB^Ӯ%sBhNM%]5y AM *^ $UWG0=[q@=M5tٍaGeNbhH0hk`l\ĢHa-TMaWx&ίӞ03s7".9ɏ+aq0ve"C\1)!30&:2lltEzt S]%LjMIu (HJeȕ*6/w BD4ѷ*,[di%i;lCP\d`c;c|}&;ÑP >u# }7#%a88KS% B|j<_fpy01R]5\6<͵OԒJuW!JhX.m L>)*dzCsAL9>7~f?%/ÿ|XN4&5,O~W{ ]Ɨ@nCNf# *t*a.#`G`2jsIWscXsj.y% Saz^2T(z c]Ю3ށ-Eämm1LcvR?Us.)`{>"5*}R~jt1e0U<f*4/&-Zk+Gz](`a}E64GU V˹Mc^ Gh2Qz0N@ǏJq0m;Lځr\`Pq> SύJKa0S͕Q1Y~tR;̫ \OrDm9Y ^ >ۥ p~aa HD hy얢*3s??[y7$L~ƝW]K͢4X2[0`z1KDJ sm@Fo,oAÄ+ǩL{ SǩǩoTz$""bÄ&DD0!""bÄ&DD0!""bÄ0 I?=F^Ih>PDb#I2!+beBDL 2!V&Dʄ&L 2!"V&ʄX+"beB V&DʄX+beBDL 2! +"beBL 2y9Ri!$O6DL )(Ӑ 6DLsԮȑ#@!beBL_ff?Ƙ1BʄXX$%%7 &Od +beW _7331W/#0Ѭ{ll,~_aYHKKy%bؚ_= vo w4ѭˆP2!V&o^TWWCh!PR@ɏ9sf`cWxλ&={`7f 0LX+ 7B> \gu?6EY`= 5pb1b Dd/7AO S'̼48;JۃBIm2 f 301D 2psFo䂺}~t@8Y1<ןAL^2 {k&TxUW誯FLu-V|.L\=(LGuf}0j1'ycʕX5 ׸}{`EyT/`0L[ߟ2 '7w%s0Y9O yO i^tL,Ětv2!V&?i@qq1J%  s)r5%P3<{bMXrnυ'N'dj/5;=_kYMլ Om7TdHމ0|:RaXڅ JѤB&낷|dĆ8w+fvke}ol}0[;M050jH)aҞGoϤ؋H9K~IvVI;VO3nτXa"ɼM,JT'.\/~3ӿ#gv~B\a!zwj ¾F oy{Z&mʾ9[6Y=I7 La٥,LY!o5MM¤u=d]]r9͞2lx L&Bql磤Iuw OOW| -\N7޺(vy^in&E}scq4^sԟ4צꃣ2 lXj`IaeLBHeb o+be1oxNߑpac:;t ~*ʟw|y9BrUn 1-a Zyni2.gI0U󿁇~Z efڅIjoi\zSj0p8Vt}&/v;q`;~zH^yfdi8?R_LZ_$ȋ:Y}0<&Y+7Tmu={@Rau?lOO>Mnm&W{bY9wA7Q dHx<2y?K P(w1_ӂkHV:Le]5de0&At.*"V&opܺuKΝ;믿¢Eŋaii' "V&ʤ0~=z)9=>#m|QQR9z^6DL0߿Ξ=NT`ҥDShOod$beBLDko\~L pZ[888hMV^$ +be"r k*>ZǎiF_+Y7X?R*uT|< %XK0? ??\&7+beBl W0ϤXvH(ÏS `ݥ%Dvw~i'}׊0Gs)N &I-Ï&L abAI=.q rJOd[?})&͡d*T$+" r Ň1LX+z$G)mҝ/eC=? "%8#υ`90aeBLab&$)xAR|S)&BRϏe0aeBLOIq/N( |Iq3[7~2Uad "oImw F&T$~'ç#? e V&M}Hd7fw9L~2᧺>HZ$X?T+ ڎ">_qRzV&}[фWsc߫*HKJ+gR峱geBL&\%Jƞ g$ NfbtHxl:Ã,}xCۣ0K8_rRoat(p@I=7 vV`|q5)߇ӡ:doCwq8/&-.}0!V&W&Yצc'/\}Splsa|n聯Gѝ&#Bxt՞ef쾣2ͺ i%8 L`U_ 0ii 2w2]4D\ "'Lb93 ~x;7l|R} C}bh䅭Gg1O6Ɩo9Lk[ZfsBl~DL3ɋھqɈx99>b sO!.. GW`p'379!X Ŧ"0 q{_j;Xܥ#fƭxxKwG[NעS80s[1}KDְ0B?AϨ$oz/&ZlANʄX{B1iELDì0"m?ހG}~ "ܿL }&/)tqܿL }Mq2!V&ʤ{T KD _ʄX+be?("V&DL 2!"V&ʄX+V&DLX+beBDL 2! +"V&DL 2!"V&ʄXÄ +6ʄXU'$0!""bÄ&DD0!""bÄ&DD0!""j@[^IENDB`mapproxy-1.11.0/doc/imgs/nearest.png000066400000000000000000000362151320454472400173400ustar00rootroot00000000000000PNG  IHDR ߁PLTEھٵ֪ժӢћКΗΑˈʂÓ{srkֿվսռԻӹӸҸҷѶѵгвϱϱϰίήɾչͭƺ̫ö̩˩˨˧˨ʦɥɣϯȢǡǠƞŜŚĚؖ•e^\UYSOKPHֽk@@EDFسWAƴ̰xѲRĢw~o}}}~zzywvwtpie_]WVQQMNJLHKGKFusqppoopnmmlkkjMIhhgggfUUUDDD@Am9HIDATxu{PT׶/W^,n0RؐB%eVQpDnR-g-"ضD1i(x l2 ;XS\^A*kc $Rzzs<~c̹Zp[ag|ӫN1Z}gU)Y $ *e zcJQW|Wk@j|o>ȎG~!yxAnXa5'Vw'A3 7UN5]<g[AGrr%}p@;adpm;47ɓ'*/c{}ǠϘXqw׻q~#kr˄KPX㏪d=Z(:n,1wR du5A nO_?xx_v J8lC7'a˿s1 U3tkOwL1PQMߖl}f u:lӵv_Fߪ>}eQZ[brA%5i* _Z6 5]bUyw|7- GN oUe@Hk+hvGM_~he< t>m⭼}:]&zX^Ůe\^~n$3YV[yC a-AW`LJqqT.[AQDUj%Q!?Zm'5^Yĥ%5fTa|Utu\讪:>Z 603:OBi)m~d&kmy9זl̀p[|eI3 |Mmu:SVFHZE5W.rGDk>^=k~{%+Vi.AJX:Vu)AQrƲ&kN1Vh|\O*\+RЌB4n`I)lRһ@CyoO_iGł,,GPdJe0 [kf]g =/j;ftvSdz5gTݎVp8x$x{ɍ>Lu;u *JZ2TuZ5u Y{l3*cj˯gdOϬ<f ^ iihV4cn%`bS50= Fenߵ.f4?-zֹ҄= / /x?rAk!~CV  aֆ8*\b]oA;I`Dl(z,KF9 Ns74iW~ˏ64& v/ #O`}n!4N,\G~żna(ѣɄu0H7)# U5xvNCՐ]VYK!gu@7W>A![FCM樂%r'Je|Zyr#to.NkJK![h']6UC)56 A0j?sp_NBhY|wlzL`nTe8Iae[ih#E `^ ֵZO\IМaݶxG+<{ dRO|D}8:y'˄'K! / {cb/WPؙzѮ,+by g(.'T]_ߊe=넥$g К o\?9U"g1VBH/)zKۨ&҅ޚ({|=;yUX Čۢ]Q#f 'O,Y3+;\vtoB6݊7ax"Z|AeK_w`~:)AHSUJ"p" DN0B'prFUCa3ZkCB?JauT_#5LDQoW|nGf>?$ҁ6I-Uԉ^EG a6jII+-dJ^)!J;tpkV~.#hck`WEnYeVu*L!.oXaPkht\G*(3 Qni<?EQ0J)WqO^6& uxNEsMmBL=CH\徺 ['ikj[yu?..VAJ-]L.Xc0na' Ym5~>ä1TP0R kCͱ}Q_ hh} *9](O y4RDI|ӿ%@ŚvRRDZV5vԶaǚ]Mk4Kޮ CXyg3=Pil:4ώq 3bf>혓܌$/N2R^M_HJKE`~ i]DPI LDUƌAjb| kHq産efÇ tZbL<[xCV.5~{mADaݱ';0m:_zï<} ?ȻNtdy*|G&\6NǮq.ą.bo|Pȵei {/;ghTi퇿څE9%,*JپԿ}%k&/yBAyC pa(@WbUNm\x 0޾A1,\k5iũ\m_!Y4"X]>Vf[ܝZ]# 9Db_A1R-N? R i8dܢNo{Ϡ`!p>T3e0EεvfIf%-I ;õ^(* 1Fl!p:ō3VXנNRnX74ƓdjIAv>P!E8iڃ䅫WͬZd%R#3?Vvp+q$dJdG (APЁ1|y\s{v}ae(di>y1zY?:J\!$7T1(.*5BEĥhO79_̓~;И[%:Yez~uȉUp  vif%:<%hraUVTT(Trѐ9ۅ2zBzlYMIJ@ j!9q'[;;S sz8g+:[Tp@%DgKT*/ إ uW&'c33/|, HJ쯓[g(x[CC]jӌLgaCi^EFϪ/>gĿx* |}l}M'.:#GY>oV& J{v "q:0Maj!_M{?*S񡗒o}=`e'hU1KQvq.UF :5+xDc7wbޠ5B.#?ֲrYxB@(`lh9NhkQCZ~ӫcOq{28mKK/gQ,ljrE("؂[b#"#/8uaͽX%ioK%lAB lmƅc<X z,=Z!K_sy.kz\Ϲ6zsٮn\/Dږoܑ?\k^!1MIhY!0kاMsr)V0&BVvʼns{sqUpka)8w+q'6H0u.V> sMP!R<`V]tP(x8kW|nV]ʄ]E4+Jzd(5vNpX>~5 ֠L*V1 I ifE&'Eqq ۯڣ l{ʴG@p ff"aJeLe?̩U % %? Xs l?BQ..M[[vCn`Աl97](播 MZHDy/A<,%IҜ2ݬNKo'ƶI?эY|:B/FWGEZ8difliVtGW")#K+4NJe;D;Ut,E,^ ~# (?a9AUi0IJwY 1n qNy@V$BCP t YP-O,Y=&X29"./~{uW_c7^/'j1eg"/Q[w;pYY=~˪N\jO{*u:f59_S@u W(/\^9IO>Sg1D<c4gv!v` ݑcZYc=Dt5ҲdA9vIJ )?61k q$ wq/ c`oplV'*ɮd%8BEqv=rhU f}bZ%nmCis]Qt5*%$FHw`0-`8)iz1 0L3[` hxq9tt+nQ;Xh|$˅݁=%8}agS~0,MS*/x@ȍ>'TCDГߓ^B& ==sl;@ݪƽ Tʰ $ UarN$$>۱ s.;"ld)V{Fݱ2szJQeFAb1 `H=ٕO1J+ 8:DFOqprnd.y AgKsb[>ށ $Dy豉=zRd|J|v$߲#%#ivw~2[kЎ_EQP?ߎ*DvJ+{uEr!%0r*/M15M{dp !9e eT_NB|RXP L)=ѡO;X; HeL0SܕKFKCڙ"R?FlVGt<s:u 1@~S(W ([L_o_/דڼay{0_Fo)zGV7[09:/: HbIl> F_;$&b5\+3.-< *7eIykSרkM~R-.#t[ѝ5JGánax ]VTԝm 0D&+c.W*lqTG(A\sqZtmoּت&gAO1wN-%9 y3ȻAB8ꁅyr$ʮ 4J{•W@fRɁR 7zɓ) >[^+$ Y*^ޡԍ.__fpӮ#/jB*(_otpG1AIa |!{ڂG.0[ri| Tr:Hܳ1>y{[DI5 "Nz ʣ07pC! a֭JnyJ]*^Ԗ./_\:'ӗ}#""ET+GV4)""|""Ex{E:lU8"8"+b+B>7^ *+`W'-+?T\d>tӝ;C{|Hտnߟ: */VV3YȁJDg|諲&RY1VHpPtDOy=$7 wPIwxJ) WXܟ#C?Or}@1?># 3˼A+sɔ"NyH*!DP[Y[8 KA)odX"b^)쓜u ?M@@~P' 7#f[ TSŵ˓|Q{DuqQn2&ʰ e,oو H!*61 +Аssx<[[ZY]2"xV,#瓚.*A8nXLԁ6?SmsKH *.9+>Z.FiJw]$MP30@Z}u;&0Ăpk}7,>n~/V OC_t wdO`Gg.;o!x"qsMϪYs2& BA.73?7JhSRn)jR޻xIF(_l#G*7q.79kQpŅ ^*VW'81:`ٴ[9QHjc.̲spx׎@O6L:*T :OJ:g+xv; ;s|rl\61j=+\} j5T/2͓Jf.2w kI9psp!VD5ό ƥ p3PmD$o%6hXo_ ;diy{_|Dhl+DA uM{oj @^9Tik߀S1nIV1X&"Z%DtsMoS';DJ4Ja/+yp.oX7,nO'T%ẅ,`I~Khg#DJv3dԉ/Y~6%5_߭뗸?-طV'}/?˙~.Ef}zB 'QDSpá% gQК`:uXf 7 kb!dd7 ,n'N>#.z)sh!kKð%HVϢ/Բ9QKY7dXXA; ƨkoje,\2_z9 P+/>&1c_N)~ -BLer}Gsz*Vjc}]rf_#0 3B>g : V.@W2ZOF&d2ASӅm 6&v 2-%)ڸ-[[I.`=9]l{YA3SM`0^HHJ(O1KO$&{^-]|sM΍PDF_L`<ӐBYs9NJZOq -j_/t_uw>}d'>vOH"7_ЛCjV|%a45«@a(;Wxik`3`Z<̵I> FWCC'ZNd ܄]H/P˄ ۷ò:YiKBQzY4Au%HXpJ~ ã ˈ௔bxS03($^~fw>ZV5/EMsu qNY/1VkJK/VJ9۸pg`C`V<=,1wFϐa1y"q"MHh&nj3Ydp+^ꗮQQXPl3nᖂupOЯEl|A'=[yt у+Q*mϋԬ|Hk{@a+iR?EqB@LN2gƟlYs;\~h!Mvz|6xPMί8wޒ sr};p@AֱN&qnamfK]Iȯ{|8܊W.M\Dgt@NEz<'mjVP߸Z1ڛ V>c0Yt0pdÓ`oFO*% QF^_%͢U-yPi!1s3aZ"J2f_;+sSPz77OL^ވ~%&xd%U@*YG5d3.pzpQCUTQ@H{^-E’$n{4/q0mcd7Wҩhش {U{n\+DTς?ޔ(N}Lgv&-*SV+,uK:RWF (mrFg  g%QSb),8_xI}+y@T<}BBs_JE[D_wP mkc7On3?u/,T?@$pCӒǎeGMQ9U7Bj$30;.s; %~6tDtʿ{5#- hosQ^n͓ d[GXEoQC"u#2 ./Æ $\b_*K{G H {̌k@P8E\`ڑ}^Un08T;o%(O,!#ϷEe3¦uEWo=A[oqqU {l|zb#!4tjN@՛z3Y&ʊRiJMVtzz.k}f,=Y q, Y~&4 vT?v65IӰ_6qgyŗ于b6C i LM~j~@=O2YX9S P.17}8.D!Moa'/7 |p엨$fk+ A)!y^`RQB ߓ͂j$0k0^0nEKoU2ƧD7/M!x |&88t!Ο*yB  ^CZD} l 1k519ǿ_aC򏘰R6lP)@REų=`/7;j9O="1ApWʆ(fvԚ rKFjMmDLE U2_d胍M#& ̟n!(ꅽ+ңøG<H2 =h"DZ B{ffQj6o@RMMr'X6/d`Nܱ}g 8sĿ[vD¾ ~eT 2%$d X]'b9ܩ~Q9{>8i yeyEo>Ge?KgXe$4&YBDzKÂJY1rJNGklG-zxȊan((bx^־X-cΤ7NqZ~2f}g-ٳmZM臘v1) T<\ bC~Zcg%{ ǜ?H?tZC~Mgzӊ )1q Apϊ72@@ZSO N v*y>+Z٢{+ãӳ \)iIENDB`mapproxy-1.11.0/doc/index.rst000066400000000000000000000007431320454472400160700ustar00rootroot00000000000000MapProxy Documentation ====================== .. toctree:: :maxdepth: 2 install install_windows install_osgeo4w tutorial configuration services sources caches seed coverages mapproxy_util mapproxy_util_autoconfig deployment configuration_examples inspire labeling auth decorate_img development mapproxy_2 .. todolist:: Indices and tables ================== * :ref:`genindex` * :ref:`search` .. * :ref:`modindex` mapproxy-1.11.0/doc/inspire.rst000066400000000000000000000122211320454472400164240ustar00rootroot00000000000000.. _inpire: .. highlight:: yaml INSPIRE View Service ==================== MapProxy can act as an INSPIRE View Service. A View Service is a WMS 1.3.0 with an extended capabilities document. .. versionadded:: 1.8.1 INSPIRE Metadata ---------------- A View Service can either link to an existing metadata document or it can embed the service and layer metadata. These two options are described as Scenario 1 and 2 in the Technical Guidance document. Linked Metadata ^^^^^^^^^^^^^^^ Scenario 1 uses links to existing INSPIRE Discovery Services (CSW). You can link to metadata documents for the service and each layer. For services you need to use the ``inspire_md`` block inside ``services.wms`` with ``type: linked``. For example:: services: wms: md: title: Example INSPIRE View Service inspire_md: type: linked metadata_url: media_type: application/vnd.iso.19139+xml url: http://example.org/csw/doc languages: default: eng The View Services specification uses the WMS 1.3.0 extended capabilities for the layers metadata. Refer to the :ref:`layers metadata documentation`. For example:: layers: - name: example_layer title: Example Layer md: metadata: - url: http://example.org/csw/layerdoc type: ISO19115:2003 format: text/xml Embedded Metadata ^^^^^^^^^^^^^^^^^ Scenario 2 embeds the metadata directly into the capabilities document. Some metadata elements are mapped to an equivalent element in the WMS capabilities. The Resource Title is set with the normal `title` option for example. Other elements need to be configured inside the ``inspire_md`` block with ``type: embedded``. Here is a full example:: services: wms: md: title: Example INSPIRE View Service abstract: This is an example service with embedded INSPIRE metadata. online_resource: http://example.org/ contact: person: Your Name Here position: Technical Director organization: Acme Inc. address: Fakestreet 123 city: Somewhere postcode: 12345 country: Germany phone: +49(0)000-000000-0 fax: +49(0)000-000000-0 email: info@example.org access_constraints: constraints fees: 'None' keyword_list: - vocabulary: GEMET keywords: [Orthoimagery] inspire_md: type: embedded resource_locators: - url: http://example.org/metadata media_type: application/vnd.iso.19139+xml temporal_reference: date_of_creation: 2015-05-01 metadata_points_of_contact: - organisation_name: Acme Inc. email: acme@example.org conformities: - title: COMMISSION REGULATION (EU) No 1089/2010 of 23 November 2010 implementing Directive 2007/2/EC of the European Parliament and of the Council as regards interoperability of spatial data sets and services date_of_publication: 2010-12-08 uris: - OJ:L:2010:323:0011:0102:EN:PDF resource_locators: - url: http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:EN:PDF media_type: application/pdf degree: notEvaluated mandatory_keywords: - infoMapAccessService - humanGeographicViewer keywords: - title: GEMET - INSPIRE themes date_of_last_revision: 2008-06-01 keyword_value: Orthoimagery metadata_date: 2015-07-23 metadata_url: media_type: application/vnd.iso.19139+xml url: http://example.org/csw/doc You can express all dates as either ``date_of_creation``, ``date_of_publication`` or ``date_of_last_revision``. The View Services specification uses the WMS 1.3.0 extended capabilities for the layers metadata. Refer to the :ref:`layers metadata documentation` for all available options. For example:: layers: - name: example_layer title: Example Layer legendurl: http://example.org/example_legend.png md: abstract: Some abstract keyword_list: - vocabulary: GEMET keywords: [Orthoimagery] metadata: - url: http://example.org/csw/layerdoc type: ISO19115:2003 format: text/xml identifier: - url: http://www.example.org name: example.org value: "http://www.example.org#cf3c8572-601f-4f47-a922-6c67d388d220" Languages --------- A View Service always needs to indicate the language of the layer names, abstracts, map labels, etc.. You can only configure a single language as MapProxy does not support multi-lingual configurations. You need to set the default language as a `ISO 639-2/alpha-3 `_ code: :: inspire_md: languages: default: eng .... mapproxy-1.11.0/doc/install.rst000066400000000000000000000176161320454472400164360ustar00rootroot00000000000000Installation ============ This tutorial guides you to the MapProxy installation process on Unix systems. For Windows refer to :doc:`install_windows`. This tutorial was created and tested with Debian 5.0/6.0 and Ubuntu 10.04 LTS, if you're installing MapProxy on a different system you might need to change some package names. MapProxy is `registered at the Python Package Index `_ (PyPI). If you have installed Python setuptools (``python-setuptools`` on Debian) you can install MapProxy with ``sudo easy_install MapProxy``. This is really easy `but` we recommend to install MapProxy into a `virtual Python environment`_. A ``virtualenv`` is a self-contained Python installation where you can install arbitrary Python packages without affecting the system installation. You also don't need root permissions for the installation. `Read about virtualenv `_ if you want to know more about the benefits. .. _`virtual Python environment`: http://guide.python-distribute.org/virtualenv.html Create a new virtual environment -------------------------------- ``virtualenv`` is available as ``python-virtualenv`` on most Linux systems. You can also download a self-contained version:: wget https://github.com/pypa/virtualenv/raw/master/virtualenv.py To create a new environment with the name ``mapproxy`` call:: virtualenv --system-site-packages mapproxy # or python virtualenv.py --system-site-packages mapproxy You should now have a Python installation under ``mapproxy/bin/python``. .. note:: Newer versions of virtualenv will use your Python system packages (like ``python-imaging`` or ``python-yaml``) only when the virtualenv was created with the ``--system-site-packages`` option. If your (older) version of virtualenv does not have this option, then it will behave that way by default. You need to either prefix all commands with ``mapproxy/bin``, set your ``PATH`` variable to include the bin directory or `activate` the virtualenv with:: source mapproxy/bin/activate This will change the ``PATH`` for you and will last for that terminal session. .. _`distribute`: http://packages.python.org/distribute/ Install Dependencies -------------------- MapProxy is written in Python, thus you will need a working Python installation. MapProxy works with Python 2.7, 3.3 and 3.4 which should already be installed with most Linux distributions. Python 2.6 should still work, but it is no longer officially supported. MapProxy has some dependencies, other libraries that are required to run. There are different ways to install each dependency. Read :ref:`dependency_details` for a list of all required and optional dependencies. Installation ^^^^^^^^^^^^ On a Debian or Ubuntu system, you need to install the following packages:: sudo aptitude install python-imaging python-yaml libproj0 To get all optional packages:: sudo aptitude install libgeos-dev python-lxml libgdal-dev python-shapely .. note:: Check that the ``python-shapely`` package is ``>=1.2``, if it is not you need to install it with ``pip install Shapely``. .. _dependency_details: Dependency details ^^^^^^^^^^^^^^^^^^ libproj ~~~~~~~ MapProxy uses the Proj4 C Library for all coordinate transformation tasks. It is included in most distributions as ``libproj0``. .. _dependencies_pil: Pillow ~~~~~~ Pillow, the successor of the Python Image Library (PIL), is used for the image processing and it is included in most distributions as ``python-imaging``. Please make sure that you have Pillow installed as MapProxy is no longer compatible with the original PIL. The version of ``python-imaging`` should be >=2. You can install a new version of Pillow from source with:: sudo aptitude install build-essential python-dev libjpeg-dev \ zlib1g-dev libfreetype6-dev pip install Pillow YAML ~~~~ MapProxy uses YAML for the configuration parsing. It is available as ``python-yaml``, but you can also install it as a Python package with ``pip install PyYAML``. Shapely and GEOS *(optional)* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You will need Shapely to use the :doc:`coverage feature ` of MapProxy. Shapely offers Python bindings for the GEOS library. You need Shapely (``python-shapely``) and GEOS (``libgeos-dev``). You can install Shapely as a Python package with ``pip install Shapely`` if you system does not provide a recent (>= 1.2.0) version of Shapely. GDAL *(optional)* ~~~~~~~~~~~~~~~~~ The :doc:`coverage feature ` allows you to read geometries from OGR datasources (Shapefiles, PostGIS, etc.). This package is optional and only required for OGR datasource support (BBOX, WKT and GeoJSON coverages are supported natively). OGR is part of GDAL (``libgdal-dev``). .. _lxml_install: lxml *(optional)* ~~~~~~~~~~~~~~~~~ `lxml`_ is used for more advanced WMS FeatureInformation operations like XSL transformation or the concatenation of multiple XML/HTML documents. It is available as ``python-lxml``. .. _`lxml`: http://lxml.de Install MapProxy ---------------- Your virtual environment should already contain `pip`_, a tool to install Python packages. If not, ``easy_install pip`` is enough to get it. To install you need to call:: pip install MapProxy You specify the release version of MapProxy. E.g.:: pip install MapProxy==1.8.0 or to get the latest 1.8.0 version:: pip install "MapProxy>=1.8.0,<=1.8.99" To check if the MapProxy was successfully installed, you can call the `mapproxy-util` command. :: mapproxy-util --version .. _`pip`: http://pip.openplans.org/ .. note:: ``pip`` and ``easy_install`` will download packages from the `Python Package Index `_ and therefore they require full internet access. You need to set the ``http_proxy`` environment variable if you only have access to the internet via an HTTP proxy. See :ref:`http_proxy` for more information. .. _create_configuration: Create a configuration ---------------------- To create a new set of configuration files for MapProxy call:: mapproxy-util create -t base-config mymapproxy This will create a ``mymapproxy`` directory with a minimal example configuration (``mapproxy.yaml`` and ``seed.yaml``) and two full example configuration files (``full_example.yaml`` and ``full_seed_example.yaml``). Refer to the :doc:`configuration documentation` for more information. With the default configuration the cached data will be placed in the ``cache_data`` subdirectory. Start the test server --------------------- To start a test server:: cd mymapproxy mapproxy-util serve-develop mapproxy.yaml There is already a test layer configured that obtains data from the `Omniscale OpenStreetMap WMS`_. Feel free to use this service for testing. MapProxy comes with a demo service that lists all configured WMS and TMS layers. You can access that service at http://localhost:8080/demo/ .. _`Omniscale OpenStreetMap WMS`: http://osm.omniscale.de/ Upgrade ------- You can upgrade MapProxy with pip in combination with a version number or with the ``--upgrade`` option. Use the ``--no-deps`` option to avoid upgrading the dependencies. To upgrade to version 1.x.y:: pip install 'MapProxy==1.x.y' To upgrade to the latest release:: pip install --upgrade --no-deps MapProxy To upgrade to the current development version:: pip install --upgrade --no-deps https://github.com/mapproxy/mapproxy/tarball/master Changes ^^^^^^^ New releases of MapProxy are backwards compatible with older configuration files. MapProxy will issue warnings on startup if a behavior will change in the next releases. You are advised to upgrade in single release steps (e.g. 1.2.0 to 1.3.0 to 1.4.0) and to check the output of ``mapproxy-util serve-develop`` for any warnings. You should also refer to the Changes Log of each release to see if there is anything to pay attention for. If you upgrade from 0.8, please read the old mirgation documentation `_. mapproxy-1.11.0/doc/install_osgeo4w.rst000066400000000000000000000045731320454472400201030ustar00rootroot00000000000000Installation on OSGeo4W ======================= `OSGeo4W`_ is a popular package of open-source geospatial tools for Windows systems. Besides packing a lot of GIS tools and a nice installer, it also features a full Python installation, along with some of the packages that MapProxy needs to run. .. _`OSGeo4W`: http://trac.osgeo.org/osgeo4w/ In order to install MapProxy within an OSGeo4W environment, the first step is to ensure that the needed Python packages are installed. In order to do so: * Download and run the `OSGeo4W installer` * Select advanced installation * When shown a list of available packages, check (at least) ``python`` and ``python-pil`` for installation. .. _`OSGeo4W installer`: http://download.osgeo.org/osgeo4w/osgeo4w-setup.exe Please refer to the `OSGeo4W installer FAQ `_ if you've got trouble running it. At this point, you should see an OSGeo4W shell icon on your desktop and/or start menu. Right-click that, and *run as administrator*. In the OSGeo4W window, run:: C:\OSGeo4W> pip install mapproxy and :: C:\OSGeo4W> pip install pyproj If these last two commands didn't print out any errors, your installation of MapProxy is successful. You can now close the OSGeo4W shell with administrator privileges, as it is no longer needed. In older versions of OSGeo4W ``pip`` may not recognized. In such a case, please follow the instructions for `installing pip with get-pip.py `_ and rerty the above ``pip install`` commands. Check installation ------------------ To check if the MapProxy was successfully installed, you can launch a regular OSGeo4W shell, and call ``mapproxy-util``. You should see the installed version number:: C:\OSGeo4W> mapproxy-util --version .. note:: You need to run *all* MapProxy-related commands from an OSGeo4W shell, and not from a standard command shell. Now continue with :ref:`Create a configuration ` from the installation documentation. Unattended OSGeo4W environment ------------------------------- If you need to run unattended commands (like scheduled runs of *mapproxy-seed*), make a copy of ``C:\OSGeo4W\OSGeo4W.bat`` and modify the last line, to call ``cmd`` so it runs the MapProxy script you need, e.g.:: cmd /c mapproxy-seed -s C:\path\to\seed.yaml -f C:\path\to\mapproxy.yaml mapproxy-1.11.0/doc/install_windows.rst000066400000000000000000000113371320454472400202020ustar00rootroot00000000000000Installation on Windows ======================= At frist you need a working Python installation. You can download Python from: https://www.python.org/download/. MapProxy requires Python 2.7, 3.3, 3.4, 3.5 or 3.6. Python 2.6 should still work, but it is no longer officially supported. We would recommend the latest 2.7 version available. Virtualenv ---------- *If* you are using your Python installation for other applications as well, then we advise you to install MapProxy into a `virtual Python environment`_ to avoid any conflicts with different dependencies. *You can skip this if you only use the Python installation for MapProxy.* `Read about virtualenv `_ if you want to now more about the benefits. .. _`virtual Python environment`: http://guide.python-distribute.org/virtualenv.html To create a new virtual environment for your MapProxy installation and to activate it go to the command line and call:: C:\Python27\python path\to\virtualenv.py c:\mapproxy_venv C:\mapproxy_venv\Scripts\activate.bat .. note:: The last step is required every time you start working with your MapProxy installation. Alternatively you can always explicitly call ``\mapproxy_venv\Scripts\``. .. note:: Apache mod_wsgi does not work well with virtualenv on Windows. If you want to use mod_wsgi for deployment, then you should skip the creation the virtualenv. After you activated the new environment, you have access to ``python`` and ``pip``. To install MapProxy with most dependencies call:: pip install MapProxy This might take a minute. You can skip the next step. PIP --- MapProxy and most dependencies can be installed with the ``pip`` command. ``pip`` is already installed if you are using Python >=2.7.9, or Python >=3.4. `Read the pip documentation for more information `_. After that you can install MapProxy with:: c:\Python27\Scripts\pip install MapProxy This might take a minute. Dependencies ------------ Read :ref:`dependency_details` for more information about all dependencies. Pillow and YAML ~~~~~~~~~~~~~~~ Pillow and PyYAML are installed automatically by ``pip``. PyProj ~~~~~~ Since libproj4 is generally not available on a Windows system, you will also need to install the Python package ``pyproj``. You need to manually download the ``pyproj`` package for your system. See below for *Platform dependent packages*. :: pip install path\to\pyproj-xxx.whl Shapely and GEOS *(optional)* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Shapely can be installed with ``pip install Shapely``. This will already include the required ``geos.dll``. GDAL *(optional)* ~~~~~~~~~~~~~~~~~ MapProxy requires GDAL/OGR for coverage support. MapProxy can either load the ``gdal.dll`` directly or use the ``osgeo.ogr`` Python package. You can `download and install inofficial Windows binaries of GDAL and the Python package `_ (e.g. `gdal-19-xxxx-code.msi`). You need to add the installation path to the Windows ``PATH`` environment variable in both cases. You can set the variable temporary on the command line (spaces in the filename need no quotes or escaping):: set PATH=%PATH%;C:\Program Files (x86)\GDAL Or you can add it to your `systems environment variables `_. You also need to set ``GDAL_DRIVER_PATH`` or ``OGR_DRIVER_PATH`` to the ``gdalplugins`` directory when you want to use the Oracle plugin (extra download from URL above):: set GDAL_DRIVER_PATH=C:\Program Files (x86)\GDAL\gdalplugins .. _win_platform_packages: Platform dependent packages --------------------------- ``pip`` downloads all packages from https://pypi.python.org/, but not all platform combinations might be available as a binary package, especially if you run a 64bit version of Python. If you run into trouble during installation, because it is trying to compile something (e.g. complaining about ``vcvarsall.bat``), you should look at Christoph Gohlke's `Unofficial Windows Binaries for Python Extension Packages `_. This is a reliable site for binary packages for Python. You need to download the right package: The ``cpxx`` code refers to the Python version (e.g. ``cp27`` for Python 2.7); ``win32`` for 32bit Python installations and ``amd64`` for 64bit. You can install the ``.whl``, ``.zip`` or ``.exe`` packages with ``pip``:: pip install path\to\package-xxx.whl Check installation ------------------ To check if the MapProxy was successfully installed you can call ``mapproxy-util``. You should see the installed version number. :: mapproxy-util --version Now continue with :ref:`Create a configuration ` from the installation documentation. mapproxy-1.11.0/doc/labeling.rst000066400000000000000000000247131320454472400165410ustar00rootroot00000000000000WMS Labeling ============ The tiling of rendered vector maps often results in issues with truncated or repeated labels. Some of these issues can be reduced with a proper configuration of MapProxy, but some require changes to the configuration of the source WMS server. This document describes settings for MapProxy and MapServer, but the problems and solutions are also valid for other WMS servers. Refer to their documentations on how to configure these settings. The Problem ----------- MapProxy always uses small tiles for caching. MapProxy does not pass through incoming requests to the source WMS [#]_, but it always requests images/tiles that are aligned to the internal grid. MapProxy combines, scales and reprojects these tiles for WMS requests and for tiled requests (TMS/KML) the tiles are combined by the client (OpenLayers, etc). .. [#] Except for uncached, cascaded WMS requests. When tiles are combined, the text labels at the boundaries need to be present at both tiles and need to be placed at the exact same (geographic) location. There are three common problems here. No placement outside the BBOX ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ WMS servers do not draw features that are outside of the map bounds. For example, a city label that extends into the neighboring map tile will not be drawn in that other tile, because the geographic feature of the city (a single point) is only present in one tile. .. image:: imgs/labeling-no-placement.png Dynamic label placement ~~~~~~~~~~~~~~~~~~~~~~~ WMS servers can adjust the position of labels so that more labels can fit on a map. For example, a city label is not always displayed at the same geographic location, but moved around to fit in the requested map or to make space for other labels. .. image:: imgs/labeling-dynamic.png Repeated labels ~~~~~~~~~~~~~~~ WMS servers render labels for polygon areas in each request. Labels for large areas will apear multiple times, once in each tile. .. image:: imgs/labeling-repeated.png MapProxy Options ---------------- There are two options that help with these issues. .. _meta_tiles: Meta Tiles ~~~~~~~~~~ You can use meta tiles to reduce the labeling issues. A meta tile is a collection of multiple tiles. Instead of requesting each tile with a single request, MapProxy requests a single image that covers the area of multiple tiles and then splits that response into the actual tiles. The following image demonstrates that: .. image:: imgs/labeling-metatiling.png The thin lines represent the tiles. The WMS request (inner box) consists of 20 tiles and without metatiling each tile results in a request to the WMS source. With a meta tile size of 4x4, only two larger requests to the source WMS are required (thick black box). Because you are requesting less images, you have less boundaries where labeling issues can appear. In this case it reduces the number of tile/image boundaries from 31 to only one. But, it only reduces the problem and does not solve it. Nonetheless, it should be used because it also reduces the load on the source WMS server. You can configure the meta tile size in the ``globals.cache`` section and for each ``cache``. It defaults to ``[4, 4]``. :: globals: cache: meta_size: [6, 6] caches: mycache: sources: [...] grids: [...] meta_size: [8, 8] This does also work for tiles services. When a client like OpenLayers requests the 20 tiles from the example above in parallel, MapProxy will still requests the two meta tiles. Locking ensures that each meta tile will be requested only once. .. _meta_buffer: Meta Buffer ~~~~~~~~~~~ In addition to meta tiles, MapProxy implements a meta buffer. The meta buffer adds extra space at the edges of the requested area. With this buffer, you can solve the first issue: no placement outside the BBOX. .. image:: imgs/labeling-meta-buffer.png You can combine meta tiling and meta buffer. MapProxy then extends the whole meta tile with the configured buffer. A meta buffer of 100 will add 100 pixels at each edge of the request. With a meta size of 4x4 and a tile size of 256x256, the requested image is extended from 1024x1024 to 1224x1224. The BBOX is also extended to match the new geographical extent. .. image:: imgs/labeling-metatiling-buffer.png To solve the first issue, the value should be at least half of your longest labels: If you have text labels that are up to 200 pixels wide, than you should use a meta buffer of around 120 pixels. You can configure the size of the meta buffer in the ``globals.cache`` section and for each ``cache``. It defaults to ``80``. :: globals: cache: meta_buffer: 100 caches: mycache: sources: [...] grids: [...] meta_buffer: 150 WMS Server Options ------------------ You can reduce some of the labeling issues with meta tiling, and solve the first issue with the meta buffer. The issues with dynamic and repeated labeling requires some changes to your WMS server. In general, you need to disable the dynamic position of labels and you need to allow the rendering of partial labels. MapServer Options ----------------- MapServer has lots of settings that affect the rendering. The two most important settings are ``PROCESSING "LABEL_NO_CLIP=ON"`` from the ``LAYER`` configuration. With this option the labels are fixed to the whole feature and not only the part of the feature that is visible in the current map request. Default is off. and ``PARTIALS`` from the ``LABEL`` configuration. If this option is true, then labels are rendered beyond the boundaries of the map request. Default is true. ``PARTIAL FALSE`` ~~~~~~~~~~~~~~~~~ The easiest option to solve all issues is ``PARTIAL FALSE`` with a meta buffer of 0. This prevents any label from truncation, but it comes with a large downside: Since no labels are rendered at the boundaries of the meta tiles, you will have areas with no labels at all. These areas form a noticeable grid pattern on your maps. The following images demonstrates a WMS request with a meta tile boundary in the center. .. image:: imgs/labeling-partial-false.png You can improve that with the right set of configuration options for each type of geometry. Points ~~~~~~ As described above, you can use a meta buffer to prevent missing labels. You need to set ``PARTIALS TRUE`` (which is the default), and configure a large enough meta buffer. The labels need to be placed at the same position with each request. You can configure that with the ``POSITION`` options. The default is ``auto`` and you should set this to an explicit value, ``cc`` or ``uc`` for example. ``example.map``:: LABEL [...] POSITION cc PARTIALS TRUE END ``mapproxy.yaml``:: caches: mycache: meta_buffer: 150 [...] .. .. ``PARTIALS TRUE``: .. .. image:: imgs/mapserver_points_partials_true.png .. .. ``PARTIALS FALSE``: .. .. image:: imgs/mapserver_points_partials_false.png Polygons ~~~~~~~~ Meta tiling reduces the number of repeated labels, but they can still apear at the border of meta tiles. You can use the ``PROCESSING "LABEL_NO_CLIP=ON"`` option to fix this problem. With this option, MapServer places the label always at a fixed position, even if that position is outside the current map request. .. image:: imgs/labeling-no-clip.png If the ``LABEL_NO_CLIP`` option is used, ``PARTIALS`` should be ``TRUE``. Otherwise label would not be rendered if they overlap the map boundary. This options also requires a meta buffer. ``example.map``:: LAYER TYPE POLYGON PROCESSING "LABEL_NO_CLIP=ON" [...] LABEL [...] POSITION cc PARTIALS TRUE END END ``mapproxy.yaml``:: caches: mycache: meta_buffer: 150 [...] .. ``PROCESSING "LABEL_NO_CLIP=ON"`` and ``PARTIALS TRUE``: .. .. image:: imgs/mapserver_area_with_labelclipping.png .. .. ``PARTIALS FALSE``: .. .. image:: imgs/mapserver_area_without_labelclipping.png Lines ~~~~~ By default, labels are repeated on longer line strings. Where these labels are repeated depends on the current view of that line. That placement might differ in two neighboring image requests for long lines. Most of the time, the labels will match at the boundaries of the meta tiles, when you use ``PARTIALS TRUE`` and a meta buffer. But, you might notice truncated labels on long line strings. In practice these issues are rare, though. ``example.map``:: LAYER TYPE LINE [...] LABEL [...] PARTIALS TRUE END END ``mapproxy.yaml``:: caches: mycache: meta_buffer: 150 [...] You can disable repeated labels with ``PROCESSING LABEL_NO_CLIP="ON"``, if don't want to have any truncated labels. Like with polygons, you need set ``PARTIALS TRUE`` and use a meta buffer. The downside of this is that each lines will only have one label in the center of that line. ``example.map``:: LAYER TYPE LINE PROCESSING "LABEL_NO_CLIP=ON" [...] LABEL [...] PARTIALS TRUE END END ``mapproxy.yaml``:: caches: mycache: meta_buffer: 150 [...] There is a third option. If you want repeated labels but don't want any truncated labels, you can set ``PARTIALS FALSE``. Remember that you will get the same grid pattern as mentioned above, but it might not be noted if you mix this layer with other point and polygon layers where ``PARTIALS`` is enabled. You need to compensate the meta buffer when you use ``PARTIALS FALSE`` in combination with other layers that require a meta buffer. You need to set the option ``LABELCACHE_MAP_EDGE_BUFFER`` to the negative value of your meta buffer. :: WEB [...] METADATA LABELCACHE_MAP_EDGE_BUFFER "-100" END END LAYER TYPE LINE [...] LABEL [...] PARTIALS FALSE END END ``mapproxy.yaml``:: caches: mycache: meta_buffer: 100 [...] .. It has to be evaluated which solution is the best for each application: some cropped or missing labels. .. .. ``PROCESSING "LABEL_NO_CLIP=ON"`` and ``PARTIALS TRUE``: .. .. image:: imgs/mapserver_road_with_labelclipping.png .. .. ``PROCESSING "LABEL_NO_CLIP=OFF"`` and ``PARTIALS FALSE``: .. .. image:: imgs/mapserver_road_without_labelclipping.png Other WMS Servers ----------------- The most important step for all WMS servers is to disable to dynamic placement of labels. Look into the documentation how to do this for you WMS server. If you want to contribute to this document then join our `mailing list `_ or use our `issue tracker `_. mapproxy-1.11.0/doc/make.bat000066400000000000000000000060171320454472400156340ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation set SPHINXBUILD=sphinx-build set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\OmniscaleProxy.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\OmniscaleProxy.ghc goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end mapproxy-1.11.0/doc/mapproxy_2.rst000066400000000000000000000037721320454472400170660ustar00rootroot00000000000000MapProxy 2.0 ############ MapProxy will change a few defaults in the configuration between 1.8 and 2.0. You might need to adapt your configuration to have MapProxy 2.0 work the same as MapProxy 1.8 or 1.7. Most changes are made to make things more consistent, to make it easier for new users and to discourage a few deprecated things. .. warning:: Please read this document carefully. Also check all warnings that the latest 1.8 version of `mapproxy-util serve-develop` will generate with your configuration before upgrading to 2.0. Grids ===== New default tile grid --------------------- MapProxy now uses GLOBAL_WEBMERCATOR as the default grid, when no grids are configured for a cache or a tile source. This grid is compatible with Google Maps and OpenStreetMap, and uses the same tile origin as the WMTS standard. The old default GLOBAL_MERCATOR uses a different tile origin (lower-left instead of upper-left) and you need to set this grid if you upgrade from MapProxy 1 and have caches or tile sources without an explicit grid configured. MapProxy used the lower-left tile in a tile grid as the origin. This is the same origin as the TMS standard uses. Google Maps, OpenStreetMap and now also WMTS are counting tiles from the upper-left tile. MapProxy changes Default origin -------------- The default origin changes from 'll' (lower-left) to 'ul' (upper-left). You need to set the origin explicitly if you use custom grids. The origin will stay the same if your custom grid is `base`d on the `GLOBAL_*` grids. WMS === SRS --- The WMS does not support EPSG:900913 by default anymore to discourage the use of this deprecated EPSG code. Please use EPSG:3857 instead or add it back to the WMS configuration (see :ref:`wms_srs`). Image formats ------------- PNG and JPEG are the right image formats for almost all use cases. GIF and TIFF are therefore no longer enabled by default. You can enable them back in the WMS configuration if you need them (:ref:`wms_image_formats`) Other ===== This document will be extended. mapproxy-1.11.0/doc/mapproxy_util.rst000066400000000000000000000437621320454472400177050ustar00rootroot00000000000000.. _mapproxy-util: ############# mapproxy-util ############# The commandline tool ``mapproxy-util`` provides sub-commands that are helpful when working with MapProxy. To get a list of all sub-commands call:: mapproxy-util To call a sub-command:: mapproxy-util subcommand Each sub-command provides additional information:: mapproxy-util subcommand --help The current sub-commands are: - :ref:`mapproxy_util_create` - :ref:`mapproxy_util_serve_develop` - :ref:`mapproxy_util_serve_multiapp_develop` - :ref:`mapproxy_util_scales` - :ref:`mapproxy_util_wms_capabilities` - :ref:`mapproxy_util_grids` - :ref:`mapproxy_util_export` - :ref:`mapproxy_defrag_compact_cache` - ``autoconfig`` (see :ref:`mapproxy_util_autoconfig`) .. _mapproxy_util_create: ``create`` ========== This sub-command creates example configurations for you. There are templates for each configuration file. .. program:: mapproxy-util create .. cmdoption:: -l, --list-templates List names of all available configuration templates. .. cmdoption:: -t , --template Create a configuration with the named template. .. cmdoption:: -f , --mapproxy-conf The path of the MapProxy configuration. Required for some templates. .. cmdoption:: --force Overwrite any existing configuration with the same output filename. Configuration templates ----------------------- Available templates are: base-config: Creates an example ``mapproxy.yaml`` and ``seed.yaml`` file. You need to pass the destination directory to the command. log-ini: Creates an example logging configuration. You need to pass the target filename to the command. wsgi-app: Creates an example server script for the given MapProxy configuration (:option:`--f/--mapproxy-conf`) . You need to pass the target filename to the command. Example ------- :: mapproxy-util create -t base-config ./ .. index:: testing, development, server .. _mapproxy_util_serve_develop: ``serve-develop`` ================= This sub-command starts a MapProxy instance of your configuration as a stand-alone server. You need to pass the MapProxy configuration as an argument. The server will automatically reload if you change the configuration or any of the MapProxy source code. .. program:: mapproxy-util serve-develop .. cmdoption:: -b

, --bind
The server address where the HTTP server should listen for incomming connections. Can be a port (``:8080``), a host (``localhost``) or both (``localhost:8081``). The default is ``localhost:8080``. You need to use ``0.0.0.0`` to be able to connect to the server from external clients. Example ------- :: mapproxy-util serve-develop ./mapproxy.yaml .. index:: testing, development, server, multiapp .. _mapproxy_util_serve_multiapp_develop: ``serve-multiapp-develop`` ========================== .. versionadded:: 1.3.0 This sub-command is similar to ``serve-develop`` but it starts a :ref:`MultiMapProxy ` instance. You need to pass a directory of your MapProxy configurations as an argument. The server will automatically reload if you change any configuration or any of the MapProxy source code. .. program:: mapproxy-util serve-multiapp-develop .. cmdoption:: -b
, --bind
The server address where the HTTP server should listen for incomming connections. Can be a port (``:8080``), a host (``localhost``) or both (``localhost:8081``). The default is ``localhost:8080``. You need to use ``0.0.0.0`` to be able to connect to the server from external clients. Example ------- :: mapproxy-util serve-multiapp-develop my_projects/ .. index:: scales, resolutions .. _mapproxy_util_scales: ``scales`` ========== .. versionadded:: 1.2.0 This sub-command helps to convert between scales and resolutions. Scales are ambiguous when the resolution of the output device (LCD, printer, mobile, etc) is unknown and therefore MapProxy only uses resolutions for configuration (see :ref:`scale_resolution`). You can use the ``scales`` sub-command to calculate between known scale values and resolutions. The command takes a list with one or more scale values and returns the corresponding resolution value. .. program:: mapproxy-util scales .. cmdoption:: --unit Return resolutions in this unit per pixel (default meter per pixel). .. cmdoption:: -l , --levels Calculate resolutions for ``n`` levels. This will double the resolution of the last scale value if ``n`` is larger than the number of the provided scales. .. cmdoption:: -d , --dpi The resolution of the output display to use for the calculation. You need to set this to the same value of the client/server software you are using. Common values are 72 and 96. The default value is the equivalent of a pixel size of .28mm, which is around 91 DPI. This is the value the OGC uses since the WMS 1.3.0 specification. .. cmdoption:: --as-res-config Format the output so that it can be pasted into a MapProxy grid configuration. .. cmdoption:: --res-to-scale Calculate from resolutions to scale. Example ------- For multiple levels as MapProxy configuration snippet: :: mapproxy-util scales -l 4 --as-res-config 100000 :: res: [ # res level scale 28.0000000000, # 0 100000.00000000 14.0000000000, # 1 50000.00000000 7.0000000000, # 2 25000.00000000 3.5000000000, # 3 12500.00000000 ] With multiple scale values and custom DPI: :: mapproxy-util scales --dpi 96 --as-res-config \ 100000 50000 25000 10000 :: res: [ # res level scale 26.4583333333, # 0 100000.00000000 13.2291666667, # 1 50000.00000000 6.6145833333, # 2 25000.00000000 2.6458333333, # 3 10000.00000000 ] .. _mapproxy_util_wms_capabilities: ``wms-capabilities`` ==================== .. versionadded:: 1.5.0 This sub-command parses a valid capabilites document from a URL and displays all available layers. This tool does not create a MapProxy configuration, but the output should help you to set up or modify your MapProxy configuration. The command takes a valid URL GetCapabilities URL. .. program:: mapproxy-util wms_capabilities .. cmdoption:: --host Display all available Layers for this service. Each new layer will be marked with a hyphen and all sublayers are indented. .. cmdoption:: --version Parse the Capabilities-document for the given version. Only version 1.1.1 and 1.3.0 are supported. The default value is 1.1.1 Example ------- With the following MapProxy layer configuration: :: layers: - name: osm title: Omniscale OSM WMS - osm.omniscale.net sources: [osm_cache] - name: foo title: Group Layer layers: - name: layer1a title: Title of Layer 1a sources: [osm_cache] - name: layer1b title: Title of Layer 1b sources: [osm_cache] Parsed capabilities document: :: mapproxy-util wms-capabilities http://127.0.0.1:8080/service?REQUEST=GetCapabilities :: Capabilities Document Version 1.1.1 Root-Layer: - title: MapProxy WMS Proxy url: http://127.0.0.1:8080/service? opaque: False srs: ['EPSG:31467', 'EPSG:31466', 'EPSG:4326', 'EPSG:25831', 'EPSG:25833', 'EPSG:25832', 'EPSG:31468', 'EPSG:900913', 'CRS:84', 'EPSG:4258'] bbox: EPSG:900913: [-20037508.3428, -20037508.3428, 20037508.3428, 20037508.3428] EPSG:4326: [-180.0, -85.0511287798, 180.0, 85.0511287798] queryable: False llbbox: [-180.0, -85.0511287798, 180.0, 85.0511287798] layers: - name: osm title: Omniscale OSM WMS - osm.omniscale.net url: http://127.0.0.1:8080/service? opaque: False srs: ['EPSG:31467', 'EPSG:31466', 'EPSG:25832', 'EPSG:25831', 'EPSG:25833', 'EPSG:4326', 'EPSG:31468', 'EPSG:900913', 'CRS:84', 'EPSG:4258'] bbox: EPSG:900913: [-20037508.3428, -20037508.3428, 20037508.3428, 20037508.3428] EPSG:4326: [-180.0, -85.0511287798, 180.0, 85.0511287798] queryable: False llbbox: [-180.0, -85.0511287798, 180.0, 85.0511287798] - name: foobar title: Group Layer url: http://127.0.0.1:8080/service? opaque: False srs: ['EPSG:31467', 'EPSG:31466', 'EPSG:25832', 'EPSG:25831', 'EPSG:25833', 'EPSG:4326', 'EPSG:31468', 'EPSG:900913', 'CRS:84', 'EPSG:4258'] bbox: EPSG:900913: [-20037508.3428, -20037508.3428, 20037508.3428, 20037508.3428] EPSG:4326: [-180.0, -85.0511287798, 180.0, 85.0511287798] queryable: False llbbox: [-180.0, -85.0511287798, 180.0, 85.0511287798] layers: - name: layer1a title: Title of Layer 1a url: http://127.0.0.1:8080/service? opaque: False srs: ['EPSG:31467', 'EPSG:31466', 'EPSG:25832', 'EPSG:25831', 'EPSG:25833', 'EPSG:4326', 'EPSG:31468', 'EPSG:900913', 'CRS:84', 'EPSG:4258'] bbox: EPSG:900913: [-20037508.3428, -20037508.3428, 20037508.3428, 20037508.3428] EPSG:4326: [-180.0, -85.0511287798, 180.0, 85.0511287798] queryable: False llbbox: [-180.0, -85.0511287798, 180.0, 85.0511287798] - name: layer1b title: Title of Layer 1b url: http://127.0.0.1:8080/service? opaque: False srs: ['EPSG:31467', 'EPSG:31466', 'EPSG:25832', 'EPSG:25831', 'EPSG:25833', 'EPSG:4326', 'EPSG:31468', 'EPSG:900913', 'CRS:84', 'EPSG:4258'] bbox: EPSG:900913: [-20037508.3428, -20037508.3428, 20037508.3428, 20037508.3428] EPSG:4326: [-180.0, -85.0511287798, 180.0, 85.0511287798] queryable: False llbbox: [-180.0, -85.0511287798, 180.0, 85.0511287798] .. _mapproxy_util_grids: ``grids`` ========= .. versionadded:: 1.5.0 This sub-command displays information about configured grids. The command takes a MapProxy configuration file and returns all configured grids. Furthermore, default values for each grid will be displayed if they are not defined explicitly. All default values are marked with an asterisk in the output. .. program:: mapproxy-util grids .. cmdoption:: -f , --mapproxy-config Display all configured grids for this MapProxy configuration with detailed information. If this option is not set, the sub-command will try to use the last argument as the mapproxy config. .. cmdoption:: -l, --list Display only the names of the grids for the given configuration, which are used by any grid. .. cmdoption:: --all Show also grids that are not referenced by any cache. .. cmdoption:: -g , --grid Display information only for a single grid. The tool will exit, if the grid name is not found. .. cmdoption:: -c , --coverage Display an approximation of the number of tiles for each level that which are within this coverage. The coverage must be defined in Seed configuration. .. cmdoption:: -s , --seed-conf This option loads the seed configuration and is needed if you use the ``--coverage`` option. Example ------- With the following MapProxy grid configuration: :: grids: localgrid: srs: EPSG:31467 bbox: [5,50,10,55] bbox_srs: EPSG:4326 min_res: 10000 localgrid2: base: localgrid srs: EPSG:25832 res_factor: sqrt2 tile_size: [512, 512] List all configured grids: :: mapproxy-util grids --list --mapproxy-config /path/to/mapproxy.yaml :: GLOBAL_GEODETIC GLOBAL_MERCATOR localgrid localgrid2 Display detailed information for one specific grid: :: mapproxy-util grids --grid localgrid --mapproxy-conf /path/to/mapproxy.yaml :: localgrid: Configuration: bbox: [5, 50, 10, 55] bbox_srs: 'EPSG:4326' min_res: 10000 origin*: 'sw' srs: 'EPSG:31467' tile_size*: [256, 256] Levels: Resolutions, # x * y = total tiles 00: 10000, # 1 * 1 = 1 01: 5000.0, # 1 * 1 = 1 02: 2500.0, # 1 * 1 = 1 03: 1250.0, # 2 * 2 = 4 04: 625.0, # 3 * 4 = 12 05: 312.5, # 5 * 8 = 40 06: 156.25, # 9 * 15 = 135 07: 78.125, # 18 * 29 = 522 08: 39.0625, # 36 * 57 = 2.052K 09: 19.53125, # 72 * 113 = 8.136K 10: 9.765625, # 144 * 226 = 32.544K 11: 4.8828125, # 287 * 451 = 129.437K 12: 2.44140625, # 574 * 902 = 517.748K 13: 1.220703125, # 1148 * 1804 = 2.071M 14: 0.6103515625, # 2295 * 3607 = 8.278M 15: 0.30517578125, # 4589 * 7213 = 33.100M 16: 0.152587890625, # 9178 * 14426 = 132.402M 17: 0.0762939453125, # 18355 * 28851 = 529.560M 18: 0.03814697265625, # 36709 * 57701 = 2.118G 19: 0.019073486328125, # 73417 * 115402 = 8.472G .. _mapproxy_util_export: ``export`` ========== This sub-command exports tiles from one cache to another. This is similar to the seed tool, but you don't need to edit the configuration. The destination cache, grid and the coverage can be defined on the command line. .. program:: mapproxy-util export Required arguments: .. cmdoption:: -f, --mapproxy-conf The path of the MapProxy configuration of the source cache. .. cmdoption:: --source Name of the source or cache to export. .. cmdoption:: --levels Comma separated list of levels to export. You can also define a range of levels. For example ``'1,2,3,4,5'``, ``'1..10'`` or ``'1,3,4,6..8'``. .. cmdoption:: --grid The tile grid for the export. The option can either be the name of the grid as defined in the in the MapProxy configuration, or it can be the grid definition itself. You can define a grid as a single string of the key-value pairs. The grid definition :ref:`supports all grid parameters `. See below for examples. .. cmdoption:: --dest Destination of the export. Can be a filename, directory or URL, depending on the export ``--type``. .. cmdoption:: --type Choose the export type. See below for a list of all options. Other options: .. cmdoption:: --fetch-missing-tiles If MapProxy should request missing tiles from the source. By default, the export tool will only existing tiles. .. cmdoption:: --coverage, --srs, --where Limit the export to this coverage. You can use a BBOX, WKT files or OGR datasources. See :doc:`coverages`. .. option:: -c N, --concurrency N The number of concurrent export processes. Export types ------------ ``tms``: Export tiles in a TMS like directory structure. ``mapproxy`` or ``tc``: Export tiles like the internal cache directory structure. This is compatible with TileCache. ``mbtile``: Export tiles into a MBTile file. ``sqlite``: Export tiles into SQLite level files. ``geopackage``: Export tiles into a GeoPackage file. ``arcgis``: Export tiles in a ArcGIS exploded cache directory structure. ``compact-v1``: Export tiles as ArcGIS compact cache bundle files (version 1). Examples -------- Export tiles into a TMS directory structure under ``./cache/``. Limit export to the BBOX and levels 0 to 6. :: mapproxy-util export -f mapproxy.yaml --grid osm_grid \ --source osm_cache --dest ./cache/ \ --levels 1..6 --coverage 5,50,10,60 --srs 4326 Export tiles into an MBTiles file. Limit export to a shape coverage. :: mapproxy-util export -f mapproxy.yaml --grid osm_grid \ --source osm_cache --dest osm.mbtiles --type mbtile \ --levels 1..6 --coverage boundaries.shp \ --where 'CNTRY_NAME = "Germany"' --srs 3857 Export tiles into an MBTiles file using a custom grid definition. :: mapproxy-util export -f mapproxy.yaml --levels 1..6 \ --grid "srs='EPSG:4326' bbox=[5,50,10,60] tile_size=[512,512]" \ --source osm_cache --dest osm.mbtiles --type mbtile \ .. _mapproxy_defrag_compact_cache: ``defrag-compact-cache`` ======================== The ArcGIS compact cache format version 1 and 2 are append only. Updating existing tiles will increase the file size. Bundle files become larger and fragmented with time. The ``defrag-compact-cache`` sub-command compacts existing bundle files by rewriting and reorganizing each bundle file. .. program:: mapproxy-util defrag-compact-cache Required arguments: .. cmdoption:: -f, --mapproxy-conf The path of the MapProxy configuration with the configured compact caches. Optional arguments: .. cmdoption:: --caches Comma separated list of caches to defragment. By default all configured compact caches will be defragmented. .. cmdoption:: --min-percent, --min-mb Bundle files with only a minmal fragmentation are skipped. You can define this threshold with ``--min-percent`` as the required minimal percentage of unused space and ``--min-mb`` as the minimal required unused space in megabytes. Both thresholds must be exceeded. Defaults to 10% and 1MB. .. option:: -n, --dry-run This will simulate the defragmentation process. Examples -------- Defragment bundle files from ``map1_cache`` and ``map2_cache`` when they have more than 20% and 5MB of unused space. E.g. a 20 MB bundle file only gets rewritten if it becomes smaller then 15MB after defragmentation; a 500MB bundle file only gets rewritten if it becomes smaller then 400MB after defragmentation. :: mapproxy-util defrag-compact-cache -f mapproxy.yaml \ --min-percent 20 \ --min-mb 5 \ --caches map1_cache,map2_cache mapproxy-1.11.0/doc/mapproxy_util_autoconfig.rst000066400000000000000000000120661320454472400221140ustar00rootroot00000000000000.. _mapproxy_util_autoconfig: ######################## mapproxy-util autoconfig ######################## The ``autoconfig`` sub-command of ``mapproxy-util`` creates MapProxy and MapProxy-seeding configurations based on existing WMS capabilities documents. It creates a ``source`` for each available layer. The source will include a BBOX coverage from the layer extent, ``legendurl`` for legend graphics, ``featureinfo`` for querlyable layers, scale hints and all detected ``supported_srs``. It will duplicate the layer tree to the ``layers`` section of the MapProxy configuration, including the name, title and abstract. The tool will create a cache for each source layer and ``supported_srs`` _if_ there is a grid configured in your ``--base`` configuration for that SRS. The MapProxy layers will use the caches when available, otherwise they will use the source directly (cascaded WMS). .. note:: The tool can help you to create new configations, but it can't predict how you will use the MapProxy services. The generated configuration can be highly inefficient, especially when multiple layers with separate caches are requested at once. Please make sure you understand the configuration and check the documentation for more options that are useful for your use-cases. Options ======= .. program:: mapproxy-util autoconfig .. cmdoption:: --capabilities URL or filename of the WMS capabilities document. The tool will add `REQUEST` and `SERVICE` parameters to the URL as necessary. .. cmdoption:: --output Filename for the created MapProxy configuration. .. cmdoption:: --output-seed Filename for the created MapProxy-seeding configuration. .. cmdoption:: --force Overwrite any existing configuration with the same output filename. .. cmdoption:: --base Base configuration that should be included in the ``--output`` file with the ``base`` option. .. cmdoption:: --overwrite .. cmdoption:: --overwrite-seed YAML configuration that overwrites configuration optoins before the generated configuration is written to ``--output``/``--output-seed``. Example ~~~~~~~ Print configuration on console:: mapproxy-util autoconfig \ --capabilities http://osm.omniscale.net/proxy/service Write MapProxy and MapProxy-seeding configuration to files:: mapproxy-util autoconfig \ --capabilities http://osm.omniscale.net/proxy/service \ --output mapproxy.yaml \ --output-seed seed.yaml Write MapProxy configuration with caches for grids from ``base.yaml``:: mapproxy-util autoconfig \ --capabilities http://osm.omniscale.net/proxy/service \ --output mapproxy.yaml \ --base base.yaml Overwrites ========== It's likely that you need to tweak the created configuration – e.g. to define another coverage, disable featureinfo, etc. You can do this by editing the output file of course, or you can modify the output by defining all changes to an overwrite file. Overwrite files are applied everytime you call ``mapproxy-util autoconfig``. Overwrites are YAML files that will be merged with the created configuration file. The overwrites are applied independently for each ``services``, ``sources``, ``caches`` and ``layers`` section. That means, for example, that you can modify the ``supported_srs`` of a source and the tool will use the updated SRS list to decide which caches will be configured for that source. Example ~~~~~~~ Created configuration:: sources: mysource_wms: type: wms req: url: http://example.org layers: a Overwrite file:: sources: mysource_wms: supported_srs: ['EPSG:4326'] # add new value for mysource_wms req: layers: a,b # overwrite existing value custom_param: 42 # new value Actual configuration written to ``--output``:: sources: mysource_wms: type: wms supported_srs: ['EPSG:4326'] req: url: http://example.org layers: a,b custom_param: 42 Special keys ~~~~~~~~~~~~ There are a few special keys that you can use in your overwrite file. All ^^^ The value of the ``__all__`` key will be merged into all dictionaries. The following overwrite will add ``sessionid`` to the ``req`` options of all ``sources``:: sources: __all__: req: sessionid: 123456789 Extend ^^^^^^ The values of keys ending with ``__extend__`` will be added to existing lists. To add another SRS for one source:: sources: my_wms: supported_srs__extend__: ['EPSG:31467'] Wildcard ^^^^^^^^ The values of keys starting or ending with three underscores (``___``) will be merged with values where the key matches the suffix or prefix. For example, to set ``levels`` for ``osm_webmercator`` and ``aerial_webmercator`` and to set ``refresh_before`` for ``osm_webmercator`` and ``osm_utm32``:: seeds: ____webmercator: levels: from: 0 to: 12 osm____: refresh_before: days: 5 mapproxy-1.11.0/doc/seed.rst000066400000000000000000000401661320454472400157040ustar00rootroot00000000000000Seeding ======= The MapProxy creates all tiles on demand. To improve the performance for commonly requested views it is possible to pre-generate these tiles. The ``mapproxy-seed`` script does this task. The tool can seed one or more polygon or BBOX areas for each cached layer. MapProxy does not seed the tile pyramid level by level, but traverses the tile pyramid depth-first, from bottom to top. This is optimized to work `with` the caches of your operating system and geospatial database, and not against. mapproxy-seed ------------- The command line script expects a seed configuration that describes which tiles from which layer should be generated. See `configuration`_ for the format of the file. Options ~~~~~~~ .. option:: -s , --seed-conf== The seed configuration. You can also pass the configuration as the last argument to ``mapproxy-seed`` .. option:: -f , --proxy-conf= The MapProxy configuration to use. This file should describe all caches and grids that the seed configuration references. .. option:: -c N, --concurrency N The number of concurrent seed worker. Some parts of the seed tool are CPU intensive (image splitting and encoding), use this option to distribute that load across multiple CPUs. To limit the concurrent requests to the source WMS see :ref:`wms_source_concurrent_requests_label` .. option:: -n, --dry-run This will simulate the seed/cleanup process without requesting, creating or removing any tiles. .. option:: --summary Print a summary of all seeding and cleanup tasks and exit. .. option:: -i, --interactive Print a summary of each seeding and cleanup task and ask if ``mapproxy-seed`` should seed/cleanup that task. It will query for each task before it starts. .. option:: --seed= Only seed the named seeding tasks. You can select multiple tasks with a list of comma seperated names, or you can use the ``--seed`` option multiple times. You can use ``ALL`` to select all tasks. This disables all cleanup tasks unless you also use the ``--cleanup`` option. .. option:: --cleanup= Only cleanup the named tasks. You can select multiple tasks with a list of comma seperated names, or you can use the ``--cleanup`` option multiple times. You can use ``ALL`` to select all tasks. This disables all seeding tasks unless you also use the ``--seed`` option. .. option:: --continue Continue an interrupted seed progress. MapProxy will start the seeding progress at the begining if the progress file (``--progress-file``) was not found. MapProxy can only continue if the previous seed was started with the ``--progress-file`` or ``--continue`` option. .. option:: --progress-file Filename where MapProxy stores the seeding progress for the ``--continue`` option. Defaults to ``.mapproxy_seed_progress`` in the current working directory. MapProxy will remove that file after a successful seed. .. option:: --duration Stop seeding process after this duration. This option accepts duration in the following format: 120s, 15m, 4h, 0.5d Use this option in combination with ``--continue`` to be able to resume the seeding. By default, .. option:: --reseed-file File created by ``mapproxy-seed`` at the start of a new seeding. .. option:: --reseed-interval Only start seeding if ``--reseed-file`` is older then this duration. This option accepts duration in the following format: 120s, 15m, 4h, 0.5d Use this option in combination with ``--continue`` to be able to resume the seeding. By default, .. option:: --use-cache-lock Lock each cache to prevent multiple parallel `mapproxy-seed` calls to work on the same cache. It does not lock normal operation of MapProxy. .. option:: --log-config The logging configuration file to use. .. versionadded:: 1.5.0 ``--continue`` and ``--progress-file`` option .. versionadded:: 1.7.0 ``--log-config`` option .. versionadded:: 1.10.0 ``--duration``, ``--reseed-file`` and ``--reseed-interval`` option Examples ~~~~~~~~ Seed with concurrency of 4:: mapproxy-seed -f mapproxy.yaml -c 4 seed.yaml Print summary of all seed tasks and exit:: mapproxy-seed -f mapproxy.yaml -s seed.yaml --summary --seed ALL Interactively select which tasks should be seeded:: mapproxy-seed -f mapproxy.yaml -s seed.yaml -i Seed task1 and task2 and cleanup task3 with concurrency of 2:: mapproxy-seed -f mapproxy.yaml -s seed.yaml -c 2 --seed task1,task2 \ --cleanup task3 Configuration ------------- .. note:: The configuration changed with MapProxy 1.0.0, the old format with ``seeds`` and ``views`` is still supported but will be deprecated in the future. See :ref:`below ` for information about the old format. The configuration is a YAML file with three sections: ``seeds`` Configure seeding tasks. ``cleanups`` Configure cleanup tasks. ``coverages`` Configure coverages for seeding and cleanup tasks. Example ~~~~~~~ :: seeds: myseed1: [...] myseed2 [...] cleanups: mycleanup1: [...] mycleanup2: [...] coverages: mycoverage1: [...] mycoverage2: [...] ``seeds`` --------- Here you can define multiple seeding tasks. A task defines *what* should be seeded. Each task is configured as a dictionary with the name of the task as the key. You can use the names to select single tasks on the command line of ``mapproxy-seed``. ``mapproxy-seed`` will always process one tile pyramid after the other. Each tile pyramid is defined by a cache and a corresponding grid. A cache with multiple grids consists of multiple tile pyramids. You can configure which tile pyramid you want to seed with the ``caches`` and ``grids`` options. You can further limit the part of the tile pyramid with the ``levels`` and ``coverages`` options. Each seed tasks takes the following options: ``caches`` ~~~~~~~~~~ A list with the caches that should be seeded for this task. The names should match the cache names in your MapProxy configuration. ``grids`` ~~~~~~~~~ A list with the grid names that should be seeded for the ``caches``. The names should match the grid names in your mapproxy configuration. All caches of this tasks need to support the grids you specify here. By default, the grids that are common to all configured caches will be seeded. ``levels`` ~~~~~~~~~~ Either a list of levels that should be seeded, or a dictionary with ``from`` and ``to`` that define a range of levels. You can omit ``from`` to start at level 0, or you can omit ``to`` to seed till the last level. By default, all levels will be seeded. Examples:: # seed multiple levels levels: [2, 3, 4, 8, 9] # seed a single level levels: [3] # seed from level 0 to 10 (including level 10) levels: to: 10 # seed from level 3 to 6 (including level 3 and 6) levels: from: 3 to: 6 ``coverages`` ~~~~~~~~~~~~~ A list with coverage names. Limits the seed area to the coverages. By default, the whole coverage of the grids will be seeded. ``refresh_before`` ~~~~~~~~~~~~~~~~~~ Regenerate all tiles that are older than the given date. The date can either be absolute or relative. By default, existing tiles will not be refreshed. MapProxy can also use the last modification time of a file. File paths should be relative to the proxy configuration or absolute. Examples:: # absolute as ISO time refresh_before: time: 2010-10-21T12:35:00 # relative from the start time of the seed process refresh_before: weeks: 1 days: 7 hours: 4 minutes: 15 # modification time of a given file refresh_before: mtime: path/to/file Example ~~~~~~~~ :: seeds: myseed1: caches: [osm_cache] coverages: [germany] grids: [GLOBAL_MERCATOR] levels: to: 10 myseed2 caches: [osm_cache] coverages: [niedersachsen, bremen, hamburg] grids: [GLOBAL_MERCATOR] refresh_before: weeks: 3 levels: from: 11 to: 15 ``cleanups`` ------------ Here you can define multiple cleanup tasks. Each task is configured as a dictionary with the name of the task as the key. You can use the names to select single tasks on the command line of ``mapproxy-seed``. ``caches`` ~~~~~~~~~~ A list with the caches where you want to cleanup old tiles. The names should match the cache names in your mapproxy configuration. ``grids`` ~~~~~~~~~ A list with the grid names for the ``caches`` where you want to cleanup. The names should match the grid names in your mapproxy configuration. All caches of this tasks need to support the grids you specify here. By default, the grids that are common to all configured caches will be used. ``levels`` ~~~~~~~~~~ Either a list of levels that should be cleaned up, or a dictionary with ``from`` and ``to`` that define a range of levels. You can omit ``from`` to start at level 0, or you can omit ``to`` to cleanup till the last level. By default, all levels will be cleaned up. Examples:: # cleanup multiple levels levels: [2, 3, 4, 8, 9] # cleanup a single level levels: [3] # cleanup from level 0 to 10 (including level 10) levels: to: 10 # cleanup from level 3 to 6 (including level 3 and 6) levels: from: 3 to: 6 ``coverages`` ~~~~~~~~~~~~~ A list with coverage names. Limits the cleanup area to the coverages. By default, the whole coverage of the grids will be cleaned up. .. note:: Be careful when cleaning up caches with large coverages and levels with lots of tiles (>14). Without ``coverages``, the seed tool works on the file system level and it only needs to check for existing tiles if they should be removed. With ``coverages``, the seed tool traverses the whole tile pyramid and needs to check every posible tile if it exists and if it should be removed. This is much slower. ``remove_all`` ~~~~~~~~~~~~~~ When set to true, remove all tiles regardless of the time they were created. You still limit the tiles with the ``levels`` and ``coverage`` options. MapProxy will try to remove tiles in a more efficient way with this option. For example: It will remove complete level directories for ``file`` caches instead of comparing each tile with a timestamp. ``remove_before`` ~~~~~~~~~~~~~~~~~ Remove all tiles that are older than the given date. The date can either be absolute or relative. ``remove_before`` defaults to the start time of the seed process, so that newly created tile will not be removed. MapProxy can also use the last modification time of a file. File paths should be relative to the proxy configuration or absolute. Examples:: # absolute as ISO time remove_before: time: 2010-10-21T12:35:00 # relative from the start time of the seed process remove_before: weeks: 1 days: 7 hours: 4 minutes: 15 # modification time of a given file remove_before: mtime: path/to/file Example ~~~~~~~~ :: cleanups: highres: caches: [osm_cache] grids: [GLOBAL_MERCATOR, GLOBAL_SPERICAL] remove_before: days: 14 levels: from: 16 old_project: caches: [osm_cache] grids: [GLOBAL_MERCATOR] coverages: [mypolygon] levels: from: 14 to: 18 ``coverages`` ------------- There are three different ways to describe the extent of a seeding or cleanup task. - a simple rectangular bounding box, - a text file with one or more polygons in WKT format, - polygons from any data source readable with OGR (e.g. Shapefile, GeoJSON, PostGIS) Read the :doc:`coverage documentation ` for more information. .. note:: You will need to install additional dependencies, if you want to use polygons to define your geographical extent of the seeding area, instead of simple bounding boxes. See :doc:`coverage documentation `. Each coverage has a name that is used in the seed and cleanup task configuration. If you don't specify a coverage for a task, then the BBOX of the grid will be used. Example ~~~~~~~ :: coverages: germany: datasource: 'shps/world_boundaries_m.shp' where: 'CNTRY_NAME = "Germany"' srs: 'EPSG:900913' switzerland: datasource: 'polygons/SZ.txt' srs: 'EPSG:900913' austria: bbox: [9.36, 46.33, 17.28, 49.09] srs: 'EPSG:4326' .. _background_seeding: Example: Background seeding --------------------------- .. versionadded:: 1.10.0 The ``--duration`` option allows you run MapProxy seeding for a limited time. In combination with the ``--continue`` option, you can resume the seeding process at a later time. You can use this to call ``mapproxy-seed`` with ``cron`` to seed in the off-hours. However, this will restart the seeding process from the begining everytime the is seeding completed. You can prevent this with the ``--reeseed-interval`` and ``--reseed-file`` option. The follwing example starts seeding for six hours. It will seed for another six hours, everytime you call this command again. Once all seed and cleanup tasks were proccessed the command will exit immediately everytime you call it within 14 days after the first call. After 14 days, the modification time of the ``reseed.time`` file will be updated and the re-seeding process starts again. :: mapproxy-seed -f mapproxy.yaml -s seed.yaml \ --reseed-interval 14d --duration 6h --reseed-file reseed.time \ --continue --progress-file .mapproxy_seed_progress You can use the ``--reseed-file`` as a ``refresh_before`` and ``remove_before`` ``mtime``-file. .. _seed_old_configuration: Old Configuration ----------------- .. note:: The following description is for the old seed configuration. The configuration contains two keys: ``views`` and ``seeds``. ``views`` describes the geographical extents that should be seeded. ``seeds`` links actual layers with those ``views``. Seeds ~~~~~ Contains a dictionary with layer/view mapping.:: seeds: cache1: views: ['world', 'germany', 'oldb'] cache2: views: ['world', 'germany'] remove_before: time: '2009-04-01T14:45:00' # or minutes: 15 hours: 4 days: 9 weeks: 8 `remove_before`: If present, recreate tiles if they are older than the date or time delta. At the end of the seeding process all tiles that are older will be removed. You can either define a fixed time or a time delta. The `time` is a ISO-like date string (no time-zones, no abbreviations). To define time delta use one or more `seconds`, `minutes`, `hours`, `days` or `weeks` entries. Views ~~~~~ Contains a dictionary with all views. Each view describes a coverage/geographical extent and the levels that should be seeded. Coverages ^^^^^^^^^ .. note:: You will need to install additional dependencies, if you want to use polygons to define your geographical extent of the seeding area, instead of simple bounding boxes. See :doc:`coverage documentation `. There are three different ways to describe the extent of the seed view. - a simple rectangular bounding box, - a text file with one or more polygons in WKT format, - polygons from any data source readable with OGR (e.g. Shapefile, PostGIS) Read the :doc:`coverage documentation ` for more information. Other options ~~~~~~~~~~~~~ ``srs``: A list with SRSs. If the layer contains caches for multiple SRS, only the caches that match one of the SRS in this list will be seeded. ``res``: Seed until this resolution is cached. or ``level``: A number until which this layer is cached, or a tuple with a range of levels that should be cached. Example configuration ^^^^^^^^^^^^^^^^^^^^^ :: views: germany: datasource: 'shps/world_boundaries_m.shp' where: 'CNTRY_NAME = "Germany"' srs: 'EPSG:900913' level: [0, 14] srs: ['EPSG:900913', 'EPSG:4326'] switzerland: datasource: 'polygons/SZ.txt' srs: EPSG:900913 level: [0, 14] srs: ['EPSG:900913'] austria: bbox: [9.36, 46.33, 17.28, 49.09] srs: EPSG:4326 level: [0, 14] srs: ['EPSG:900913'] seeds: osm: views: ['germany', 'switzerland', 'austria'] remove_before: time: '2010-02-20T16:00:00' osm_roads: views: ['germany'] remove_before: days: 30 mapproxy-1.11.0/doc/services.rst000066400000000000000000000317511320454472400166070ustar00rootroot00000000000000.. _services: Services ======== The following services are available: - :ref:`wms_service_label` and :ref:`wmsc_service_label` - :ref:`tms_service_label` - :ref:`kml_service_label` - :ref:`wmts_service_label` - :ref:`demo_service_label` You need to add the service to the ``services`` section of your MapProxy configuration to enable it. Some services take additional options. :: services: tms: kml: wms: wmsoption1: xxx wmsoption2: xxx .. index:: WMS Service .. _wms_service_label: Web Map Service (OGC WMS) ------------------------- The WMS server is accessible at ``/service``, ``/ows`` and ``/wms`` and it supports the WMS versions 1.0.0, 1.1.1 and 1.3.0. See :doc:`inspire` for configuring INSPIRE metadata. The WMS service will use all configured :ref:`layers `. The service takes the following additional option. ``attribution`` """"""""""""""" Adds an attribution (copyright) line to all WMS requests. ``text`` The text line of the attribution (e.g. some copyright notice, etc). .. _wms_md: ``md`` """""" ``md`` is for metadata. These fields are used for the WMS ``GetCapabilities`` responses. See the example below for all supported keys. .. versionadded:: 1.8.1 ``keyword_list`` .. _wms_srs: ``srs`` """"""" The ``srs`` option defines which SRS the WMS service supports.:: srs: ['EPSG:4326', 'CRS:84', 'EPSG:900913'] See :ref:`axis order` for further configuration that might be needed for WMS 1.3.0. ``bbox_srs`` """""""""""" .. versionadded:: 1.3.0 The ``bbox_srs`` option controls in which SRS the BBOX is advertised in the capabilities document. It should only contain SRS that are configured in the ``srs`` option. You need to make sure that all layer extents are valid for these SRS. E.g. you can't choose a local SRS like UTM if you're using a global grid without limiting all sources with a ``coverage``. For example, a config with:: services: wms: srs: ['EPSG:4326', 'EPSG:3857', 'EPSG:31467'] bbox_srs: ['EPSG:4326', 'EPSG:3857', 'EPSG:31467'] will show the bbox in the capabilities in EPSG:4326, EPSG:3857 and EPSG:31467. .. versionadded:: 1.7.0 You can also define an explicit bbox for specific SRS. This bbox will overwrite all layer extents for that SRS. The following example will show the actual bbox of each layer in EPSG:4326 and EPSG:3857, but always the specified bbox for EPSG:31467:: services: wms: srs: ['EPSG:4326', 'EPSG:3857', 'EPSG:31467'] bbox_srs: - 'EPSG:4326' - 'EPSG:3857' - srs: 'EPSG:31467' bbox: [2750000, 5000000, 4250000, 6500000] You can use this to offer global datasets with SRS that are only valid in a local region, like UTM zones. .. _wms_image_formats: ``image_formats`` """"""""""""""""" A list of image mime types the server should offer. .. _wms_featureinfo_types: ``featureinfo_types`` """"""""""""""""""""" A list of feature info types the server should offer. Available types are ``text``, ``html``, ``xml`` and ``json``. The types are advertised in the capabilities with the correct mime type. Defaults to ``[text, html, xml]``. ``featureinfo_xslt`` """""""""""""""""""" You can define XSLT scripts to transform outgoing feature information. You can define scripts for different feature info types: ``html`` Define a script for ``INFO_FORMAT=text/html`` requests. ``xml`` Define a script for ``INFO_FORMAT=application/vnd.ogc.gml`` and ``INFO_FORMAT=text/xml`` requests. See :ref:`FeatureInformation for more informaiton `. ``strict`` """""""""" Some WMS clients do not send all required parameters in feature info requests, MapProxy ignores these errors unless you set ``strict`` to ``true``. ``on_source_errors`` """""""""""""""""""" Configure what MapProxy should do when one or more sources return errors or no response at all (e.g. timeout). The default is ``notify``, which adds a text line in the image response for each erroneous source, but only if a least one source was successful. When ``on_source_errors`` is set to ``raise``, MapProxy will return an OGC service exception in any error case. ``max_output_pixels`` """"""""""""""""""""" .. versionadded:: 1.3.0 The maximum output size for a WMS requests in pixel. MapProxy returns an WMS exception in XML format for requests that are larger. Defaults to ``[4000, 4000]`` which will limit the maximum output size to 16 million pixels (i.e. 5000x3000 is still allowed). See also :ref:`globals.cache.max_tile_limit ` for the maximum number of tiles MapProxy will merge together for each layer. ``versions`` """""""""""" .. versionadded:: 1.7.0 A list of WMS version numbers that MapProxy should support. Defaults to ``['1.0.0', '1.1.0', '1.1.1', '1.3.0']``. Full example """""""""""" :: services: wms: srs: ['EPSG:4326', 'CRS:83', 'EPSG:900913'] versions: ['1.1.1'] image_formats: ['image/png', 'image/jpeg'] attribution: text: "© MyCompany" md: title: MapProxy WMS Proxy abstract: This is the fantastic MapProxy. online_resource: http://mapproxy.org/ contact: person: Your Name Here position: Technical Director organization: address: Fakestreet 123 city: Somewhere postcode: 12345 state: XYZ country: Germany phone: +49(0)000-000000-0 fax: +49(0)000-000000-0 email: you@example.org access_constraints: This service is intended for private and evaluation use only. fees: 'None' keyword_list: - vocabulary: GEMET keywords: [Orthoimagery] - keywords: ["View Service", MapProxy] .. index:: WMS-C Service .. _wmsc_service_label: WMS-C """"" The MapProxy WMS service also supports the `WMS Tiling Client Recommendation `_ from OSGeo. If you add ``tiled=true`` to the GetCapabilities request, MapProxy will add metadata about the internal tile structure to the WMS capabilities document. Clients that support WMS-C can use this information to request tiles at the exact tile boundaries. MapProxy can return the tile as-it-is for these requests, the performace is on par with the TMS service. MapProxy will limit the WMS support when ``tiled=true`` is added to the `GetMap` requests and it will return WMS service exceptions for requests that do not match the exact tile boundaries or if the requested image size or format differs. .. index:: TMS Service, Tile Service .. _tms_service_label: Tiled Map Services (TMS) ------------------------ MapProxy supports the `Tile Map Service Specification`_ from the OSGeo. The TMS is available at ``/tms/1.0.0``. The TMS service will use all configured :ref:`layers ` that have a name and single cached source. Any layer grouping will be flattened. Here is an example TMS request: ``/tms/1.0.0/base/EPSG900913/3/1/0.png``. ``png`` is the internal format of the cached tiles. ``base`` is the name of the layer and ``EPSG900913`` is the SRS of the layer. The tiles are also available under the layer name ``base_EPSG900913`` when ``use_grid_names`` is false or unset. A request to ``/tms/1.0.0`` will return the TMS metadata as XML. ``/tms/1.0.0/layername`` will return information about the bounding box, resolutions and tile size of this specific layer. ``use_grid_names`` """""""""""""""""" .. versionadded:: 1.5.0 When set to `true`, MapProxy uses the actual name of the grid as the grid identifier instead of the SRS code. Tiles will then be available under ``/tms/1.0.0/mylayer/mygrid/`` instead of ``/tms/1.0.0/mylayer/EPSG1234/`` or ``/tms/1.0.0/mylayer_EPSG1234/``. Example """"""" :: services: tms: use_grid_names: true .. index:: OpenLayers .. _open_layers_label: OpenLayers """""""""" When you create a map in OpenLayers with an explicit ``mapExtent``, it will request only a single tile for the first (z=0) level. TMS begins with two or four tiles by default, depending on the SRS. MapProxy supports a different TMS mode to support this use-case. MapProxy will start with a single-tile level if you request ``/tiles`` instead of ``/tms``. Alternatively, you can use the OpenLayers TMS option ``zoomOffset`` to compensate the difference. The option is available since OpenLayers 2.10. There is an example available at :ref:`the configuration-examples section`, which shows the use of OpenLayers in combination with an overlay of tiles on top of OpenStreetMap tiles. .. index:: Google Maps .. _google_maps_label: Google Maps """"""""""" The TMS standard counts tiles starting from the lower left corner of the tile grid, while Google Maps and compatible services start at the upper left corner. The ``/tiles`` service accepts an ``origin`` parameter that flips the y-axis accordingly. You can set it to either ``sw`` (south-west), the default, or to ``nw`` (north-west), required for Google Maps. Example:: http://localhost:8080/tiles/osm_EPSG900913/1/0/1.png?origin=nw .. versionadded:: 1.5.0 You can use the ``origin`` option of the TMS service to change the default origin of the tiles service. If you set it to ``nw`` then you can leave the ``?origin=nw`` parameter from the URL. This only works for the tiles service at ``/tiles``, not for the TMS at ``/tms/1.0.0/``. Example:: services: tms: origin: 'nw' .. _`Tile Map Service Specification`: http://wiki.osgeo.org/wiki/Tile_Map_Service_Specification .. index:: KML Service, Super Overlay .. _kml_service_label: Keyhole Markup Language (OGC KML) --------------------------------- MapProxy supports KML version 2.2 for integration into Google Earth. Each layer is available as a Super Overlay – image tiles are loaded on demand when the user zooms to a specific region. The initial KML file is available at ``/kml/layername/EPSG1234/0/0/0.kml``. The tiles are also available under the layer name ``layername_EPSG1234`` when ``use_grid_names`` is false or unset. .. versionadded:: 1.5.0 The initial KML is also available at ``/kml/layername_EPSG1234`` and ``/kml/layername/EPSG1234``. ``use_grid_names`` """""""""""""""""" .. versionadded:: 1.5.0 When set to `true`, MapProxy uses the actual name of the grid as the grid identifier instead of the SRS code. Tiles will then be available under ``/kml/mylayer/mygrid/`` instead of ``/kml/mylayer/EPSG1234/``. Example """"""" :: services: kml: use_grid_names: true .. index:: WMTS Service, Tile Service .. _wmts_service_label: Web Map Tile Services (WMTS) ---------------------------- .. versionadded:: 1.1.0 MapProxy supports the OGC WMTS 1.0.0 specification. The WMTS service is similar to the TMS service and will use all configured :ref:`layers ` that have a name and single cached source. Any layer grouping will be flattened. There are some limitations depending on the grid configuration you use. Please refer to :ref:`grid.origin ` for more information. The metadata (ServiceContact, etc. ) of this service is taken from the WMS configuration. You can add ``md`` to the ``wmts`` configuration to replace the WMS metadata. See :ref:`WMS metadata `. WMTS defines different access methods and MapProxy supports KVP and RESTful access. Both are enabled by default. KVP """ MapProxy supports ``GetCapabilities`` and ``GetTile`` KVP requests. The KVP service is available at ``/service`` and ``/ows``. You can enable or disable the KVP service with the ``kvp`` option. It is enabled by default and you need to enable ``restful`` if you disable this one. :: services: wmts: kvp: false restful: true RESTful """"""" .. versionadded:: 1.3.0 MapProxy supports RESTful WMTS requests with custom URL templates. The RESTful service capabilities are available at ``/wmts/1.0.0/WMTSCapabilities.xml``. You can enable or disable the RESTful service with the ``restful`` option. It is enabled by default and you need to enable ``kvp`` if you disable this one. :: services: wmts: restful: false kvp: true URL Template ~~~~~~~~~~~~ WMTS RESTful services supports custom tile URLs. You can configure your own URL template with the ``restful_template`` option. The default template is ``/{Layer}/{TileMatrixSet}/{TileMatrix}/{TileCol}/{TileRow}.{Format}`` The template variables are identical with the WMTS specification. ``TileMatrixSet`` is the grid name, ``TileMatrix`` is the zoom level, ``TileCol`` and ``TileRow`` are the x and y of the tile. You can access the tile x=3, y=9, z=4 at ``http://example.org//1.0.0/mylayer-mygrid/4-3-9/tile`` with the following configuration:: services: wmts: restful: true restful_template: '/1.0.0/{Layer}-{TileMatrixSet}/{TileMatrix}-{TileCol}-{TileRow}/tile' .. index:: Demo Service, OpenLayers .. _demo_service_label: MapProxy Demo Service --------------------- MapProxy comes with a demo service that lists all configured WMS and TMS layers. You can test each layer with a simple OpenLayers client. The service is available at ``/demo/``. This service takes no further options:: services: demo: mapproxy-1.11.0/doc/sources.rst000066400000000000000000000432761320454472400164540ustar00rootroot00000000000000.. _sources: Sources ####### MapProxy supports the following sources: - :ref:`wms_label` - :ref:`arcgis_label` - :ref:`tiles_label` - :ref:`mapserver_label` - :ref:`mapnik_label` - :ref:`debug_label` You need to choose a unique name for each configured source. This name will be used to reference the source in the ``caches`` and ``layers`` configuration. The sources section looks like:: sources: mysource1: type: xxx type_dependend_option1: a type_dependend_option2: b mysource2: type: yyy type_dependend_option3: c See below for a detailed description of each service. .. _wms_label: WMS """ Use the type ``wms`` to for WMS servers. ``req`` ^^^^^^^ This describes the WMS source. The only required options are ``url`` and ``layers``. You need to set ``transparent`` to ``true`` if you want to use this source as an overlay. :: req: url: http://example.org/service? layers: base,roads transparent: true All other options are added to the query string of the request. :: req: url: http://example.org/service? layers: roads styles: simple map: /path/to/mapfile You can also configure ``sld`` or ``sld_body`` parameters, in this case you can omit ``layers``. ``sld`` can also point to a ``file://``-URL. MapProxy will read this file and use the content as the ``sld_body``. See :ref:`sources with SLD ` for more information. You can omit layers if you use :ref:`tagged_source_names`. ``wms_opts`` ^^^^^^^^^^^^ This option affects what request MapProxy sends to the source WMS server. ``version`` The WMS version number used for requests (supported: 1.0.0, 1.1.0, 1.1.1, 1.3.0). Defaults to 1.1.1. ``legendgraphic`` If this is set to ``true``, MapProxy will request legend graphics from this source. Each MapProxy WMS layer that contains one or more sources with legend graphics will then have a LegendURL. ``legendurl`` Configure a URL to an image that should be returned as the legend for this source. Local URLs (``file://``) are also supported. ``map`` If this is set to ``false``, MapProxy will not request images from this source. You can use this option in combination with ``featureinfo: true`` to create a source that is only used for feature info requests. ``featureinfo`` If this is set to ``true``, MapProxy will mark the layer as queryable and incoming `GetFeatureInfo` requests will be forwarded to the source server. ``featureinfo_xslt`` Path to an XSLT script that should be used to transform incoming feature information. ``featureinfo_format`` The ``INFO_FORMAT`` for FeatureInfo requests. By default MapProxy will use the same format as requested by the client. ``featureinfo_xslt`` and ``featureinfo_format`` See :ref:`FeatureInformation for more information `. ``coverage`` ^^^^^^^^^^^^ Define the covered area of the source. The source will only be requested if there is an intersection between the requested data and the coverage. See :doc:`coverages ` for more information about the configuration. The intersection is calculated for meta-tiles and not the actual client request, so you should expect more visible data at the coverage boundaries. .. _wms_seed_only: ``seed_only`` ^^^^^^^^^^^^^ Disable this source in regular mode. If set to ``true``, this source will always return a blank/transparent image. The source will only be requested during the seeding process. You can use this option to run MapProxy in an offline mode. .. _source_minmax_res: ``min_res``, ``max_res`` or ``min_scale``, ``max_scale`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. NOTE paragraph also in configuration/layers section Limit the source to the given min and max resolution or scale. MapProxy will return a blank image for requests outside of these boundaries (``min_res`` is inclusive, ``max_res`` exclusive). You can use either the resolution or the scale values, missing values will be interpreted as `unlimited`. Resolutions should be in meters per pixel. The values will also apear in the capabilities documents (i.e. WMS ScaleHint and Min/MaxScaleDenominator). The boundaries will be regarded for each source, but the values in the capabilities might differ if you combine multiple sources or if the MapProxy layer already has a ``min/max_res`` configuration. Please read :ref:`scale vs. resolution ` for some notes on `scale`. .. _supported_srs: ``supported_srs`` ^^^^^^^^^^^^^^^^^ A list with SRSs that the WMS source supports. MapProxy will only query the source in these SRSs. It will reproject data if it needs to get data from this layer in any other SRS. You don't need to configure this if you only use this WMS as a cache source and the WMS supports all SRS of the cache. If MapProxy needs to reproject and the source has multiple ``supported_srs``, then it will use the first projected SRS for requests in a projected SRS, or the first geographic SRS for requests in a geographic SRS. E.g when `supported_srs` is ``['EPSG:4326', 'EPSG:31467']`` caches with EPSG:3857 (projected, meter) will use EPSG:31467 (projected, meter) and not EPSG:4326 (geographic, lat/long). .. .. note:: For the configuration of SRS for MapProxy see `srs_configuration`_. ``forward_req_params`` ^^^^^^^^^^^^^^^^^^^^^^ .. versionadded:: 1.5.0 A list with request parameters that will be forwarded to the source server (if available in the original request). A typical use case of this feature would be to forward the `TIME` parameter when working with a WMS-T server. This feature only works with :ref:`uncached sources `. ``supported_formats`` ^^^^^^^^^^^^^^^^^^^^^ Use this option to specify which image formats your source WMS supports. MapProxy only requests images in one of these formats, and will convert any image if it needs another format. If you do not supply this options, MapProxy assumes that the source supports all formats. ``image`` ^^^^^^^^^ See :ref:`image_options` for other options. ``transparent_color`` Specify a color that should be converted to full transparency. Can be either a list of color values (``[255, 255, 255]``) or a hex string (``#ffffff``). ``transparent_color_tolerance`` Tolerance for the ``transparent_color`` substitution. The value defines the tolerance in each direction. E.g. a tolerance of 5 and a color value of 100 will convert colors in the range of 95 to 105. :: image: transparent_color: '#ffffff' transparent_color_tolerance: 20 .. _wms_source_concurrent_requests_label: ``concurrent_requests`` ^^^^^^^^^^^^^^^^^^^^^^^ This limits the number of parallel requests MapProxy will issue to the source server. It even works across multiple WMS sources as long as all have the same ``concurrent_requests`` value and all ``req.url`` parameters point to the same host. Defaults to 0, which means no limitation. ``http`` ^^^^^^^^ You can configure the following HTTP related options for this source: - ``method`` - ``headers`` - ``client_timeout`` - ``ssl_ca_certs`` - ``ssl_no_cert_checks`` See :ref:`HTTP Options ` for detailed documentation. .. _tagged_source_names: Tagged source names ^^^^^^^^^^^^^^^^^^^ .. versionadded:: 1.1.0 MapProxy supports tagged source names for most sources. This allows you to define the layers of a source in the caches or (WMS)-layers configuration. Instead of referring to a source by the name alone, you can add a list of comma delimited layers: ``sourcename:lyr1,lyr2``. You need to use quotes for tagged source names. This works for layers and caches:: layers: - name: test title: Test Layer sources: ['wms1:lyr1,lyr2'] caches: cache1: sources: ['wms1:lyrA,lyrB'] [...] sources: wms1: type: wms req: url: http://example.org/service? You can either omit the ``layers`` in the ``req`` parameter, or you can use them to limit the tagged layers. In this case MapProxy will raise an error if you configure ``layers: lyr1,lyr2`` and then try to access ``wms:lyr2,lyr3`` for example. Example configuration ^^^^^^^^^^^^^^^^^^^^^ Minimal example:: my_minimal_wmssource: type: wms req: url: http://localhost:8080/service? layers: base Full example:: my_wmssource: type: wms wms_opts: version: 1.0.0 featureinfo: True supported_srs: ['EPSG:4326', 'EPSG:31467'] image: transparent_color: '#ffffff' transparent_color_tolerance: 0 coverage: polygons: GM.txt polygons_srs: EPSG:900913 forward_req_params: ['TIME', 'CUSTOM'] req: url: http://localhost:8080/service?mycustomparam=foo layers: roads another_param: bar transparent: true .. _arcgis_label: ArcGIS REST API """"""""""""""" .. versionadded: 1.9.0 Use the type ``arcgis`` for ArcGIS MapServer and ImageServer REST server endpoints. This source is based on :ref:`the WMS source ` and most WMS options apply to the ArcGIS source too. ``req`` ^^^^^^^ This describes the ArcGIS source. The only required option is ``url``. You need to set ``transparent`` to ``true`` if you want to use this source as an overlay. You can also add ArcGIS specific parameters to ``req``, for example to set the `interpolation method for ImageServers `_. ``opts`` ^^^^^^^^ .. versionadded:: 1.10.0 .. versionadded:: 1.11.0 ``map`` option This option affects what request MapProxy sends to the source ArcGIS server. ``featureinfo`` If this is set to ``true``, MapProxy will mark the layer as queryable and incoming `GetFeatureInfo` requests will be forwarded as ``identify`` requests to the source server. ArcGIS REST server support only HTML and JSON format. You need to enable support for JSON :ref:`wms_featureinfo_types`. ``featureinfo_return_geometries`` Whether the source should include the feature geometries. ``featureinfo_tolerance`` Tolerance in pixel within the ArcGIS server should identify features. ``map`` If this is set to ``false``, MapProxy will not request images from this source. You can use this option in combination with ``featureinfo: true`` to create a source that is only used for feature info requests. ``seed_only`` ^^^^^^^^^^^^^ .. versionadded:: 1.11.0 See :ref:`seed_only ` Example configuration ^^^^^^^^^^^^^^^^^^^^^ MapServer example:: my_minimal_arcgissource: type: arcgis req: layers: show: 0,1 url: http://example.org/ArcGIS/rest/services/Imagery/MapService transparent: true ImageServer example:: my_arcgissource: type: arcgis coverage: polygons: GM.txt srs: EPSG:3857 req: url: http://example.org/ArcGIS/rest/services/World/MODIS/ImageServer interpolation: RSP_CubicConvolution bandIds: 2,0,1 .. _tiles_label: Tiles """"" Use the type ``tile`` to request data from from existing tile servers like TileCache and GeoWebCache. You can also use this source cascade MapProxy installations. ``url`` ^^^^^^^ This source takes a ``url`` option that contains a URL template. The template format is ``%(key_name)s``. MapProxy supports the following named variables in the URL: ``x``, ``y``, ``z`` The tile coordinate. ``format`` The format of the tile. ``quadkey`` Quadkey for the tile as described in http://msdn.microsoft.com/en-us/library/bb259689.aspx ``tc_path`` TileCache path like ``09/000/000/264/000/000/345``. Note that it does not contain any format extension. ``tms_path`` TMS path like ``5/12/9``. Note that it does not contain the version, the layername or the format extension. ``arcgiscache_path`` ArcGIS cache path like ``L05/R00000123/C00000abc``. Note that it does not contain any format extension. ``bbox`` Bounding box of the tile. For WMS-C servers that expect a fixed parameter order. .. versionadded:: 1.1.0 ``arcgiscache_path`` and ``bbox`` parameter. ``origin`` ^^^^^^^^^^ .. deprecated:: 1.3.0 Use grid with the ``origin`` option. The origin of the tile grid (i.e. the location of the 0,0 tile). Supported values are ``sw`` for south-west (lower-left) origin or ``nw`` for north-west (upper-left) origin. ``sw`` is the default. ``grid`` ^^^^^^^^ The grid of the tile source. Defaults to ``GLOBAL_MERCATOR``, a grid that is compatible with popular web mapping applications. ``coverage`` ^^^^^^^^^^^^ Define the covered area of the source. The source will only be requested if there is an intersection between the incoming request and the coverage. See :doc:`coverages ` for more information. ``transparent`` ^^^^^^^^^^^^^^^ You need to set this to ``true`` if you want to use this source as an overlay. ``http`` ^^^^^^^^ You can configure the following HTTP related options for this source: - ``headers`` - ``client_timeout`` - ``ssl_ca_certs`` - ``ssl_no_cert_checks`` See :ref:`HTTP Options ` for detailed documentation. ``seed_only`` ^^^^^^^^^^^^^ See :ref:`seed_only ` ``min_res``, ``max_res`` or ``min_scale``, ``max_scale`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. versionadded:: 1.5.0 See :ref:`source_minmax_res`. ``on_error`` ^^^^^^^^^^^^ .. versionadded:: 1.4.0 You can configure what MapProxy should do when the tile service returns an error. Instead of raising an error, MapProxy can generate a single color tile. You can configure if MapProxy should cache this tile, or if it should use it only to generate a tile or WMS response. You can configure multiple status codes within the ``on_error`` option. You can also use the catch-all value ``other``. This will not only catch all other HTTP status codes, but also source errors like HTTP timeouts or non-image responses. Each status code takes the following options: ``response`` Specify the color of the tile that should be returned in case of this error. Can be either a list of color values (``[255, 255, 255]``, ``[255, 255, 255, 0]``)) or a hex string (``'#ffffff'``, ``'#fa1fbb00'``) with RGBA values, or the string ``transparent``. ``cache`` Set this to ``True`` if MapProxy should cache the single color tile. Otherwise (``False``) MapProxy will use this generated tile only for this request. This is the default. You need to enable ``transparent`` for your source, if you use ``on_error`` responses with transparency. :: my_tile_source: type: tile url: http://localhost:8080/tiles/%(tms_path)s.png transparent: true on_error: 204: response: transparent cache: True 502: response: '#ede9e3' cache: False other: response: '#ff0000' cache: False Example configuration ^^^^^^^^^^^^^^^^^^^^^ :: my_tile_source: type: tile grid: mygrid url: http://localhost:8080/tile?x=%(x)s&y=%(y)s&z=%(z)s&format=%(format)s .. _mapserver_label: Mapserver """"""""" .. versionadded:: 1.1.0 Use the type ``mapserver`` to directly call the Mapserver CGI executable. This source is based on :ref:`the WMS source ` and most options apply to the Mapserver source too. The only differences are that it does not support the ``http`` option and the ``req.url`` parameter is ignored. The ``req.map`` should point to your Mapserver mapfile. The mapfile used must have a WMS server enabled, e.g. with ``wms_enable_request`` or ``ows_enable_request`` in the mapfile. ``mapserver`` ^^^^^^^^^^^^^ You can also set these options in the :ref:`globals-conf-label` section. ``binary`` The complete path to the ``mapserv`` executable. ``working_dir`` Path where the Mapserver should be executed from. It should be the directory where any relative paths in your mapfile are based on. .. versionadded:: 1.11.0 The ``mapserv`` binary is searched in all directories of the ``PATH`` environment, if ``binary`` is not set. Example configuration ^^^^^^^^^^^^^^^^^^^^^ :: my_ms_source: type: mapserver req: layers: base map: /path/to/my.map mapserver: binary: /usr/cgi-bin/mapserv working_dir: /path/to .. _mapnik_label: Mapnik """""" .. versionadded:: 1.1.0 .. versionchanged:: 1.2.0 New ``layers`` option and support for :ref:`tagged sources `. Use the type ``mapnik`` to directly call Mapnik without any WMS service. It uses the Mapnik Python API and you need to have a working Mapnik installation that is accessible by the Python installation that runs MapProxy. A call of ``python -c 'import mapnik'`` should return no error. ``mapfile`` ^^^^^^^^^^^ The filename of you Mapnik XML mapfile. ``layers`` ^^^^^^^^^^ A list of layer names you want to render. MapProxy disables each layer that is not included in this list. It does not reorder the layers and unnamed layers (`Unknown`) are always rendered. ``use_mapnik2`` ^^^^^^^^^^^^^^^ .. versionadded:: 1.3.0 Use Mapnik 2 if set to ``true``. This option is now deprecated and only required for Mapnik 2.0.0. Mapnik 2.0.1 and newer are available as ``mapnik`` package. ``transparent`` ^^^^^^^^^^^^^^^ Set to ``true`` to render from mapnik sources with background-color="transparent", ``false`` (default) will force a black background color. ``scale_factor`` ^^^^^^^^^^^^^^^^ .. versionadded:: 1.8.0 Set the `Mapnik scale_factor `_ option. Mapnik scales most style options like the width of lines and font sizes by this factor. See also :ref:`hq_tiles`. Other options ^^^^^^^^^^^^^ The Mapnik source also supports the ``min_res``/``max_res``/``min_scale``/``max_scale``, ``concurrent_requests``, ``seed_only`` and ``coverage`` options. See :ref:`wms_label`. Example configuration ^^^^^^^^^^^^^^^^^^^^^ :: my_mapnik_source: type: mapnik mapfile: /path/to/mapnik.xml .. _debug_label: Debug """"" Adds information like resolution and BBOX to the response image. This is useful to determine a fixed set of resolutions for the ``res``-parameter. It takes no options. Example:: debug_source: type: debug mapproxy-1.11.0/doc/test.html000066400000000000000000000024171320454472400160740ustar00rootroot00000000000000
mapproxy-1.11.0/doc/tutorial.rst000066400000000000000000000352401320454472400166240ustar00rootroot00000000000000Tutorial ######## This tutorial should give you a quick introduction to the MapProxy configuration. You should have a :doc:`working MapProxy installation `, if you want to follow this tutorial. Configuration format ==================== The configuration of MapProxy uses the YAML format. YAML is a superset of JSON. That means every valid JSON is also valid YAML. MapProxy uses no advanced features of YAML, so you could even use JSON. YAML uses a more readable and user-friendly syntax. We encourage you to use it. If you are familiar with YAML you can skip to the next section. The YAML configuration consist of comments, dictionaries, lists, strings, numbers and booleans. Comments -------- Everything after a hash character (``#``) is a comment and will be ignored. Numbers ------- Any numerical value like ``12``, ``-4``, ``0``, and ``3.1415``. Strings ------- Any string within single or double quotes. You can omit the quotes if the string has no other meaning in YAML syntax. For example:: 'foo' foo '43' # with quotes, otherwise it would be numeric '[string, not a list]' A string with spaces and punctuation. Booleans -------- True or false values:: yes true True no false False List ---- A list is a collection of other valid objects. There are two formats. The condensed form uses square brackets:: [1, 2, 3] [42, string, [another list with a string]] The block form requires every list item on a separate line, starting with ``-`` (dash and a blank):: - 1 - 2 - 3 - 42 - string - [another list] Dictionaries ------------ A dictionary maps keys to values. Values itself can be any valid object. There are two formats. The condensed form uses braces:: {foo: 3, bar: baz} The block form requires every key value pair on a seperate line:: foo: 3 bar: baz You can also nest dictionaries. Each nested dictionary needs to be indented by one or more whitespaces. Tabs are *not* permitted and all keys to the same dictionary need to be indented by the same amount of spaces. :: baz: ham: 2 spam: bam: True inside_baz: 'yepp' Configuration Layout ==================== The MapProxy configuration is a dictionary, each key configures a different aspect of MapProxy. There are the following keys: - ``services``: This is the place to activate and configure MapProxy's services like WMS and TMS. - ``layers``: Configure the layers that MapProxy offers. Each layer can consist of multiple sources and caches. - ``sources``: Define where MapProxy can retrieve new data. - ``caches``: Here you can configure the internal caches. - ``grids``: MapProxy aligns all cached images (tiles) to a grid. Here you can define that grid. - ``globals``: Here you can define some internals of MapProxy and default values that are used in the other configuration directives. The order of the directives is not important, so you can organize it your way. Example Configuration ===================== Configuring a Service --------------------- At first we need to :ref:`configure at least one service `. To enable a service, you have to include its name as a key in the `services` dictionary. For example:: services: tms: Each service is a YAML dictionary, with the service type as the key. The dictionary can be empty, but you need to add the colon so that the configuration parser knows it's a dictionary. A service might accept more configuration options. The WMS service, for example, takes a dictionary with metadata. This data is used in the capabilities documents. Here is an example with some contact information: .. literalinclude:: tutorial.yaml :end-before: #end services `access_constraints` demonstrates how you can write a string over multiple lines, just indent every line the same way as the first. And remember, YAML does not accept tab characters, you must use space. For this tutorial we add another service called `demo`. This is a demo service that lists all configured WMS and TMS layers. You can test each layer with a simple OpenLayers client. So our configuration file should look like:: services: demo: wms: [rest of WMS configuration] Adding a Source ---------------- Next you need to :ref:`define the source ` of your data. Every source has a name and a type. Let's add a WMS source: .. literalinclude:: tutorial.yaml :prepend: sources: :start-after: #start source :end-before: #end source In this example `test_wms` is the name of the source, you need this name later to reference it. Most sources take more parameters – some are optional, some are required. The type `wms` requires the `req` parameter that describes the WMS request. You need to define at least a URL and the layer names, but you can add more options like `transparent` or `format`. Adding a Layer -------------- After defining a source we can use it to :ref:`create a layer ` for the MapProxy WMS. A layer requires a title, which will be used in the capabilities documents and a source. For this layer we want to use our `test_wms` data source: .. literalinclude:: tutorial.yaml :prepend: layers: :start-after: #start cascaded layer :end-before: #end cascaded layer Now we have setuped MapProxy as cascading WMS. That means MapProxy only redirect requests to the WMS defined in `test_wms` data source. Starting the development server ------------------------------- That's it for the first configuration, you can now :ref:`start MapProxy `:: mapproxy-util serve-develop mapproxy.yaml You can :download:`download the configuration `. When you type `localhost:8080/demo/` in the URL of your webbrowser you should see a demo site like shown below. .. image:: imgs/mapproxy-demo.png Here you can see the capabilities of your configured service and watch it in action. Adding a Cache -------------- To speed up the source with MapProxy we :ref:`create a cache ` for this source. Each cache needs to know where it can get new data and how it should be cached. We define our `test_wms` as source for the cache. MapProxy splits images in small tiles and these tiles will be aligned to a grid. It also caches images in different resolutions, like an image pyramid. You can define this image pyramid in detail but we start with one of the default grid definitions of MapProxy. `GLOBAL_GEODETIC` defines a grid that covers the whole world. It uses EPSG:4326 as the spatial reference system and aligns with the default grid and resolutions that OpenLayers uses. Our cache configuration should now look like: .. literalinclude:: tutorial.yaml :start-after: #start caches :end-before: #end caches Adding a cached Layer --------------------- We can now use our defined cache as source for a layer. When the layer is requested by a client, MapProxy looks in the cache for the requested data and only if it hasn't cached the data yet, it requests the `test_wms` data source. The layer configuration should now look like: .. literalinclude:: tutorial.yaml :prepend: layers: :start-after: #start cached layer :end-before: #end cached layer You can :download:`download the configuration `. Defining Resolutions -------------------- By default MapProxy caches traditional power-of-two image pyramids with a default number of cached resolutions of 20. The resolutions between each pyramid level doubles. If you want to change this, you can do so by :ref:`defining your own grid `. Fortunately MapProxy grids provied the ability to inherit from an other grid. We let our grid inherit from the previously used `GLOBAL_GEODETIC` grid and add five fixed resolutions to it. The grid configuration should look like: .. literalinclude:: tutorial.yaml :prepend: grids: :start-after: #start res grid :end-before: #end res grid As you see, we used `base` to inherit from `GLOBAL_GEODETIC` and `res` to define our preferred resolutions. The resolutions are always in the unit of the SRS, in this case in degree per pixel. You can use the :ref:`MapProxy scales util ` to convert between scales and resolutions. Instead of defining fixed resolutions, we can also define a factor that is used to calculate the resolutions. The default value of this factor is 2, but you can set it to each value you want. Just change `res` with `res_factor` and add your preferred factor after it. A magical value of `res_factor` is **sqrt2**, the square root of two. It doubles the number of cached resolutions, so you have 40 instead of 20 available resolutions. Every second resolution is identical to the power-of-two resolutions, so you can use this layer not only in classic WMS clients with free zomming, but also in tile-based clients like OpenLayers which only request in these resolutions. Look at the :ref:`configuration examples for vector data for more information `. Defining a Grid --------------- In the previous section we saw how to extend a grid to provide self defined resolutions, but sometimes `GLOBAL_GEODETIC` grid is not useful because it covers the whole world and we want only a part of it. So let's see how to :ref:`define our own grid `. For this example we define a grid for Germany. We need a spatial reference system (`srs`) that match the region of Germany and a bounding box (`bbox`) around Germany to limit the requestable aera. To make the specification of the `bbox` a little bit easier, we put the `bbox_srs` parameter to the grid configuration. So we can define the `bbox` in EPSG:4326. The `grids` configuration is a dictionary and each grid configuration is identified by its name. We call our grid `germany` and its configuration should look like: .. literalinclude:: tutorial.yaml :prepend: grids: :start-after: #start germany grid :end-before: #end germany grid We have to replace `GLOBAL_GEODETIC` in the cache configuration with our `germany` grid. After that MapProxy caches all data in UTM32. MapProxy request the source in the projection of the grid. You can configure :ref:`the supported SRS for each WMS source ` and MapProxy takes care of any transformations if the `srs` of our grid is different from the data source. You can :download:`download the configuration `. Merging Multiple Layers ----------------------- If you have two WMS and want to offer a single layer with data from both server, you can combine these in one cache. MapProxy will combine the images before it stores the tiles on disk. The sources should be defined from bottom to top and all sources except the bottom need to be transparent. The code below is an example for configure MapProxy to combine two WMS in one cache and one layer: .. literalinclude:: tutorial.yaml :start-after: #start combined sources :end-before: #end combined sources You can :download:`download the configuration `. Coverages --------- Sometimes you don't want to provide the full data of a WMS in a layer. With MapProxy you can define areas where data is available or where data you are interested in is. MapProxy provides three ways to restrict the area of available data: Bounding boxes, polygons and OGR datasource. To keep it simple, we only discuss bounding boxes. For more informations about the other methods take a look at :ref:`the coverages documentation `. To restrict the area with a bounding box, we have to define it in the coverage option of the data source. The listing below restricts the requestable area to Germany: .. literalinclude:: tutorial.yaml :start-after: #start coverage :end-before: #end coverage As you see notation of a coverage bounding box is similar to the notation in the grid option. Meta Tiles and Meta Buffer -------------------------- When you have experience with WMS in tiled clients you should know the problem of labeling issues. MapProxy can help to resolve these issues with two methods called :ref:`Meta Tiling ` and :ref:`Meta Buffering `. There is a :doc:`chapter on WMS labeling issues ` that discusses these options. Seeding ------- Configuration ~~~~~~~~~~~~~ MapProxy creates all tiles on demand. That means, only tiles requested once are cached. Fortunately MapProxy comes with a command line script for pre-generating all required tiles called ``mapproxy-seed``. It has its own configuration file called ``seed.yaml`` and a couple of options. We now create a config file for ``mapproxy-seed``. As all MapProxy configuration files it's notated in YAML. The mandatory option is ``seeds``. Here you can create multiple seeding tasks that define what should be seeded. You can specify a list of caches for seeding with ``caches`` . The cache names should match the names in your MapProxy configuration. If you have specified multiple grids for one cache in your MapProxy configuration, you can select these caches to seed. They must also comply with the caches in your MapProxy configuration. Furthermore you can limit the levels that should be seeded. If you want to seed only a limited area, you can use the ``coverages`` option. In the example below, we configure ``mapproxy-seed`` to seed our previously created cache ``test_wms_cache`` from level 6 to level 16. To show a different possibility to define a coverage, we use a polygon file to determine the area we want to seed. .. literalinclude:: yaml/seed.yaml As you see in the ``coverages`` section the ``polygons`` option point to a text file. This text file contains polygons in Well-Known-Text (WKT) form. The third option tells ``mapproxy-seed`` the ``srs`` of the WKT polygons. You can :download:`download the configuration ` and the :download:`polygon file `. Start Seeding ~~~~~~~~~~~~~ Now it's time to start seeding. ``mapproxy-seed`` has a couple of options. We have to use options ``-s`` to define our ``seed.yaml`` and ``-f`` for our MapProxy configuration file. We also use the ``--dry-run`` option to see what MapProxy would do, without making any actual requests to our sources. A mis-configured seeding can take days or weeks, so you should keep an eye on the tile numbers the dry-run prints out. Run ``mapproxy-seed`` like:: mapproxy-seed -f mapproxy.yaml -s seed.yaml --dry-run If you sure, that seeding works right, remove ``--dry-run``. What's next? ------------ You should read the :doc:`configuration examples ` to get a few more ideas what MapProxy can do. MapProxy has lots of small features that might be useful for your projects, so it is a good idea to read the other chapters of the documentation after that. If you have any questions? We have a `mailing list and IRC channel `_ where you can get support. mapproxy-1.11.0/doc/tutorial.yaml000066400000000000000000000046141320454472400167570ustar00rootroot00000000000000services: wms: md: title: MapProxy WMS Proxy abstract: This is the fantastic MapProxy. online_resource: http://mapproxy.org/ contact: person: Your Name Here position: Technical Director organization: address: Fakestreet 123 city: Somewhere postcode: 12345 country: Germany phone: +49(0)000-000000-0 fax: +49(0)000-000000-0 email: info@omniscale.de access_constraints: This service is intended for private and evaluation use only. The data is licensed as Creative Commons Attribution-Share Alike 2.0 (http://creativecommons.org/licenses/by-sa/2.0/) fees: 'None' #end services sources: #start source test_wms: type: wms req: url: http://osm.omniscale.net/proxy/service? layers: osm #end source #start caches caches: test_wms_cache: sources: [test_wms] grids: [GLOBAL_GEODETIC] #end caches layers: #start cascaded layer - name: cascaded_test title: Cascaded Test Layer sources: [test_wms] #end cascaded layer #start cached layer - name: test_wms_cache title: Cached Test Layer sources: [test_wms_cache] #end cached layer grids: #start res grid res_grid: base: GLOBAL_GEODETIC res: [1, 0.5, 0.25, 0.125, 0.0625] #end res grid #start germany grid germany: srs: 'EPSG:25832' bbox: [6, 47.3, 15.1, 55] bbox_srs: 'EPSG:4326' #end germany grid #start combined sources services: wms: demo: sources: test_wms: type: wms req: url: http://osm.omniscale.net/proxy/service? layers: osm roads_wms: type: wms req: url: http://osm.omniscale.net/proxy/service? layers: osm_roads transparent: true caches: combined_cache: sources: [test_wms, roads_wms] grids: [GLOBAL_GEODETIC] layers: - name: cached_test_wms_with_roads title: Cached Test WMS with Roads sources: [combined_cache] #end combined sources #start coverage sources: test_wms: type: wms req: url: http://osm.omniscale.net/proxy/service? layers: osm coverage: bbox: [5.5, 47.4, 15.2, 54.8] bbox_srs: 'EPSG:4326' #end coverage #start meta caches: meta_cache: sources: [test_wms] grids: [GLOBAL_GEODETIC] meta_size: [4, 4] meta_buffer: 100 #end metamapproxy-1.11.0/doc/yaml/000077500000000000000000000000001320454472400151655ustar00rootroot00000000000000mapproxy-1.11.0/doc/yaml/cache_conf.yaml000066400000000000000000000015361320454472400201260ustar00rootroot00000000000000services: demo: wms: md: title: MapProxy WMS Proxy abstract: This is the fantastic MapProxy. online_resource: http://mapproxy.org/ contact: person: Your Name Here position: Technical Director organization: address: Fakestreet 123 city: Somewhere postcode: 12345 country: Germany phone: +49(0)000-000000-0 fax: +49(0)000-000000-0 email: info@omniscale.de access_constraints: Insert license and copyright information for this service. fees: 'None' sources: test_wms: type: wms req: url: http://osm.omniscale.net/proxy/service? layers: osm caches: test_wms_cache: sources: [test_wms] grids: [GLOBAL_GEODETIC] layers: - name: cached_test title: Cached Test Layer sources: [test_wms_cache] mapproxy-1.11.0/doc/yaml/grid_conf.yaml000066400000000000000000000017521320454472400200100ustar00rootroot00000000000000services: demo: wms: md: title: MapProxy WMS Proxy abstract: This is the fantastic MapProxy. online_resource: http://mapproxy.org/ contact: person: Your Name Here position: Technical Director organization: address: Fakestreet 123 city: Somewhere postcode: 12345 country: Germany phone: +49(0)000-000000-0 fax: +49(0)000-000000-0 email: info@omniscale.de access_constraints: Insert license and copyright information for this service. fees: 'None' sources: test_wms: type: wms req: url: http://osm.omniscale.net/proxy/service? layers: osm caches: test_wms_cache: sources: [test_wms] grids: [germany] layers: - name: cached_grid_test title: Cached Grid Test Layer sources: [test_wms_cache] grids: germany: res: [10000, 7500, 5000, 3500, 2500] srs: 'EPSG:25832' bbox: [6, 47.3, 15.1, 55] bbox_srs: 'EPSG:4326' mapproxy-1.11.0/doc/yaml/merged_conf.yaml000066400000000000000000000020101320454472400203120ustar00rootroot00000000000000services: demo: wms: md: title: MapProxy WMS Proxy abstract: This is the fantastic MapProxy. online_resource: http://mapproxy.org/ contact: person: Your Name Here position: Technical Director organization: address: Fakestreet 123 city: Somewhere postcode: 12345 country: Germany phone: +49(0)000-000000-0 fax: +49(0)000-000000-0 email: info@omniscale.de access_constraints: Insert license and copyright information for this service. fees: 'None' sources: test_wms: type: wms req: url: http://osm.omniscale.net/proxy/service? layers: osm roads_wms: type: wms req: url: http://osm.omniscale.net/proxy/service? layers: osm_roads transparent: true caches: combined_cache: sources: [test_wms, roads_wms] grids: [GLOBAL_GEODETIC] layers: - name: cached_test_wms_with_roads title: Cached Test WMS with Roads sources: [combined_cache] mapproxy-1.11.0/doc/yaml/meta_conf.yaml000066400000000000000000000020221320454472400200000ustar00rootroot00000000000000services: demo: wms: md: title: MapProxy WMS Proxy abstract: This is the fantastic MapProxy. online_resource: http://mapproxy.org/ contact: person: Your Name Here position: Technical Director organization: address: Fakestreet 123 city: Somewhere postcode: 12345 country: Germany phone: +49(0)000-000000-0 fax: +49(0)000-000000-0 email: info@omniscale.de access_constraints: Insert license and copyright information for this service. fees: 'None' sources: test_wms: type: wms req: url: http://osm.omniscale.net/proxy/service? layers: osm coverage: bbox: [5.5, 47.4, 15.2, 54.8] bbox_srs: 'EPSG:4326' caches: test_wms_cache: sources: [test_wms] grids: [GLOBAL_GEODETIC] meta_cache: sources: [test_wms] grids: [GLOBAL_GEODETIC] meta_size: [4, 4] meta_buffer: 100 layers: - name: meta_test title: Meta Test Layer sources: [meta_cache] mapproxy-1.11.0/doc/yaml/seed.yaml000066400000000000000000000003031320454472400167650ustar00rootroot00000000000000seeds: test_cache_seed: caches: [test_wms_cache] levels: from: 6 to: 16 coverages: [germany] coverages: germany: polygons: ./GM.txt polygons_srs: EPSG:900913 mapproxy-1.11.0/doc/yaml/simple_conf.yaml000066400000000000000000000014141320454472400203470ustar00rootroot00000000000000services: demo: wms: md: title: MapProxy WMS Proxy abstract: This is the fantastic MapProxy. online_resource: http://mapproxy.org/ contact: person: Your Name Here position: Technical Director organization: address: Fakestreet 123 city: Somewhere postcode: 12345 country: Germany phone: +49(0)000-000000-0 fax: +49(0)000-000000-0 email: info@omniscale.de access_constraints: Insert license and copyright information for this service. fees: 'None' sources: test_wms: type: wms req: url: http://osm.omniscale.net/proxy/service? layers: osm layers: - name: cascaded_test title: Cascaded Test Layer sources: [test_wms] mapproxy-1.11.0/mapproxy/000077500000000000000000000000001320454472400153355ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/__init__.py000066400000000000000000000000001320454472400174340ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/cache/000077500000000000000000000000001320454472400164005ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/cache/__init__.py000066400000000000000000000023271320454472400205150ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Tile caching (creation, caching and retrieval of tiles). .. digraph:: Schematic Call Graph ranksep = 0.1; node [shape="box", height="0", width="0"] cl [label="CacheMapLayer" href=""] tm [label="TileManager", href=""]; fc [label="FileCache", href=""]; s [label="Source", href=""]; { cl -> tm [label="load_tile_coords"]; tm -> fc [label="load\\nstore\\nis_cached"]; tm -> s [label="get_map"] } """ mapproxy-1.11.0/mapproxy/cache/base.py000066400000000000000000000063341320454472400176720ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import sys import time from contextlib import contextmanager from mapproxy.util.lock import FileLock, cleanup_lockdir, DummyLock class CacheBackendError(Exception): pass @contextmanager def tile_buffer(tile): data = tile.source.as_buffer(seekable=True) data.seek(0) yield data tile.size = data.tell() if not tile.timestamp: tile.timestamp = time.time() data.seek(0) tile.stored = True class TileCacheBase(object): """ Base implementation of a tile cache. """ supports_timestamp = True def load_tile(self, tile, with_metadata=False): raise NotImplementedError() def load_tiles(self, tiles, with_metadata=False): all_succeed = True for tile in tiles: if not self.load_tile(tile, with_metadata=with_metadata): all_succeed = False return all_succeed def store_tile(self, tile): raise NotImplementedError() def store_tiles(self, tiles): all_succeed = True for tile in tiles: if not self.store_tile(tile): all_succeed = False return all_succeed def remove_tile(self, tile): raise NotImplementedError() def remove_tiles(self, tiles): for tile in tiles: self.remove_tile(tile) def is_cached(self, tile): """ Return ``True`` if the tile is cached. """ raise NotImplementedError() def load_tile_metadata(self, tile): """ Fill the metadata attributes of `tile`. Sets ``.timestamp`` and ``.size``. """ raise NotImplementedError() # whether we immediately remove lock files or not REMOVE_ON_UNLOCK = True if sys.platform == 'win32': # windows does not handle this well REMOVE_ON_UNLOCK = False class TileLocker(object): def __init__(self, lock_dir, lock_timeout, lock_cache_id): self.lock_dir = lock_dir self.lock_timeout = lock_timeout self.lock_cache_id = lock_cache_id def lock_filename(self, tile): return os.path.join(self.lock_dir, self.lock_cache_id + '-' + '-'.join(map(str, tile.coord)) + '.lck') def lock(self, tile): """ Returns a lock object for this tile. """ if getattr(self, 'locking_disabled', False): return DummyLock() lock_filename = self.lock_filename(tile) cleanup_lockdir(self.lock_dir, max_lock_time=self.lock_timeout + 10, force=False) return FileLock(lock_filename, timeout=self.lock_timeout, remove_on_unlock=REMOVE_ON_UNLOCK) mapproxy-1.11.0/mapproxy/cache/compact.py000066400000000000000000000521111320454472400204000ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2016-2017 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import contextlib import errno import hashlib import os import shutil import struct from mapproxy.image import ImageSource from mapproxy.cache.base import TileCacheBase, tile_buffer from mapproxy.util.fs import ensure_directory, write_atomic from mapproxy.util.lock import FileLock from mapproxy.compat import BytesIO import logging log = logging.getLogger(__name__) class CompactCacheBase(TileCacheBase): supports_timestamp = False bundle_class = None def __init__(self, cache_dir): self.lock_cache_id = 'compactcache-' + hashlib.md5(cache_dir.encode('utf-8')).hexdigest() self.cache_dir = cache_dir def _get_bundle_fname_and_offset(self, tile_coord): x, y, z = tile_coord level_dir = os.path.join(self.cache_dir, 'L%02d' % z) c = x // BUNDLEX_V1_GRID_WIDTH * BUNDLEX_V1_GRID_WIDTH r = y // BUNDLEX_V1_GRID_HEIGHT * BUNDLEX_V1_GRID_HEIGHT basename = 'R%04xC%04x' % (r, c) return os.path.join(level_dir, basename), (c, r) def _get_bundle(self, tile_coord): bundle_fname, offset = self._get_bundle_fname_and_offset(tile_coord) return self.bundle_class(bundle_fname, offset=offset) def is_cached(self, tile): if tile.coord is None: return True if tile.source: return True return self._get_bundle(tile.coord).is_cached(tile) def store_tile(self, tile): if tile.stored: return True return self._get_bundle(tile.coord).store_tile(tile) def store_tiles(self, tiles): if len(tiles) > 1: # Check if all tiles are from a single bundle. bundle_files = set() tile_coord = None for t in tiles: if t.stored: continue bundle_files.add(self._get_bundle_fname_and_offset(t.coord)[0]) tile_coord = t.coord if len(bundle_files) == 1: return self._get_bundle(tile_coord).store_tiles(tiles) # Tiles are across multiple bundles failed = False for tile in tiles: if not self.store_tile(tile): failed = True return not failed def load_tile(self, tile, with_metadata=False): if tile.source or tile.coord is None: return True return self._get_bundle(tile.coord).load_tile(tile) def load_tiles(self, tiles, with_metadata=False): if len(tiles) > 1: # Check if all tiles are from a single bundle. bundle_files = set() tile_coord = None for t in tiles: if t.source or t.coord is None: continue bundle_files.add(self._get_bundle_fname_and_offset(t.coord)[0]) tile_coord = t.coord if len(bundle_files) == 1: return self._get_bundle(tile_coord).load_tiles(tiles) # No support_bulk_load or tiles are across multiple bundles missing = False for tile in tiles: if not self.load_tile(tile, with_metadata=with_metadata): missing = True return not missing def remove_tile(self, tile): if tile.coord is None: return True return self._get_bundle(tile.coord).remove_tile(tile) def load_tile_metadata(self, tile): if self.load_tile(tile): tile.timestamp = -1 def remove_level_tiles_before(self, level, timestamp): if timestamp == 0: level_dir = os.path.join(self.cache_dir, 'L%02d' % level) shutil.rmtree(level_dir, ignore_errors=True) return True return False BUNDLE_EXT = '.bundle' BUNDLEX_V1_EXT = '.bundlx' class BundleV1(object): def __init__(self, base_filename, offset): self.base_filename = base_filename self.lock_filename = base_filename + '.lck' self.offset = offset def _rel_tile_coord(self, tile_coord): return ( tile_coord[0] % BUNDLEX_V1_GRID_WIDTH, tile_coord[1] % BUNDLEX_V1_GRID_HEIGHT, ) def data(self): return BundleDataV1(self.base_filename + BUNDLE_EXT, self.offset) def index(self): return BundleIndexV1(self.base_filename + BUNDLEX_V1_EXT) def is_cached(self, tile): if tile.source or tile.coord is None: return True with self.index().readonly() as idx: if not idx: return False x, y = self._rel_tile_coord(tile.coord) offset = idx.tile_offset(x, y) if offset == 0: return False with self.data().readonly() as bundle: size = bundle.read_size(offset) return size != 0 def store_tile(self, tile): if tile.stored: return True return self.store_tiles([tile]) def store_tiles(self, tiles): tiles_data = [] for t in tiles: if t.stored: continue with tile_buffer(t) as buf: data = buf.read() tiles_data.append((t.coord, data)) with FileLock(self.lock_filename): with self.data().readwrite() as bundle: with self.index().readwrite() as idx: for tile_coord, data in tiles_data: x, y = self._rel_tile_coord(tile_coord) offset = idx.tile_offset(x, y) offset, size = bundle.append_tile(data, prev_offset=offset) idx.update_tile_offset(x, y, offset=offset, size=size) return True def load_tile(self, tile, with_metadata=False): if tile.source or tile.coord is None: return True return self.load_tiles([tile], with_metadata) def load_tiles(self, tiles, with_metadata=False): missing = False with self.index().readonly() as idx: if not idx: return False with self.data().readonly() as bundle: for t in tiles: if t.source or t.coord is None: continue x, y = self._rel_tile_coord(t.coord) offset = idx.tile_offset(x, y) if offset == 0: missing = True continue data = bundle.read_tile(offset) if not data: missing = True continue t.source = ImageSource(BytesIO(data)) return not missing def remove_tile(self, tile): if tile.coord is None: return True with FileLock(self.lock_filename): with self.index().readwrite() as idx: x, y = self._rel_tile_coord(tile.coord) idx.remove_tile_offset(x, y) return True def size(self): total_size = 0 with self.index().readonly() as idx: if not idx: return 0, 0 with self.data().readonly() as bundle: for y in range(BUNDLEX_V1_GRID_HEIGHT): for x in range(BUNDLEX_V1_GRID_WIDTH): offset = idx.tile_offset(x, y) if not offset: continue size = bundle.read_size(offset) if not size: continue total_size += size + 4 actual_size = os.path.getsize(bundle.filename) return total_size + BUNDLE_V1_HEADER_SIZE + (BUNDLEX_V1_GRID_HEIGHT * BUNDLEX_V1_GRID_WIDTH * 4), actual_size BUNDLEX_V1_GRID_WIDTH = 128 BUNDLEX_V1_GRID_HEIGHT = 128 BUNDLEX_V1_HEADER_SIZE = 16 BUNDLEX_V1_HEADER = b'\x03\x00\x00\x00\x10\x00\x00\x00\x00\x40\x00\x00\x05\x00\x00\x00' BUNDLEX_V1_FOOTER_SIZE = 16 BUNDLEX_V1_FOOTER = b'\x00\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00' INT64LE = struct.Struct(' missing tile yield None else: raise ex @contextlib.contextmanager def readwrite(self): self._init_index() with open(self.filename, 'r+b') as fh: b = BundleIndexV1(self.filename) b._fh = fh yield b # The bundle file has a header with 15 little-endian long values (60 bytes). # NOTE: the fixed values might be some flags for image options (format, aliasing) # all files available for testing had the same values however. BUNDLE_V1_HEADER_SIZE = 60 BUNDLE_V1_HEADER = [ 3 , # 0, fixed 16384 , # 1, max. num of tiles 128*128 = 16384 16 , # 2, size of largest tile 5 , # 3, fixed 0 , # 4, num of tiles in bundle (*4) 60+65536 , # 5, bundle size 40 , # 6 fixed 16 , # 7, fixed 0 , # 8, y0 127 , # 9, y1 0 , # 10, x0 127 , # 11, x1 ] BUNDLE_V1_HEADER_STRUCT_FORMAT = '<4I3Q5I' class BundleDataV1(object): def __init__(self, filename, tile_offsets): self.filename = filename self.tile_offsets = tile_offsets self._fh = None if not os.path.exists(self.filename): self._init_bundle() def _init_bundle(self): ensure_directory(self.filename) header = list(BUNDLE_V1_HEADER) header[10], header[8] = self.tile_offsets header[11], header[9] = header[10]+127, header[8]+127 write_atomic(self.filename, struct.pack(BUNDLE_V1_HEADER_STRUCT_FORMAT, *header) + # zero-size entry for each tile (b'\x00' * (BUNDLEX_V1_GRID_HEIGHT * BUNDLEX_V1_GRID_WIDTH * 4))) @contextlib.contextmanager def readonly(self): with open(self.filename, 'rb') as fh: b = BundleDataV1(self.filename, self.tile_offsets) b._fh = fh yield b @contextlib.contextmanager def readwrite(self): with open(self.filename, 'r+b') as fh: b = BundleDataV1(self.filename, self.tile_offsets) b._fh = fh yield b def read_size(self, offset): if self._fh is None: raise RuntimeError('not called within readonly/readwrite context') self._fh.seek(offset) return struct.unpack(' 0: is_new_tile = False self._fh.seek(0, os.SEEK_END) offset = self._fh.tell() if offset == 0: self._fh.write(b'\x00' * 16) # header offset = 16 self._fh.write(struct.pack('> 40 if size == 0: return 0, 0 offset = val - (size << 40) return offset, size def _load_tile(self, fh, tile): if tile.source or tile.coord is None: return True x, y = self._rel_tile_coord(tile.coord) offset, size = self._tile_offset_size(fh, x, y) if not size: return False fh.seek(offset) data = fh.read(size) tile.source = ImageSource(BytesIO(data)) return True def load_tile(self, tile, with_metadata=False): if tile.source or tile.coord is None: return True return self.load_tiles([tile], with_metadata) def load_tiles(self, tiles, with_metadata=False): missing = False with self._readonly() as fh: if not fh: return False for t in tiles: if t.source or t.coord is None: continue if not self._load_tile(fh, t): missing = True return not missing def is_cached(self, tile): with self._readonly() as fh: if not fh: return False x, y = self._rel_tile_coord(tile.coord) _, size = self._tile_offset_size(fh, x, y) if not size: return False return True def _update_tile_offset(self, fh, x, y, offset, size): idx_offset = self._tile_idx_offset(x, y) val = offset + (size << 40) fh.seek(idx_offset, os.SEEK_SET) fh.write(INT64LE.pack(val)) def _append_tile(self, fh, data): # Write tile size first, then tile data. # Offset points to actual tile data. fh.seek(0, os.SEEK_END) fh.write(struct.pack(' old_tilesize: fh.seek(8) fh.write(struct.pack(' missing tile yield None else: raise ex @contextlib.contextmanager def _readwrite(self): self._init_index() with open(self.filename, 'r+b') as fh: yield fh class CompactCacheV1(CompactCacheBase): bundle_class = BundleV1 class CompactCacheV2(CompactCacheBase): bundle_class = BundleV2 mapproxy-1.11.0/mapproxy/cache/couchdb.py000066400000000000000000000244101320454472400203620ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import codecs import datetime import json import socket import time import hashlib import base64 from mapproxy.image import ImageSource from mapproxy.cache.base import ( TileCacheBase, tile_buffer, CacheBackendError,) from mapproxy.source import SourceError from mapproxy.srs import SRS from mapproxy.compat import string_type, iteritems, BytesIO from threading import Lock try: import requests except ImportError: requests = None import logging log = logging.getLogger(__name__) class UnexpectedResponse(CacheBackendError): pass class CouchDBCache(TileCacheBase): def __init__(self, url, db_name, file_ext, tile_grid, md_template=None, tile_id_template=None): if requests is None: raise ImportError("CouchDB backend requires 'requests' package.") self.lock_cache_id = 'couchdb-' + hashlib.md5((url + db_name).encode('utf-8')).hexdigest() self.file_ext = file_ext self.tile_grid = tile_grid self.md_template = md_template self.couch_url = '%s/%s' % (url.rstrip('/'), db_name.lower()) self.req_session = requests.Session() self.req_session.timeout = 5 self.db_initialised = False self.app_init_db_lock = Lock() self.tile_id_template = tile_id_template def init_db(self): with self.app_init_db_lock: if self.db_initialised: return try: self.req_session.put(self.couch_url) self.db_initialised = True except requests.exceptions.RequestException as ex: log.warn('unable to initialize CouchDB: %s', ex) def tile_url(self, coord): return self.document_url(coord) + '/tile' def document_url(self, coord, relative=False): x, y, z = coord grid_name = self.tile_grid.name couch_url = self.couch_url if relative: if self.tile_id_template: if self.tile_id_template.startswith('%(couch_url)s/'): tile_id_template = self.tile_id_template[len('%(couch_url)s/'):] else: tile_id_template = self.tile_id_template return tile_id_template % locals() else: return '%(grid_name)s-%(z)s-%(x)s-%(y)s' % locals() else: if self.tile_id_template: return self.tile_id_template % locals() else: return '%(couch_url)s/%(grid_name)s-%(z)s-%(x)s-%(y)s' % locals() def is_cached(self, tile): if tile.coord is None or tile.source: return True url = self.document_url(tile.coord) try: self.init_db() resp = self.req_session.get(url) if resp.status_code == 200: doc = json.loads(codecs.decode(resp.content, 'utf-8')) tile.timestamp = doc.get(self.md_template.timestamp_key) return True except (requests.exceptions.RequestException, socket.error) as ex: # is_cached should not fail (would abort seeding for example), # so we catch these errors here and just return False log.warn('error while requesting %s: %s', url, ex) return False if resp.status_code == 404: return False raise SourceError('%r: %r' % (resp.status_code, resp.content)) def _tile_doc(self, tile): tile_id = self.document_url(tile.coord, relative=True) if self.md_template: tile_doc = self.md_template.doc(tile, self.tile_grid) else: tile_doc = {} tile_doc['_id'] = tile_id with tile_buffer(tile) as buf: data = buf.read() tile_doc['_attachments'] = { 'tile': { 'content_type': 'image/' + self.file_ext, 'data': codecs.decode( base64.b64encode(data).replace(b'\n', b''), 'ascii', ), } } return tile_id, tile_doc def _store_bulk(self, tiles): tile_docs = {} for tile in tiles: tile_id, tile_doc = self._tile_doc(tile) tile_docs[tile_id] = tile_doc duplicate_tiles = self._post_bulk(tile_docs) if duplicate_tiles: self._fill_rev_ids(duplicate_tiles) self._post_bulk(duplicate_tiles, no_conflicts=True) return True def _post_bulk(self, tile_docs, no_conflicts=False): """ POST multiple tiles, returns all tile docs with conflicts during POST. """ doc = {'docs': list(tile_docs.values())} data = json.dumps(doc) self.init_db() resp = self.req_session.post(self.couch_url + '/_bulk_docs', data=data, headers={'Content-type': 'application/json'}) if resp.status_code != 201: raise UnexpectedResponse('got unexpected resp (%d) from CouchDB: %s' % (resp.status_code, resp.content)) resp_doc = json.loads(codecs.decode(resp.content, 'utf-8')) duplicate_tiles = {} for tile in resp_doc: if tile.get('error', 'false') == 'conflict': duplicate_tiles[tile['id']] = tile_docs[tile['id']] if no_conflicts and duplicate_tiles: raise UnexpectedResponse('got unexpected resp (%d) from CouchDB: %s' % (resp.status_code, resp.content)) return duplicate_tiles def _fill_rev_ids(self, tile_docs): """ Request all revs for tile_docs and insert it into the tile_docs. """ keys_doc = {'keys': list(tile_docs.keys())} data = json.dumps(keys_doc) self.init_db() resp = self.req_session.post(self.couch_url + '/_all_docs', data=data, headers={'Content-type': 'application/json'}) if resp.status_code != 200: raise UnexpectedResponse('got unexpected resp (%d) from CouchDB: %s' % (resp.status_code, resp.content)) resp_doc = json.loads(codecs.decode(resp.content, 'utf-8')) for tile in resp_doc['rows']: tile_docs[tile['id']]['_rev'] = tile['value']['rev'] def store_tile(self, tile): if tile.stored: return True return self._store_bulk([tile]) def store_tiles(self, tiles): tiles = [t for t in tiles if not t.stored] return self._store_bulk(tiles) def load_tile_metadata(self, tile): if tile.timestamp: return # is_cached loads metadata self.is_cached(tile) def load_tile(self, tile, with_metadata=False): # bulk loading with load_tiles is not implemented, because # CouchDB's /all_docs? does not include attachments if tile.source or tile.coord is None: return True url = self.document_url(tile.coord) + '?attachments=true' self.init_db() resp = self.req_session.get(url, headers={'Accept': 'application/json'}) if resp.status_code == 200: doc = json.loads(codecs.decode(resp.content, 'utf-8')) tile_data = BytesIO(base64.b64decode(doc['_attachments']['tile']['data'])) tile.source = ImageSource(tile_data) tile.timestamp = doc.get(self.md_template.timestamp_key) return True return False def remove_tile(self, tile): if tile.coord is None: return True url = self.document_url(tile.coord) resp = requests.head(url) if resp.status_code == 404: # already removed return True rev_id = resp.headers['etag'] url += '?rev=' + rev_id.strip('"') self.init_db() resp = self.req_session.delete(url) if resp.status_code == 200: return True return False def utc_now_isoformat(): now = datetime.datetime.utcnow() now = now.isoformat() # remove milliseconds, add Zulu timezone now = now.rsplit('.', 1)[0] + 'Z' return now class CouchDBMDTemplate(object): def __init__(self, attributes): self.attributes = attributes for key, value in iteritems(attributes): if value == '{{timestamp}}': self.timestamp_key = key break else: attributes['timestamp'] = '{{timestamp}}' self.timestamp_key = 'timestamp' def doc(self, tile, grid): doc = {} x, y, z = tile.coord for key, value in iteritems(self.attributes): if not isinstance(value, string_type) or not value.startswith('{{'): doc[key] = value continue if value == '{{timestamp}}': doc[key] = time.time() elif value == '{{x}}': doc[key] = x elif value == '{{y}}': doc[key] = y elif value in ('{{z}}', '{{level}}'): doc[key] = z elif value == '{{utc_iso}}': doc[key] = utc_now_isoformat() elif value == '{{wgs_tile_centroid}}': tile_bbox = grid.tile_bbox(tile.coord) centroid = ( tile_bbox[0] + (tile_bbox[2]-tile_bbox[0])/2, tile_bbox[1] + (tile_bbox[3]-tile_bbox[1])/2 ) centroid = grid.srs.transform_to(SRS(4326), centroid) doc[key] = centroid elif value == '{{tile_centroid}}': tile_bbox = grid.tile_bbox(tile.coord) centroid = ( tile_bbox[0] + (tile_bbox[2]-tile_bbox[0])/2, tile_bbox[1] + (tile_bbox[3]-tile_bbox[1])/2 ) doc[key] = centroid else: raise ValueError('unknown CouchDB tile_metadata value: %r' % (value, )) return doc mapproxy-1.11.0/mapproxy/cache/dummy.py000066400000000000000000000020501320454472400201020ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.cache.base import TileCacheBase from mapproxy.util.lock import DummyLock class DummyCache(TileCacheBase): def is_cached(self, tile): return False def lock(self, tile): return DummyLock() def load_tile(self, tile, with_metadata=False): pass def store_tile(self, tile): pass class DummyLocker(object): def lock(self, tile): return DummyLock() mapproxy-1.11.0/mapproxy/cache/file.py000066400000000000000000000141171320454472400176750ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import errno import hashlib from mapproxy.util.fs import ensure_directory, write_atomic from mapproxy.image import ImageSource, is_single_color_image from mapproxy.cache import path from mapproxy.cache.base import TileCacheBase, tile_buffer import logging log = logging.getLogger('mapproxy.cache.file') class FileCache(TileCacheBase): """ This class is responsible to store and load the actual tile data. """ def __init__(self, cache_dir, file_ext, directory_layout='tc', link_single_color_images=False): """ :param cache_dir: the path where the tile will be stored :param file_ext: the file extension that will be appended to each tile (e.g. 'png') """ super(FileCache, self).__init__() self.lock_cache_id = hashlib.md5(cache_dir.encode('utf-8')).hexdigest() self.cache_dir = cache_dir self.file_ext = file_ext self.link_single_color_images = link_single_color_images self._tile_location, self._level_location = path.location_funcs(layout=directory_layout) if self._level_location is None: self.level_location = None # disable level based clean-ups def tile_location(self, tile, create_dir=False): return self._tile_location(tile, self.cache_dir, self.file_ext, create_dir=create_dir) def level_location(self, level): """ Return the path where all tiles for `level` will be stored. >>> c = FileCache(cache_dir='/tmp/cache/', file_ext='png') >>> c.level_location(2) '/tmp/cache/02' """ return self._level_location(level, self.cache_dir) def _single_color_tile_location(self, color, create_dir=False): """ >>> c = FileCache(cache_dir='/tmp/cache/', file_ext='png') >>> c._single_color_tile_location((254, 0, 4)).replace('\\\\', '/') '/tmp/cache/single_color_tiles/fe0004.png' """ parts = ( self.cache_dir, 'single_color_tiles', ''.join('%02x' % v for v in color) + '.' + self.file_ext ) location = os.path.join(*parts) if create_dir: ensure_directory(location) return location def load_tile_metadata(self, tile): location = self.tile_location(tile) try: stats = os.lstat(location) tile.timestamp = stats.st_mtime tile.size = stats.st_size except OSError as ex: if ex.errno != errno.ENOENT: raise tile.timestamp = 0 tile.size = 0 def is_cached(self, tile): """ Returns ``True`` if the tile data is present. """ if tile.is_missing(): location = self.tile_location(tile) if os.path.exists(location): return True else: return False else: return True def load_tile(self, tile, with_metadata=False): """ Fills the `Tile.source` of the `tile` if it is cached. If it is not cached or if the ``.coord`` is ``None``, nothing happens. """ if not tile.is_missing(): return True location = self.tile_location(tile) if os.path.exists(location): if with_metadata: self.load_tile_metadata(tile) tile.source = ImageSource(location) return True return False def remove_tile(self, tile): location = self.tile_location(tile) try: os.remove(location) except OSError as ex: if ex.errno != errno.ENOENT: raise def store_tile(self, tile): """ Add the given `tile` to the file cache. Stores the `Tile.source` to `FileCache.tile_location`. """ if tile.stored: return tile_loc = self.tile_location(tile, create_dir=True) if self.link_single_color_images: color = is_single_color_image(tile.source.as_image()) if color: self._store_single_color_tile(tile, tile_loc, color) else: self._store(tile, tile_loc) else: self._store(tile, tile_loc) def _store(self, tile, location): if os.path.islink(location): os.unlink(location) with tile_buffer(tile) as buf: log.debug('writing %r to %s' % (tile.coord, location)) write_atomic(location, buf.read()) def _store_single_color_tile(self, tile, tile_loc, color): real_tile_loc = self._single_color_tile_location(color, create_dir=True) if not os.path.exists(real_tile_loc): self._store(tile, real_tile_loc) log.debug('linking %r from %s to %s', tile.coord, real_tile_loc, tile_loc) # remove any file before symlinking. # exists() returns False if it links to non- # existing file, islink() test to check that if os.path.exists(tile_loc) or os.path.islink(tile_loc): os.unlink(tile_loc) # Use relative path for the symlink real_tile_loc = os.path.relpath(real_tile_loc, os.path.dirname(tile_loc)) try: os.symlink(real_tile_loc, tile_loc) except OSError as e: # ignore error if link was created by other process if e.errno != errno.EEXIST: raise e return def __repr__(self): return '%s(%r, %r)' % (self.__class__.__name__, self.cache_dir, self.file_ext) mapproxy-1.11.0/mapproxy/cache/geopackage.py000066400000000000000000000664571320454472400210620ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011-2013 Omniscale # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import hashlib import logging import os import re import sqlite3 import threading from mapproxy.cache.base import TileCacheBase, tile_buffer, REMOVE_ON_UNLOCK from mapproxy.compat import BytesIO, PY2, itertools from mapproxy.image import ImageSource from mapproxy.srs import get_epsg_num from mapproxy.util.fs import ensure_directory from mapproxy.util.lock import FileLock log = logging.getLogger(__name__) class GeopackageCache(TileCacheBase): supports_timestamp = False def __init__(self, geopackage_file, tile_grid, table_name, with_timestamps=False, timeout=30, wal=False): self.tile_grid = tile_grid self.table_name = self._check_table_name(table_name) self.lock_cache_id = 'gpkg' + hashlib.md5(geopackage_file.encode('utf-8')).hexdigest() self.geopackage_file = geopackage_file # XXX timestamps not implemented self.supports_timestamp = with_timestamps self.timeout = timeout self.wal = wal self.ensure_gpkg() self._db_conn_cache = threading.local() @property def db(self): if not getattr(self._db_conn_cache, 'db', None): self.ensure_gpkg() self._db_conn_cache.db = sqlite3.connect(self.geopackage_file, timeout=self.timeout) return self._db_conn_cache.db def cleanup(self): """ Close all open connection and remove them from cache. """ if getattr(self._db_conn_cache, 'db', None): self._db_conn_cache.db.close() self._db_conn_cache.db = None @staticmethod def _check_table_name(table_name): """ >>> GeopackageCache._check_table_name("test") 'test' >>> GeopackageCache._check_table_name("test_2") 'test_2' >>> GeopackageCache._check_table_name("test-2") 'test-2' >>> GeopackageCache._check_table_name("test3;") Traceback (most recent call last): ... ValueError: The table_name test3; contains unsupported characters. >>> GeopackageCache._check_table_name("table name") Traceback (most recent call last): ... ValueError: The table_name table name contains unsupported characters. @param table_name: A desired name for an geopackage table. @return: The name of the table if it is good, otherwise an exception. """ # Regex string indicating table names which will be accepted. regex_str = '^[a-zA-Z0-9_-]+$' if re.match(regex_str, table_name): return table_name else: msg = ("The table name may only contain alphanumeric characters, an underscore, " "or a dash: {}".format(regex_str)) log.info(msg) raise ValueError("The table_name {0} contains unsupported characters.".format(table_name)) def ensure_gpkg(self): if not os.path.isfile(self.geopackage_file): with FileLock(self.geopackage_file + '.init.lck', remove_on_unlock=REMOVE_ON_UNLOCK): ensure_directory(self.geopackage_file) self._initialize_gpkg() else: if not self.check_gpkg(): ensure_directory(self.geopackage_file) self._initialize_gpkg() def check_gpkg(self): if not self._verify_table(): return False if not self._verify_gpkg_contents(): return False if not self._verify_tile_size(): return False return True def _verify_table(self): with sqlite3.connect(self.geopackage_file) as db: cur = db.execute("""SELECT name FROM sqlite_master WHERE type='table' AND name=?""", (self.table_name,)) content = cur.fetchone() if not content: # Table doesn't exist _initialize_gpkg will create a new one. return False return True def _verify_gpkg_contents(self): with sqlite3.connect(self.geopackage_file) as db: cur = db.execute("""SELECT * FROM gpkg_contents WHERE table_name = ?""" , (self.table_name,)) results = cur.fetchone() if not results: # Table doesn't exist in gpkg_contents _initialize_gpkg will add it. return False gpkg_data_type = results[1] gpkg_srs_id = results[9] cur = db.execute("""SELECT * FROM gpkg_spatial_ref_sys WHERE srs_id = ?""" , (gpkg_srs_id,)) gpkg_coordsys_id = cur.fetchone()[3] if gpkg_data_type.lower() != "tiles": log.info("The geopackage table name already exists for a data type other than tiles.") raise ValueError("table_name is improperly configured.") if gpkg_coordsys_id != get_epsg_num(self.tile_grid.srs.srs_code): log.info( "The geopackage {0} table name {1} already exists and has an SRS of {2}, which does not match the configured" \ " Mapproxy SRS of {3}.".format(self.geopackage_file, self.table_name, gpkg_coordsys_id, get_epsg_num(self.tile_grid.srs.srs_code))) raise ValueError("srs is improperly configured.") return True def _verify_tile_size(self): with sqlite3.connect(self.geopackage_file) as db: cur = db.execute( """SELECT * FROM gpkg_tile_matrix WHERE table_name = ?""", (self.table_name,)) results = cur.fetchall() results = results[0] tile_size = self.tile_grid.tile_size if not results: # There is no tile conflict. Return to allow the creation of new tiles. return True gpkg_table_name, gpkg_zoom_level, gpkg_matrix_width, gpkg_matrix_height, gpkg_tile_width, gpkg_tile_height, \ gpkg_pixel_x_size, gpkg_pixel_y_size = results resolution = self.tile_grid.resolution(gpkg_zoom_level) if gpkg_tile_width != tile_size[0] or gpkg_tile_height != tile_size[1]: log.info( "The geopackage {0} table name {1} already exists and has tile sizes of ({2},{3})" " which is different than the configure tile sizes of ({4},{5}).".format(self.geopackage_file, self.table_name, gpkg_tile_width, gpkg_tile_height, tile_size[0], tile_size[1])) log.info("The current mapproxy configuration is invalid for this geopackage.") raise ValueError("tile_size is improperly configured.") if not is_close(gpkg_pixel_x_size, resolution) or not is_close(gpkg_pixel_y_size, resolution): log.info( "The geopackage {0} table name {1} already exists and level {2} a resolution of ({3:.13f},{4:.13f})" " which is different than the configured resolution of ({5:.13f},{6:.13f}).".format(self.geopackage_file, self.table_name, gpkg_zoom_level, gpkg_pixel_x_size, gpkg_pixel_y_size, resolution, resolution)) log.info("The current mapproxy configuration is invalid for this geopackage.") raise ValueError("res is improperly configured.") return True def _initialize_gpkg(self): log.info('initializing Geopackage file %s', self.geopackage_file) db = sqlite3.connect(self.geopackage_file) if self.wal: db.execute('PRAGMA journal_mode=wal') proj = get_epsg_num(self.tile_grid.srs.srs_code) stmts = [""" CREATE TABLE IF NOT EXISTS gpkg_contents (table_name TEXT NOT NULL PRIMARY KEY, -- The name of the tiles, or feature table data_type TEXT NOT NULL, -- Type of data stored in the table: "features" per clause Features (http://www.geopackage.org/spec/#features), "tiles" per clause Tiles (http://www.geopackage.org/spec/#tiles), or an implementer-defined value for other data tables per clause in an Extended GeoPackage identifier TEXT UNIQUE, -- A human-readable identifier (e.g. short name) for the table_name content description TEXT DEFAULT '', -- A human-readable description for the table_name content last_change DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), -- Timestamp value in ISO 8601 format as defined by the strftime function %Y-%m-%dT%H:%M:%fZ format string applied to the current time min_x DOUBLE, -- Bounding box minimum easting or longitude for all content in table_name min_y DOUBLE, -- Bounding box minimum northing or latitude for all content in table_name max_x DOUBLE, -- Bounding box maximum easting or longitude for all content in table_name max_y DOUBLE, -- Bounding box maximum northing or latitude for all content in table_name srs_id INTEGER, -- Spatial Reference System ID: gpkg_spatial_ref_sys.srs_id; when data_type is features, SHALL also match gpkg_geometry_columns.srs_id; When data_type is tiles, SHALL also match gpkg_tile_matrix_set.srs.id CONSTRAINT fk_gc_r_srs_id FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys(srs_id)) """, """ CREATE TABLE IF NOT EXISTS gpkg_spatial_ref_sys (srs_name TEXT NOT NULL, -- Human readable name of this SRS (Spatial Reference System) srs_id INTEGER NOT NULL PRIMARY KEY, -- Unique identifier for each Spatial Reference System within a GeoPackage organization TEXT NOT NULL, -- Case-insensitive name of the defining organization e.g. EPSG or epsg organization_coordsys_id INTEGER NOT NULL, -- Numeric ID of the Spatial Reference System assigned by the organization definition TEXT NOT NULL, -- Well-known Text representation of the Spatial Reference System description TEXT) """, """ CREATE TABLE IF NOT EXISTS gpkg_tile_matrix (table_name TEXT NOT NULL, -- Tile Pyramid User Data Table Name zoom_level INTEGER NOT NULL, -- 0 <= zoom_level <= max_level for table_name matrix_width INTEGER NOT NULL, -- Number of columns (>= 1) in tile matrix at this zoom level matrix_height INTEGER NOT NULL, -- Number of rows (>= 1) in tile matrix at this zoom level tile_width INTEGER NOT NULL, -- Tile width in pixels (>= 1) for this zoom level tile_height INTEGER NOT NULL, -- Tile height in pixels (>= 1) for this zoom level pixel_x_size DOUBLE NOT NULL, -- In t_table_name srid units or default meters for srid 0 (>0) pixel_y_size DOUBLE NOT NULL, -- In t_table_name srid units or default meters for srid 0 (>0) CONSTRAINT pk_ttm PRIMARY KEY (table_name, zoom_level), CONSTRAINT fk_tmm_table_name FOREIGN KEY (table_name) REFERENCES gpkg_contents(table_name)) """, """ CREATE TABLE IF NOT EXISTS gpkg_tile_matrix_set (table_name TEXT NOT NULL PRIMARY KEY, -- Tile Pyramid User Data Table Name srs_id INTEGER NOT NULL, -- Spatial Reference System ID: gpkg_spatial_ref_sys.srs_id min_x DOUBLE NOT NULL, -- Bounding box minimum easting or longitude for all content in table_name min_y DOUBLE NOT NULL, -- Bounding box minimum northing or latitude for all content in table_name max_x DOUBLE NOT NULL, -- Bounding box maximum easting or longitude for all content in table_name max_y DOUBLE NOT NULL, -- Bounding box maximum northing or latitude for all content in table_name 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)) """, """ CREATE TABLE IF NOT EXISTS [{0}] (id INTEGER PRIMARY KEY AUTOINCREMENT, -- Autoincrement primary key zoom_level INTEGER NOT NULL, -- min(zoom_level) <= zoom_level <= max(zoom_level) for t_table_name tile_column INTEGER NOT NULL, -- 0 to tile_matrix matrix_width - 1 tile_row INTEGER NOT NULL, -- 0 to tile_matrix matrix_height - 1 tile_data BLOB NOT NULL, -- Of an image MIME type specified in clauses Tile Encoding PNG, Tile Encoding JPEG, Tile Encoding WEBP UNIQUE (zoom_level, tile_column, tile_row)) """.format(self.table_name) ] for stmt in stmts: db.execute(stmt) db.execute("PRAGMA foreign_keys = 1;") # List of WKT execute statements and data.(""" wkt_statement = """ INSERT OR REPLACE INTO gpkg_spatial_ref_sys ( srs_id, organization, organization_coordsys_id, srs_name, definition) VALUES (?, ?, ?, ?, ?) """ wkt_entries = [(3857, 'epsg', 3857, 'WGS 84 / Pseudo-Mercator', """ PROJCS["WGS 84 / Pseudo-Mercator",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"]],AUTHORITY["EPSG","4326"]],\ PROJECTION["Mercator_1SP"],PARAMETER["central_meridian",0],PARAMETER["scale_factor",1],PARAMETER["false_easting",0],\ PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["X",EAST],AXIS["Y",NORTH],\ AUTHORITY["EPSG","3857"]]\ """ ), (4326, 'epsg', 4326, 'WGS 84', """ 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"]],AUTHORITY["EPSG","4326"]]\ """ ), (-1, 'NONE', -1, ' ', 'undefined'), (0, 'NONE', 0, ' ', 'undefined') ] if get_epsg_num(self.tile_grid.srs.srs_code) not in [4326, 3857]: wkt_entries.append((proj, 'epsg', proj, 'Not provided', "Added via Mapproxy.")) db.commit() # Add geopackage version to the header (1.0) db.execute("PRAGMA application_id = 1196437808;") db.commit() for wkt_entry in wkt_entries: try: db.execute(wkt_statement, (wkt_entry[0], wkt_entry[1], wkt_entry[2], wkt_entry[3], wkt_entry[4])) except sqlite3.IntegrityError: log.info("srs_id already exists.".format(wkt_entry[0])) db.commit() # Ensure that tile table exists here, don't overwrite a valid entry. try: db.execute(""" INSERT INTO gpkg_contents ( table_name, data_type, identifier, description, min_x, max_x, min_y, max_y, srs_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); """, (self.table_name, "tiles", self.table_name, "Created with Mapproxy.", self.tile_grid.bbox[0], self.tile_grid.bbox[2], self.tile_grid.bbox[1], self.tile_grid.bbox[3], proj)) except sqlite3.IntegrityError: pass db.commit() # Ensure that tile set exists here, don't overwrite a valid entry. try: db.execute(""" INSERT INTO gpkg_tile_matrix_set (table_name, srs_id, min_x, max_x, min_y, max_y) VALUES (?, ?, ?, ?, ?, ?); """, ( self.table_name, proj, self.tile_grid.bbox[0], self.tile_grid.bbox[2], self.tile_grid.bbox[1], self.tile_grid.bbox[3])) except sqlite3.IntegrityError: pass db.commit() tile_size = self.tile_grid.tile_size for grid, resolution, level in zip(self.tile_grid.grid_sizes, self.tile_grid.resolutions, range(20)): db.execute("""INSERT OR REPLACE INTO gpkg_tile_matrix (table_name, zoom_level, matrix_width, matrix_height, tile_width, tile_height, pixel_x_size, pixel_y_size) VALUES(?, ?, ?, ?, ?, ?, ?, ?) """, (self.table_name, level, grid[0], grid[1], tile_size[0], tile_size[1], resolution, resolution)) db.commit() db.close() def is_cached(self, tile): if tile.coord is None: return True if tile.source: return True return self.load_tile(tile) def store_tile(self, tile): if tile.stored: return True return self._store_bulk([tile]) def store_tiles(self, tiles): tiles = [t for t in tiles if not t.stored] return self._store_bulk(tiles) def _store_bulk(self, tiles): records = [] # tile_buffer (as_buffer) will encode the tile to the target format # we collect all tiles before, to avoid having the db transaction # open during this slow encoding for tile in tiles: with tile_buffer(tile) as buf: if PY2: content = buffer(buf.read()) else: content = buf.read() x, y, level = tile.coord records.append((level, x, y, content)) cursor = self.db.cursor() try: stmt = "INSERT OR REPLACE INTO [{0}] (zoom_level, tile_column, tile_row, tile_data) VALUES (?,?,?,?)".format( self.table_name) cursor.executemany(stmt, records) self.db.commit() except sqlite3.OperationalError as ex: log.warn('unable to store tile: %s', ex) return False return True def load_tile(self, tile, with_metadata=False): if tile.source or tile.coord is None: return True cur = self.db.cursor() cur.execute("""SELECT tile_data FROM [{0}] WHERE tile_column = ? AND tile_row = ? AND zoom_level = ?""".format(self.table_name), tile.coord) content = cur.fetchone() if content: tile.source = ImageSource(BytesIO(content[0])) return True else: return False def load_tiles(self, tiles, with_metadata=False): # associate the right tiles with the cursor tile_dict = {} coords = [] for tile in tiles: if tile.source or tile.coord is None: continue x, y, level = tile.coord coords.append(x) coords.append(y) coords.append(level) tile_dict[(x, y)] = tile if not tile_dict: # all tiles loaded or coords are None return True stmt_base = "SELECT tile_column, tile_row, tile_data FROM [{0}] WHERE ".format(self.table_name) loaded_tiles = 0 # SQLite is limited to 1000 args -> split into multiple requests if more arguments are needed while coords: cur_coords = coords[:999] stmt = stmt_base + ' OR '.join( ['(tile_column = ? AND tile_row = ? AND zoom_level = ?)'] * (len(cur_coords) // 3)) cursor = self.db.cursor() cursor.execute(stmt, cur_coords) for row in cursor: loaded_tiles += 1 tile = tile_dict[(row[0], row[1])] data = row[2] tile.size = len(data) tile.source = ImageSource(BytesIO(data)) cursor.close() coords = coords[999:] return loaded_tiles == len(tile_dict) def remove_tile(self, tile): cursor = self.db.cursor() cursor.execute( "DELETE FROM [{0}] WHERE (tile_column = ? AND tile_row = ? AND zoom_level = ?)".format(self.table_name), tile.coord) self.db.commit() if cursor.rowcount: return True return False def remove_level_tiles_before(self, level, timestamp): if timestamp == 0: cursor = self.db.cursor() cursor.execute( "DELETE FROM [{0}] WHERE (zoom_level = ?)".format(self.table_name), (level,)) self.db.commit() log.info("Cursor rowcount = {0}".format(cursor.rowcount)) if cursor.rowcount: return True return False def load_tile_metadata(self, tile): self.load_tile(tile) class GeopackageLevelCache(TileCacheBase): def __init__(self, geopackage_dir, tile_grid, table_name, timeout=30, wal=False): self.lock_cache_id = 'gpkg-' + hashlib.md5(geopackage_dir.encode('utf-8')).hexdigest() self.cache_dir = geopackage_dir self.tile_grid = tile_grid self.table_name = table_name self.timeout = timeout self.wal = wal self._geopackage = {} self._geopackage_lock = threading.Lock() def _get_level(self, level): if level in self._geopackage: return self._geopackage[level] with self._geopackage_lock: if level not in self._geopackage: geopackage_filename = os.path.join(self.cache_dir, '%s.gpkg' % level) self._geopackage[level] = GeopackageCache( geopackage_filename, self.tile_grid, self.table_name, with_timestamps=True, timeout=self.timeout, wal=self.wal, ) return self._geopackage[level] def cleanup(self): """ Close all open connection and remove them from cache. """ with self._geopackage_lock: for gp in self._geopackage.values(): gp.cleanup() def is_cached(self, tile): if tile.coord is None: return True if tile.source: return True return self._get_level(tile.coord[2]).is_cached(tile) def store_tile(self, tile): if tile.stored: return True return self._get_level(tile.coord[2]).store_tile(tile) def store_tiles(self, tiles): failed = False for level, tiles in itertools.groupby(tiles, key=lambda t: t.coord[2]): tiles = [t for t in tiles if not t.stored] res = self._get_level(level).store_tiles(tiles) if not res: failed = True return failed def load_tile(self, tile, with_metadata=False): if tile.source or tile.coord is None: return True return self._get_level(tile.coord[2]).load_tile(tile, with_metadata=with_metadata) def load_tiles(self, tiles, with_metadata=False): level = None for tile in tiles: if tile.source or tile.coord is None: continue level = tile.coord[2] break if not level: return True return self._get_level(level).load_tiles(tiles, with_metadata=with_metadata) def remove_tile(self, tile): if tile.coord is None: return True return self._get_level(tile.coord[2]).remove_tile(tile) def remove_level_tiles_before(self, level, timestamp): level_cache = self._get_level(level) if timestamp == 0: level_cache.cleanup() os.unlink(level_cache.geopackage_file) return True else: return level_cache.remove_level_tiles_before(level, timestamp) def is_close(a, b, rel_tol=1e-09, abs_tol=0.0): """ See PEP 485, added here for legacy versions. >>> is_close(0.0, 0.0) True >>> is_close(1, 1.0) True >>> is_close(0.01, 0.001) False >>> is_close(0.0001001, 0.0001, rel_tol=1e-02) True >>> is_close(0.0001001, 0.0001) False @param a: An int or float. @param b: An int or float. @param rel_tol: Relative tolerance - maximumed allow difference between two numbers. @param abs_tol: Absolute tolerance - minimum absolute tolerance. @return: True if the values a and b are close. """ return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) mapproxy-1.11.0/mapproxy/cache/legend.py000066400000000000000000000051711320454472400202140ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import hashlib from mapproxy.image import ImageSource from mapproxy.image.opts import ImageOptions from mapproxy.util.fs import ensure_directory, write_atomic import logging log = logging.getLogger(__name__) def legend_identifier(legends): """ >>> legend_identifier([("http://example/?", "foo"), ("http://example/?", "bar")]) 'http://example/?foohttp://example/?bar' :param legends: list of legend URL and layer tuples """ parts = [] for url, layer in legends: parts.append(url) if layer: parts.append(layer) return ''.join(parts) def legend_hash(identifier, scale): md5 = hashlib.md5() md5.update(identifier.encode('utf-8')) md5.update(str(scale).encode('ascii')) return md5.hexdigest() class LegendCache(object): def __init__(self, cache_dir=None, file_ext='png'): self.cache_dir = cache_dir self.file_ext = file_ext def store(self, legend): if legend.stored: return if legend.location is None: hash = legend_hash(legend.id, legend.scale) legend.location = os.path.join(self.cache_dir, hash) + '.' + self.file_ext ensure_directory(legend.location) data = legend.source.as_buffer(ImageOptions(format='image/' + self.file_ext), seekable=True) data.seek(0) log.debug('writing to %s' % (legend.location)) write_atomic(legend.location, data.read()) data.seek(0) legend.stored = True def load(self, legend): hash = legend_hash(legend.id, legend.scale) legend.location = os.path.join(self.cache_dir, hash) + '.' + self.file_ext if os.path.exists(legend.location): legend.source = ImageSource(legend.location) return True return False class Legend(object): def __init__(self, source=None, id=None, scale=None): self.source = source self.stored = None self.location = None self.id = id self.scale = scale mapproxy-1.11.0/mapproxy/cache/mbtiles.py000066400000000000000000000313411320454472400204130ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011-2013 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import glob import hashlib import os import sqlite3 import threading import time from mapproxy.image import ImageSource from mapproxy.cache.base import TileCacheBase, tile_buffer, REMOVE_ON_UNLOCK from mapproxy.util.fs import ensure_directory from mapproxy.util.lock import FileLock from mapproxy.compat import BytesIO, PY2, itertools import logging log = logging.getLogger(__name__) if not hasattr(glob, 'escape'): import re glob.escape = lambda pathname: re.sub(r'([*?[])', r'[\1]', pathname) def sqlite_datetime_to_timestamp(datetime): if datetime is None: return None d = time.strptime(datetime, "%Y-%m-%d %H:%M:%S") return time.mktime(d) class MBTilesCache(TileCacheBase): supports_timestamp = False def __init__(self, mbtile_file, with_timestamps=False, timeout=30, wal=False): self.lock_cache_id = 'mbtiles-' + hashlib.md5(mbtile_file.encode('utf-8')).hexdigest() self.mbtile_file = mbtile_file self.supports_timestamp = with_timestamps self.timeout = timeout self.wal = wal self.ensure_mbtile() self._db_conn_cache = threading.local() @property def db(self): if not getattr(self._db_conn_cache, 'db', None): self.ensure_mbtile() self._db_conn_cache.db = sqlite3.connect(self.mbtile_file, self.timeout) return self._db_conn_cache.db def cleanup(self): """ Close all open connection and remove them from cache. """ if getattr(self._db_conn_cache, 'db', None): self._db_conn_cache.db.close() self._db_conn_cache.db = None def ensure_mbtile(self): if not os.path.exists(self.mbtile_file): with FileLock(self.mbtile_file + '.init.lck', remove_on_unlock=REMOVE_ON_UNLOCK): if not os.path.exists(self.mbtile_file): ensure_directory(self.mbtile_file) self._initialize_mbtile() def _initialize_mbtile(self): log.info('initializing MBTile file %s', self.mbtile_file) db = sqlite3.connect(self.mbtile_file) if self.wal: db.execute('PRAGMA journal_mode=wal') stmt = """ CREATE TABLE tiles ( zoom_level integer, tile_column integer, tile_row integer, tile_data blob """ if self.supports_timestamp: stmt += """ , last_modified datetime DEFAULT (datetime('now','localtime')) """ stmt += """ ); """ db.execute(stmt) db.execute(""" CREATE TABLE metadata (name text, value text); """) db.execute(""" CREATE UNIQUE INDEX idx_tile on tiles (zoom_level, tile_column, tile_row); """) db.commit() db.close() def update_metadata(self, name='', description='', version=1, overlay=True, format='png'): db = sqlite3.connect(self.mbtile_file) db.execute(""" CREATE TABLE IF NOT EXISTS metadata (name text, value text); """) db.execute("""DELETE FROM metadata;""") if overlay: layer_type = 'overlay' else: layer_type = 'baselayer' db.executemany(""" INSERT INTO metadata (name, value) VALUES (?,?) """, ( ('name', name), ('description', description), ('version', version), ('type', layer_type), ('format', format), ) ) db.commit() db.close() def is_cached(self, tile): if tile.coord is None: return True if tile.source: return True return self.load_tile(tile) def store_tile(self, tile): if tile.stored: return True return self._store_bulk([tile]) def store_tiles(self, tiles): tiles = [t for t in tiles if not t.stored] return self._store_bulk(tiles) def _store_bulk(self, tiles): records = [] # tile_buffer (as_buffer) will encode the tile to the target format # we collect all tiles before, to avoid having the db transaction # open during this slow encoding for tile in tiles: with tile_buffer(tile) as buf: if PY2: content = buffer(buf.read()) else: content = buf.read() x, y, level = tile.coord if self.supports_timestamp: records.append((level, x, y, content, time.time())) else: records.append((level, x, y, content)) cursor = self.db.cursor() try: if self.supports_timestamp: stmt = "INSERT OR REPLACE INTO tiles (zoom_level, tile_column, tile_row, tile_data, last_modified) VALUES (?,?,?,?, datetime(?, 'unixepoch', 'localtime'))" cursor.executemany(stmt, records) else: stmt = "INSERT OR REPLACE INTO tiles (zoom_level, tile_column, tile_row, tile_data) VALUES (?,?,?,?)" cursor.executemany(stmt, records) self.db.commit() except sqlite3.OperationalError as ex: log.warn('unable to store tile: %s', ex) return False return True def load_tile(self, tile, with_metadata=False): if tile.source or tile.coord is None: return True cur = self.db.cursor() if self.supports_timestamp: cur.execute('''SELECT tile_data, last_modified FROM tiles WHERE tile_column = ? AND tile_row = ? AND zoom_level = ?''', tile.coord) else: cur.execute('''SELECT tile_data FROM tiles WHERE tile_column = ? AND tile_row = ? AND zoom_level = ?''', tile.coord) content = cur.fetchone() if content: tile.source = ImageSource(BytesIO(content[0])) if self.supports_timestamp: tile.timestamp = sqlite_datetime_to_timestamp(content[1]) return True else: return False def load_tiles(self, tiles, with_metadata=False): #associate the right tiles with the cursor tile_dict = {} coords = [] for tile in tiles: if tile.source or tile.coord is None: continue x, y, level = tile.coord coords.append(x) coords.append(y) coords.append(level) tile_dict[(x, y)] = tile if not tile_dict: # all tiles loaded or coords are None return True if self.supports_timestamp: stmt_base = "SELECT tile_column, tile_row, tile_data, last_modified FROM tiles WHERE " else: stmt_base = "SELECT tile_column, tile_row, tile_data FROM tiles WHERE " loaded_tiles = 0 # SQLite is limited to 1000 args -> split into multiple requests if more arguments are needed while coords: cur_coords = coords[:999] stmt = stmt_base + ' OR '.join( ['(tile_column = ? AND tile_row = ? AND zoom_level = ?)'] * (len(cur_coords) // 3)) cursor = self.db.cursor() cursor.execute(stmt, cur_coords) for row in cursor: loaded_tiles += 1 tile = tile_dict[(row[0], row[1])] data = row[2] tile.size = len(data) tile.source = ImageSource(BytesIO(data)) if self.supports_timestamp: tile.timestamp = sqlite_datetime_to_timestamp(row[3]) cursor.close() coords = coords[999:] return loaded_tiles == len(tile_dict) def remove_tile(self, tile): cursor = self.db.cursor() cursor.execute( "DELETE FROM tiles WHERE (tile_column = ? AND tile_row = ? AND zoom_level = ?)", tile.coord) self.db.commit() if cursor.rowcount: return True return False def remove_level_tiles_before(self, level, timestamp): if timestamp == 0: cursor = self.db.cursor() cursor.execute( "DELETE FROM tiles WHERE (zoom_level = ?)", (level, )) self.db.commit() if cursor.rowcount: return True return False if self.supports_timestamp: cursor = self.db.cursor() cursor.execute( "DELETE FROM tiles WHERE (zoom_level = ? AND last_modified < datetime(?, 'unixepoch', 'localtime'))", (level, timestamp)) self.db.commit() if cursor.rowcount: return True return False def load_tile_metadata(self, tile): if not self.supports_timestamp: # MBTiles specification does not include timestamps. # This sets the timestamp of the tile to epoch (1970s) tile.timestamp = -1 else: self.load_tile(tile) class MBTilesLevelCache(TileCacheBase): supports_timestamp = True def __init__(self, mbtiles_dir, timeout=30, wal=False): self.lock_cache_id = 'sqlite-' + hashlib.md5(mbtiles_dir.encode('utf-8')).hexdigest() self.cache_dir = mbtiles_dir self._mbtiles = {} self.timeout = timeout self.wal = wal self._mbtiles_lock = threading.Lock() def _get_level(self, level): if level in self._mbtiles: return self._mbtiles[level] with self._mbtiles_lock: if level not in self._mbtiles: mbtile_filename = os.path.join(self.cache_dir, '%s.mbtile' % level) self._mbtiles[level] = MBTilesCache( mbtile_filename, with_timestamps=True, timeout=self.timeout, wal=self.wal, ) return self._mbtiles[level] def cleanup(self): """ Close all open connection and remove them from cache. """ with self._mbtiles_lock: for mbtile in self._mbtiles.values(): mbtile.cleanup() def is_cached(self, tile): if tile.coord is None: return True if tile.source: return True return self._get_level(tile.coord[2]).is_cached(tile) def store_tile(self, tile): if tile.stored: return True return self._get_level(tile.coord[2]).store_tile(tile) def store_tiles(self, tiles): failed = False for level, tiles in itertools.groupby(tiles, key=lambda t: t.coord[2]): tiles = [t for t in tiles if not t.stored] res = self._get_level(level).store_tiles(tiles) if not res: failed = True return failed def load_tile(self, tile, with_metadata=False): if tile.source or tile.coord is None: return True return self._get_level(tile.coord[2]).load_tile(tile, with_metadata=with_metadata) def load_tiles(self, tiles, with_metadata=False): level = None for tile in tiles: if tile.source or tile.coord is None: continue level = tile.coord[2] break if not level: return True return self._get_level(level).load_tiles(tiles, with_metadata=with_metadata) def remove_tile(self, tile): if tile.coord is None: return True return self._get_level(tile.coord[2]).remove_tile(tile) def load_tile_metadata(self, tile): self.load_tile(tile) def remove_level_tiles_before(self, level, timestamp): level_cache = self._get_level(level) if timestamp == 0: level_cache.cleanup() os.unlink(level_cache.mbtile_file) for file in glob.glob("%s-*" % glob.escape(level_cache.mbtile_file)): os.unlink(file) return True else: return level_cache.remove_level_tiles_before(level, timestamp) mapproxy-1.11.0/mapproxy/cache/meta.py000066400000000000000000000046331320454472400177060ustar00rootroot00000000000000from __future__ import print_function import struct from mapproxy.cache.base import tile_buffer from mapproxy.image import ImageSource class MetaTileFile(object): def __init__(self, meta_tile): self.meta_tile = meta_tile def write_tiles(self, tiles): tile_positions = [] count = len(tiles) # self.meta_tile.grid_size[0] header_size = ( 4 # META + 4 # metasize**2 + 3*4 # x, y, z + count * 8 #offset/size * tiles ) with open('/tmp/foo.metatile', 'wb') as f: f.write("META") f.write(struct.pack('i', count)) f.write(struct.pack('iii', *tiles[0].coord)) offsets_header_pos = f.tell() f.seek(header_size, 0) for tile in tiles: offset = f.tell() with tile_buffer(tile) as buf: tile_data = buf.read() f.write(tile_data) tile_positions.append((offset, len(tile_data))) f.seek(offsets_header_pos, 0) for offset, size in tile_positions: f.write(struct.pack('ii', offset, size)) def _read_header(self, f): f.seek(0, 0) assert f.read(4) == "META" count, x, y, z = struct.unpack('iiii', f.read(4*4)) tile_positions = [] for i in range(count): offset, size = struct.unpack('ii', f.read(4*2)) tile_positions.append((offset, size)) return tile_positions def read_tiles(self): with open('/tmp/foo.metatile', 'rb') as f: tile_positions = self._read_header(f) for i, (offset, size) in enumerate(tile_positions): f.seek(offset, 0) # img = ImageSource(BytesIO(f.read(size))) open('/tmp/img-%02d.png' % i, 'wb').write(f.read(size)) if __name__ == '__main__': from io import BytesIO from mapproxy.cache.tile import Tile from mapproxy.test.image import create_tmp_image tiles = [] img = create_tmp_image((256, 256)) for x in range(8): for y in range(8): tiles.append(Tile((x, y, 4), ImageSource(BytesIO(img)))) m = MetaTileFile(None) print('!') m.write_tiles(tiles) print('!') m.read_tiles() print('!') x = y = 0 METATILE = 8 for meta in range(METATILE ** 2): print(x + (meta / METATILE), y + (meta % METATILE));mapproxy-1.11.0/mapproxy/cache/path.py000066400000000000000000000171111320454472400177070ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010-2016 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os from mapproxy.compat import string_type from mapproxy.util.fs import ensure_directory def location_funcs(layout): if layout == 'tc': return tile_location_tc, level_location elif layout == 'mp': return tile_location_mp, level_location elif layout == 'tms': return tile_location_tms, level_location elif layout == 'reverse_tms': return tile_location_reverse_tms, None elif layout == 'quadkey': return tile_location_quadkey, no_level_location elif layout == 'arcgis': return tile_location_arcgiscache, level_location_arcgiscache else: raise ValueError('unknown directory_layout "%s"' % layout) def level_location(level, cache_dir): """ Return the path where all tiles for `level` will be stored. >>> level_location(2, '/tmp/cache') '/tmp/cache/02' """ if isinstance(level, string_type): return os.path.join(cache_dir, level) else: return os.path.join(cache_dir, "%02d" % level) def level_part(level): """ Return the path where all tiles for `level` will be stored. >>> level_part(2) '02' >>> level_part('2') '2' """ if isinstance(level, string_type): return level else: return "%02d" % level def tile_location_tc(tile, cache_dir, file_ext, create_dir=False): """ Return the location of the `tile`. Caches the result as ``location`` property of the `tile`. :param tile: the tile object :param create_dir: if True, create all necessary directories :return: the full filename of the tile >>> from mapproxy.cache.tile import Tile >>> tile_location_tc(Tile((3, 4, 2)), '/tmp/cache', 'png').replace('\\\\', '/') '/tmp/cache/02/000/000/003/000/000/004.png' """ if tile.location is None: x, y, z = tile.coord parts = (cache_dir, level_part(z), "%03d" % int(x / 1000000), "%03d" % (int(x / 1000) % 1000), "%03d" % (int(x) % 1000), "%03d" % int(y / 1000000), "%03d" % (int(y / 1000) % 1000), "%03d.%s" % (int(y) % 1000, file_ext)) tile.location = os.path.join(*parts) if create_dir: ensure_directory(tile.location) return tile.location def tile_location_mp(tile, cache_dir, file_ext, create_dir=False): """ Return the location of the `tile`. Caches the result as ``location`` property of the `tile`. :param tile: the tile object :param create_dir: if True, create all necessary directories :return: the full filename of the tile >>> from mapproxy.cache.tile import Tile >>> tile_location_mp(Tile((3, 4, 2)), '/tmp/cache', 'png').replace('\\\\', '/') '/tmp/cache/02/0000/0003/0000/0004.png' >>> tile_location_mp(Tile((12345678, 98765432, 22)), '/tmp/cache', 'png').replace('\\\\', '/') '/tmp/cache/22/1234/5678/9876/5432.png' """ if tile.location is None: x, y, z = tile.coord parts = (cache_dir, level_part(z), "%04d" % int(x / 10000), "%04d" % (int(x) % 10000), "%04d" % int(y / 10000), "%04d.%s" % (int(y) % 10000, file_ext)) tile.location = os.path.join(*parts) if create_dir: ensure_directory(tile.location) return tile.location def tile_location_tms(tile, cache_dir, file_ext, create_dir=False): """ Return the location of the `tile`. Caches the result as ``location`` property of the `tile`. :param tile: the tile object :param create_dir: if True, create all necessary directories :return: the full filename of the tile >>> from mapproxy.cache.tile import Tile >>> tile_location_tms(Tile((3, 4, 2)), '/tmp/cache', 'png').replace('\\\\', '/') '/tmp/cache/2/3/4.png' """ if tile.location is None: x, y, z = tile.coord tile.location = os.path.join( cache_dir, level_part(str(z)), str(x), str(y) + '.' + file_ext ) if create_dir: ensure_directory(tile.location) return tile.location def tile_location_reverse_tms(tile, cache_dir, file_ext, create_dir=False): """ Return the location of the `tile`. Caches the result as ``location`` property of the `tile`. :param tile: the tile object :param create_dir: if True, create all necessary directories :return: the full filename of the tile >>> from mapproxy.cache.tile import Tile >>> tile_location_reverse_tms(Tile((3, 4, 2)), '/tmp/cache', 'png').replace('\\\\', '/') '/tmp/cache/4/3/2.png' """ if tile.location is None: x, y, z = tile.coord tile.location = os.path.join( cache_dir, str(y), str(x), str(z) + '.' + file_ext ) if create_dir: ensure_directory(tile.location) return tile.location def level_location_tms(level, cache_dir): return level_location(str(level), cache_dir=cache_dir) def tile_location_quadkey(tile, cache_dir, file_ext, create_dir=False): """ Return the location of the `tile`. Caches the result as ``location`` property of the `tile`. :param tile: the tile object :param create_dir: if True, create all necessary directories :return: the full filename of the tile >>> from mapproxy.cache.tile import Tile >>> tile_location_quadkey(Tile((3, 4, 2)), '/tmp/cache', 'png').replace('\\\\', '/') '/tmp/cache/11.png' """ if tile.location is None: x, y, z = tile.coord quadKey = "" for i in range(z,0,-1): digit = 0 mask = 1 << (i-1) if (x & mask) != 0: digit += 1 if (y & mask) != 0: digit += 2 quadKey += str(digit) tile.location = os.path.join( cache_dir, quadKey + '.' + file_ext ) if create_dir: ensure_directory(tile.location) return tile.location def no_level_location(level, cache_dir): # dummy for quadkey cache which stores all tiles in one directory raise NotImplementedError('cache does not have any level location') def tile_location_arcgiscache(tile, cache_dir, file_ext, create_dir=False): """ Return the location of the `tile`. Caches the result as ``location`` property of the `tile`. :param tile: the tile object :param create_dir: if True, create all necessary directories :return: the full filename of the tile >>> from mapproxy.cache.tile import Tile >>> tile_location_arcgiscache(Tile((1234567, 87654321, 9)), '/tmp/cache', 'png').replace('\\\\', '/') '/tmp/cache/L09/R05397fb1/C0012d687.png' """ if tile.location is None: x, y, z = tile.coord parts = (cache_dir, 'L%02d' % z, 'R%08x' % y, 'C%08x.%s' % (x, file_ext)) tile.location = os.path.join(*parts) if create_dir: ensure_directory(tile.location) return tile.location def level_location_arcgiscache(z, cache_dir): return level_location('L%02d' % z, cache_dir=cache_dir)mapproxy-1.11.0/mapproxy/cache/redis.py000066400000000000000000000046621320454472400200700ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2017 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import import hashlib from mapproxy.image import ImageSource from mapproxy.cache.base import ( TileCacheBase, tile_buffer, ) from mapproxy.compat import BytesIO try: import redis except ImportError: redis = None import logging log = logging.getLogger(__name__) class RedisCache(TileCacheBase): def __init__(self, host, port, prefix, ttl=0, db=0): if redis is None: raise ImportError("Redis backend requires 'redis' package.") self.prefix = prefix self.lock_cache_id = 'redis-' + hashlib.md5((host + str(port) + prefix + str(db)).encode('utf-8')).hexdigest() self.ttl = ttl self.r = redis.StrictRedis(host=host, port=port, db=db) def _key(self, tile): x, y, z = tile.coord return self.prefix + '-%d-%d-%d' % (z, x, y) def is_cached(self, tile): if tile.coord is None or tile.source: return True return self.r.exists(self._key(tile)) def store_tile(self, tile): if tile.stored: return True key = self._key(tile) with tile_buffer(tile) as buf: data = buf.read() r = self.r.set(key, data) if self.ttl: # use ms expire times for unit-tests self.r.pexpire(key, int(self.ttl * 1000)) return r def load_tile(self, tile, with_metadata=False): if tile.source or tile.coord is None: return True key = self._key(tile) tile_data = self.r.get(key) if tile_data: tile.source = ImageSource(BytesIO(tile_data)) return True return False def remove_tile(self, tile): if tile.coord is None: return True key = self._key(tile) self.r.delete(key) return True mapproxy-1.11.0/mapproxy/cache/renderd.py000066400000000000000000000071651320454472400204060ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2012, 2013 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import time import hashlib try: import json; json except ImportError: json = None try: import requests; requests except ImportError: requests = None from mapproxy.client.log import log_request from mapproxy.cache.tile import TileCreator, Tile from mapproxy.source import SourceError from mapproxy.util.lock import LockTimeout def has_renderd_support(): if not json or not requests: return False return True class RenderdTileCreator(TileCreator): def __init__(self, renderd_address, tile_mgr, dimensions=None, priority=100, tile_locker=None): TileCreator.__init__(self, tile_mgr, dimensions) self.tile_locker = tile_locker.lock or self.tile_mgr.lock self.renderd_address = renderd_address self.priority = priority def _create_single_tile(self, tile): with self.tile_locker(tile): if not self.is_cached(tile): self._create_renderd_tile(tile.coord) self.cache.load_tile(tile) return [tile] def _create_meta_tile(self, meta_tile): main_tile = Tile(meta_tile.main_tile_coord) with self.tile_locker(main_tile): if not all(self.is_cached(t) for t in meta_tile.tiles if t is not None): self._create_renderd_tile(main_tile.coord) tiles = [Tile(coord) for coord in meta_tile.tiles] self.cache.load_tiles(tiles) return tiles def _create_renderd_tile(self, tile_coord): start_time = time.time() result = self._send_tile_request(self.tile_mgr.identifier, [tile_coord]) duration = time.time()-start_time address = '%s:%s:%r' % (self.renderd_address, self.tile_mgr.identifier, tile_coord) if result['status'] == 'error': log_request(address, 500, None, duration=duration, method='RENDERD') raise SourceError("Error from renderd: %s" % result.get('error_message', 'unknown error from renderd')) elif result['status'] == 'lock': log_request(address, 503, None, duration=duration, method='RENDERD') raise LockTimeout("Lock timeout from renderd: %s" % result.get('error_message', 'unknown lock timeout error from renderd')) log_request(address, 200, None, duration=duration, method='RENDERD') def _send_tile_request(self, cache_identifier, tile_coords): identifier = hashlib.sha1(str((cache_identifier, tile_coords)).encode('ascii')).hexdigest() message = { 'command': 'tile', 'id': identifier, 'tiles': tile_coords, 'cache_identifier': cache_identifier, 'priority': self.priority } try: resp = requests.post(self.renderd_address, data=json.dumps(message)) return resp.json() except ValueError: raise SourceError("Error while communicating with renderd: invalid JSON") except requests.RequestException as ex: raise SourceError("Error while communicating with renderd: %s" % ex)mapproxy-1.11.0/mapproxy/cache/riak.py000066400000000000000000000147401320454472400177060ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2013 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import import threading import hashlib from io import BytesIO from mapproxy.image import ImageSource from mapproxy.cache.tile import Tile from mapproxy.cache.base import TileCacheBase, tile_buffer, CacheBackendError try: import riak except ImportError: riak = None import logging log = logging.getLogger(__name__) class UnexpectedResponse(CacheBackendError): pass class RiakCache(TileCacheBase): def __init__(self, nodes, protocol, bucket, tile_grid, use_secondary_index=False, timeout=60): if riak is None: raise ImportError("Riak backend requires 'riak' package.") self.nodes = nodes self.protocol = protocol self.lock_cache_id = 'riak-' + hashlib.md5(bucket.encode('utf-8')).hexdigest() self.request_timeout = timeout * 1000 self.bucket_name = bucket self.tile_grid = tile_grid self.use_secondary_index = use_secondary_index self._db_conn_cache = threading.local() @property def connection(self): if not getattr(self._db_conn_cache, 'connection', None): self._db_conn_cache.connection = riak.RiakClient(protocol=self.protocol, nodes=self.nodes) return self._db_conn_cache.connection @property def bucket(self): if not getattr(self._db_conn_cache, 'bucket', None): self._db_conn_cache.bucket = self.connection.bucket(self.bucket_name) return self._db_conn_cache.bucket def _get_object(self, coord): (x, y, z) = coord key = '%(z)d_%(x)d_%(y)d' % locals() obj = False try: obj = self.bucket.get(key, r=1, timeout=self.request_timeout) except Exception as e: log.warn('error while requesting %s: %s', key, e) if not obj: obj = self.bucket.new(key=key, data=None, content_type='application/octet-stream') return obj def _get_timestamp(self, obj): metadata = obj.usermeta timestamp = metadata.get('timestamp') if timestamp != None: return float(timestamp) obj.usermeta = {'timestamp': '0'} return 0.0 def is_cached(self, tile): return self.load_tile(tile, True) def _store_bulk(self, tiles): for tile in tiles: res = self._get_object(tile.coord) with tile_buffer(tile) as buf: data = buf.read() res.encoded_data = data res.usermeta = { 'timestamp': str(tile.timestamp), 'size': str(tile.size), } if self.use_secondary_index: x, y, z = tile.coord res.add_index('tile_coord_bin', '%02d-%07d-%07d' % (z, x, y)) try: res.store(w=1, dw=1, pw=1, return_body=False, timeout=self.request_timeout) except riak.RiakError as ex: log.warn('unable to store tile: %s', ex) return False return True def store_tile(self, tile): if tile.stored: return True return self._store_bulk([tile]) def store_tiles(self, tiles): tiles = [t for t in tiles if not t.stored] return self._store_bulk(tiles) def load_tile_metadata(self, tile): if tile.timestamp: return # is_cached loads metadata self.load_tile(tile, True) def load_tile(self, tile, with_metadata=False): if tile.timestamp is None: tile.timestamp = 0 if tile.source or tile.coord is None: return True res = self._get_object(tile.coord) if res.exists: tile_data = BytesIO(res.encoded_data) tile.source = ImageSource(tile_data) if with_metadata: tile.timestamp = self._get_timestamp(res) tile.size = len(res.encoded_data) return True return False def remove_tile(self, tile): if tile.coord is None: return True res = self._get_object(tile.coord) if not res.exists: # already removed return True try: res.delete(w=1, r=1, dw=1, pw=1, timeout=self.request_timeout) except riak.RiakError as ex: log.warn('unable to remove tile: %s', ex) return False return True def _fill_metadata_from_obj(self, obj, tile): tile_md = obj.usermeta timestamp = tile_md.get('timestamp') if timestamp: tile.timestamp = float(timestamp) def _key_iterator(self, level): """ Generator for all tile keys in `level`. """ # index() returns a list of all keys so we check for tiles in # batches of `chunk_size`*`chunk_size`. grid_size = self.tile_grid.grid_sizes[level] chunk_size = 256 for x in range(grid_size[0]/chunk_size): start_x = x * chunk_size end_x = start_x + chunk_size - 1 for y in range(grid_size[1]/chunk_size): start_y = y * chunk_size end_y = start_y + chunk_size - 1 query = self.bucket.get_index('tile_coord_bin', '%02d-%07d-%07d' % (level, start_x, start_y), '%02d-%07d-%07d' % (level, end_x, end_y)) for link in query.run(): yield link.get_key() def remove_tiles_for_level(self, level, before_timestamp=None): bucket = self.bucket client = self.connection for key in self._key_iterator(level): if before_timestamp: obj = self.bucket.get(key, r=1) dummy_tile = Tile((0, 0, 0)) self._fill_metadata_from_obj(obj, dummy_tile) if dummy_tile.timestamp < before_timestamp: obj.delete() else: riak.RiakObject(client, bucket, key).delete() mapproxy-1.11.0/mapproxy/cache/s3.py000066400000000000000000000131511320454472400173000ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2016 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import calendar import hashlib import sys import threading from mapproxy.image import ImageSource from mapproxy.cache import path from mapproxy.cache.base import tile_buffer, TileCacheBase from mapproxy.util import async from mapproxy.util.py import reraise_exception try: import boto3 import botocore except ImportError: boto3 = None import logging log = logging.getLogger('mapproxy.cache.s3') _s3_sessions_cache = threading.local() def s3_session(profile_name=None): if not hasattr(_s3_sessions_cache, 'sessions'): _s3_sessions_cache.sessions = {} if profile_name not in _s3_sessions_cache.sessions: _s3_sessions_cache.sessions[profile_name] = boto3.session.Session(profile_name=profile_name) return _s3_sessions_cache.sessions[profile_name] class S3ConnectionError(Exception): pass class S3Cache(TileCacheBase): def __init__(self, base_path, file_ext, directory_layout='tms', bucket_name='mapproxy', profile_name=None, _concurrent_writer=4): super(S3Cache, self).__init__() self.lock_cache_id = hashlib.md5(base_path.encode('utf-8') + bucket_name.encode('utf-8')).hexdigest() self.bucket_name = bucket_name try: self.bucket = self.conn().head_bucket(Bucket=bucket_name) except botocore.exceptions.ClientError as e: if e.response['Error']['Code'] == '404': raise S3ConnectionError('No such bucket: %s' % bucket_name) elif e.response['Error']['Code'] == '403': raise S3ConnectionError('Access denied. Check your credentials') else: reraise_exception( S3ConnectionError('Unknown error: %s' % e), sys.exc_info(), ) self.base_path = base_path self.file_ext = file_ext self._concurrent_writer = _concurrent_writer self._tile_location, _ = path.location_funcs(layout=directory_layout) def tile_key(self, tile): return self._tile_location(tile, self.base_path, self.file_ext).lstrip('/') def conn(self): if boto3 is None: raise ImportError("S3 Cache requires 'boto3' package.") try: return s3_session().client("s3") except Exception as e: raise S3ConnectionError('Error during connection %s' % e) def load_tile_metadata(self, tile): if tile.timestamp: return self.is_cached(tile) def _set_metadata(self, response, tile): if 'LastModified' in response: tile.timestamp = calendar.timegm(response['LastModified'].timetuple()) if 'ContentLength' in response: tile.size = response['ContentLength'] def is_cached(self, tile): if tile.is_missing(): key = self.tile_key(tile) try: r = self.conn().head_object(Bucket=self.bucket_name, Key=key) self._set_metadata(r, tile) except botocore.exceptions.ClientError as e: if e.response['Error']['Code'] in ('404', 'NoSuchKey'): return False raise return True def load_tiles(self, tiles, with_metadata=True): p = async.Pool(min(4, len(tiles))) return all(p.map(self.load_tile, tiles)) def load_tile(self, tile, with_metadata=True): if not tile.is_missing(): return True key = self.tile_key(tile) log.debug('S3:load_tile, key: %s' % key) try: r = self.conn().get_object(Bucket=self.bucket_name, Key=key) self._set_metadata(r, tile) tile.source = ImageSource(r['Body']) except botocore.exceptions.ClientError as e: error = e.response.get('Errors', e.response)['Error'] # moto get_object can return Error wrapped in Errors... if error['Code'] in ('404', 'NoSuchKey'): return False raise return True def remove_tile(self, tile): key = self.tile_key(tile) log.debug('remove_tile, key: %s' % key) self.conn().delete_object(Bucket=self.bucket_name, Key=key) def store_tiles(self, tiles): p = async.Pool(min(self._concurrent_writer, len(tiles))) p.map(self.store_tile, tiles) def store_tile(self, tile): if tile.stored: return key = self.tile_key(tile) log.debug('S3: store_tile, key: %s' % key) extra_args = {} if self.file_ext in ('jpeg', 'png'): extra_args['ContentType'] = 'image/' + self.file_ext with tile_buffer(tile) as buf: self.conn().upload_fileobj( NopCloser(buf), # upload_fileobj closes buf, wrap in NopCloser self.bucket_name, key, ExtraArgs=extra_args) class NopCloser(object): def __init__(self, wrapped): self.wrapped = wrapped def close(self): pass def __getattr__(self, name): return getattr(self.wrapped, name) mapproxy-1.11.0/mapproxy/cache/tile.py000066400000000000000000000476171320454472400177260ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Tile caching (creation, caching and retrieval of tiles). .. digraph:: Schematic Call Graph ranksep = 0.1; node [shape="box", height="0", width="0"] cl [label="CacheMapLayer" href=""] tm [label="TileManager", href=""]; fc [label="FileCache", href=""]; s [label="Source", href=""]; { cl -> tm [label="load_tile_coords"]; tm -> fc [label="load\\nstore\\nis_cached"]; tm -> s [label="get_map"] } """ from functools import partial from contextlib import contextmanager from mapproxy.grid import MetaGrid from mapproxy.image.merge import merge_images from mapproxy.image.tile import TileSplitter from mapproxy.layer import MapQuery, BlankImage from mapproxy.util import async from mapproxy.util.py import reraise class TileManager(object): """ Manages tiles for a single grid. Loads tiles from the cache, creates new tiles from sources and stores them into the cache, or removes tiles. :param pre_store_filter: a list with filter. each filter will be called with a tile before it will be stored to disc. the filter should return this or a new tile object. """ def __init__(self, grid, cache, sources, format, locker, image_opts=None, request_format=None, meta_buffer=None, meta_size=None, minimize_meta_requests=False, identifier=None, pre_store_filter=None, concurrent_tile_creators=1, tile_creator_class=None, bulk_meta_tiles=False, ): self.grid = grid self.cache = cache self.locker = locker self.identifier = identifier self.meta_grid = None self.format = format self.image_opts = image_opts self.request_format = request_format or format self.sources = sources self.minimize_meta_requests = minimize_meta_requests self._expire_timestamp = None self.pre_store_filter = pre_store_filter or [] self.concurrent_tile_creators = concurrent_tile_creators self.tile_creator_class = tile_creator_class or TileCreator if meta_buffer or (meta_size and not meta_size == [1, 1]): if all(source.supports_meta_tiles for source in sources): self.meta_grid = MetaGrid(grid, meta_size=meta_size, meta_buffer=meta_buffer) elif any(source.supports_meta_tiles for source in sources): raise ValueError('meta tiling configured but not supported by all sources') elif meta_size and not meta_size == [1, 1] and bulk_meta_tiles: # meta tiles configured but all sources are tiled # use bulk_meta_tile mode that download tiles in parallel self.meta_grid = MetaGrid(grid, meta_size=meta_size, meta_buffer=0) self.tile_creator_class = partial(self.tile_creator_class, bulk_meta_tiles=True) @contextmanager def session(self): """ Context manager for access to the cache. Cleans up after usage for connection based caches. >>> with tile_manager.session(): #doctest: +SKIP ... tile_manager.load_tile_coords(tile_coords) """ yield self.cleanup() def cleanup(self): if hasattr(self.cache, 'cleanup'): self.cache.cleanup() def load_tile_coord(self, tile_coord, dimensions=None, with_metadata=False): tile = Tile(tile_coord) self.cache.load_tile(tile, with_metadata) if tile.coord is not None and not self.is_cached(tile, dimensions=dimensions): # missing or staled creator = self.creator(dimensions=dimensions) created_tiles = creator.create_tiles([tile]) for created_tile in created_tiles: if created_tile.coord == tile_coord: return created_tile return tile def load_tile_coords(self, tile_coords, dimensions=None, with_metadata=False): tiles = TileCollection(tile_coords) uncached_tiles = [] # load all in batch self.cache.load_tiles(tiles, with_metadata) for tile in tiles: if tile.coord is not None and not self.is_cached(tile, dimensions=dimensions): # missing or staled uncached_tiles.append(tile) if uncached_tiles: creator = self.creator(dimensions=dimensions) created_tiles = creator.create_tiles(uncached_tiles) for created_tile in created_tiles: if created_tile.coord in tiles: tiles[created_tile.coord].source = created_tile.source return tiles def remove_tile_coords(self, tile_coords, dimensions=None): tiles = TileCollection(tile_coords) self.cache.remove_tiles(tiles) def creator(self, dimensions=None): return self.tile_creator_class(self, dimensions=dimensions) def lock(self, tile): if self.meta_grid: tile = Tile(self.meta_grid.main_tile(tile.coord)) return self.locker.lock(tile) def is_cached(self, tile, dimensions=None): """ Return True if the tile is cached. """ if isinstance(tile, tuple): tile = Tile(tile) if tile.coord is None: return True cached = self.cache.is_cached(tile) max_mtime = self.expire_timestamp(tile) if cached and max_mtime is not None: self.cache.load_tile_metadata(tile) stale = tile.timestamp < max_mtime if stale: cached = False return cached def is_stale(self, tile, dimensions=None): """ Return True if tile exists _and_ is expired. """ if isinstance(tile, tuple): tile = Tile(tile) if self.cache.is_cached(tile): # tile exists if not self.is_cached(tile): # expired return True return False return False def expire_timestamp(self, tile=None): """ Return the timestamp until which a tile should be accepted as up-to-date, or ``None`` if the tiles should not expire. :note: Returns _expire_timestamp by default. """ return self._expire_timestamp def apply_tile_filter(self, tile): """ Apply all `pre_store_filter` to this tile. Returns filtered tile. """ if tile.stored: return tile for img_filter in self.pre_store_filter: tile = img_filter(tile) return tile class TileCreator(object): def __init__(self, tile_mgr, dimensions=None, image_merger=None, bulk_meta_tiles=False): self.cache = tile_mgr.cache self.sources = tile_mgr.sources self.grid = tile_mgr.grid self.meta_grid = tile_mgr.meta_grid self.bulk_meta_tiles = bulk_meta_tiles self.tile_mgr = tile_mgr self.dimensions = dimensions self.image_merger = image_merger def is_cached(self, tile): """ Return True if the tile is cached. """ return self.tile_mgr.is_cached(tile) def create_tiles(self, tiles): if not self.meta_grid: created_tiles = self._create_single_tiles(tiles) elif self.tile_mgr.minimize_meta_requests and len(tiles) > 1: # use minimal requests only for mulitple tile requests (ie not for TMS) meta_tile = self.meta_grid.minimal_meta_tile([t.coord for t in tiles]) created_tiles = self._create_meta_tile(meta_tile) else: meta_tiles = [] meta_bboxes = set() for tile in tiles: meta_tile = self.meta_grid.meta_tile(tile.coord) if meta_tile.bbox not in meta_bboxes: meta_tiles.append(meta_tile) meta_bboxes.add(meta_tile.bbox) created_tiles = self._create_meta_tiles(meta_tiles) return created_tiles def _create_single_tiles(self, tiles): if self.tile_mgr.concurrent_tile_creators > 1 and len(tiles) > 1: return self._create_threaded(self._create_single_tile, tiles) created_tiles = [] for tile in tiles: created_tiles.extend(self._create_single_tile(tile)) return created_tiles def _create_threaded(self, create_func, tiles): result = [] async_pool = async.Pool(self.tile_mgr.concurrent_tile_creators) for new_tiles in async_pool.imap(create_func, tiles): result.extend(new_tiles) return result def _create_single_tile(self, tile): tile_bbox = self.grid.tile_bbox(tile.coord) query = MapQuery(tile_bbox, self.grid.tile_size, self.grid.srs, self.tile_mgr.request_format, dimensions=self.dimensions) with self.tile_mgr.lock(tile): if not self.is_cached(tile): source = self._query_sources(query) if not source: return [] if self.tile_mgr.image_opts != source.image_opts: # call as_buffer to force conversion into cache format source.as_buffer(self.tile_mgr.image_opts) source.image_opts = self.tile_mgr.image_opts tile.source = source tile.cacheable = source.cacheable tile = self.tile_mgr.apply_tile_filter(tile) if source.cacheable: self.cache.store_tile(tile) else: self.cache.load_tile(tile) return [tile] def _query_sources(self, query): """ Query all sources and return the results as a single ImageSource. Multiple sources will be merged into a single image. """ # directly return get_map without merge if ... if (len(self.sources) == 1 and not self.image_merger and # no special image_merger (like BandMerger) not (self.sources[0].coverage and # no clipping coverage self.sources[0].coverage.clip and self.sources[0].coverage.intersects(query.bbox, query.srs)) ): try: return self.sources[0].get_map(query) except BlankImage: return None def get_map_from_source(source): try: img = source.get_map(query) except BlankImage: return None, None else: return (img, source.coverage) layers = [] for layer in async.imap(get_map_from_source, self.sources): if layer[0] is not None: layers.append(layer) return merge_images(layers, size=query.size, bbox=query.bbox, bbox_srs=query.srs, image_opts=self.tile_mgr.image_opts, merger=self.image_merger) def _create_meta_tiles(self, meta_tiles): if self.bulk_meta_tiles: created_tiles = [] for meta_tile in meta_tiles: created_tiles.extend(self._create_bulk_meta_tile(meta_tile)) return created_tiles if self.tile_mgr.concurrent_tile_creators > 1 and len(meta_tiles) > 1: return self._create_threaded(self._create_meta_tile, meta_tiles) created_tiles = [] for meta_tile in meta_tiles: created_tiles.extend(self._create_meta_tile(meta_tile)) return created_tiles def _create_meta_tile(self, meta_tile): """ _create_meta_tile queries a single meta tile and splits it into tiles. """ tile_size = self.grid.tile_size query = MapQuery(meta_tile.bbox, meta_tile.size, self.grid.srs, self.tile_mgr.request_format, dimensions=self.dimensions) main_tile = Tile(meta_tile.main_tile_coord) with self.tile_mgr.lock(main_tile): if not all(self.is_cached(t) for t in meta_tile.tiles if t is not None): meta_tile_image = self._query_sources(query) if not meta_tile_image: return [] splitted_tiles = split_meta_tiles(meta_tile_image, meta_tile.tile_patterns, tile_size, self.tile_mgr.image_opts) splitted_tiles = [self.tile_mgr.apply_tile_filter(t) for t in splitted_tiles] if meta_tile_image.cacheable: self.cache.store_tiles(splitted_tiles) return splitted_tiles # else tiles = [Tile(coord) for coord in meta_tile.tiles] self.cache.load_tiles(tiles) return tiles def _create_bulk_meta_tile(self, meta_tile): """ _create_bulk_meta_tile queries each tile of the meta tile in parallel (using concurrent_tile_creators). """ tile_size = self.grid.tile_size main_tile = Tile(meta_tile.main_tile_coord) with self.tile_mgr.lock(main_tile): if not all(self.is_cached(t) for t in meta_tile.tiles if t is not None): async_pool = async.Pool(self.tile_mgr.concurrent_tile_creators) def query_tile(coord): try: query = MapQuery(self.grid.tile_bbox(coord), tile_size, self.grid.srs, self.tile_mgr.request_format, dimensions=self.dimensions) tile_image = self._query_sources(query) if tile_image is None: return None if self.tile_mgr.image_opts != tile_image.image_opts: # call as_buffer to force conversion into cache format tile_image.as_buffer(self.tile_mgr.image_opts) tile = Tile(coord, cacheable=tile_image.cacheable) tile.source = tile_image tile = self.tile_mgr.apply_tile_filter(tile) except BlankImage: return None else: return tile tiles = [] for tile_task in async_pool.imap(query_tile, [t for t in meta_tile.tiles if t is not None], use_result_objects=True, ): if tile_task.exception is None: tile = tile_task.result if tile is not None: tiles.append(tile) else: ex = tile_task.exception async_pool.shutdown(True) reraise(ex) self.cache.store_tiles([t for t in tiles if t.cacheable]) return tiles # else tiles = [Tile(coord) for coord in meta_tile.tiles] self.cache.load_tiles(tiles) return tiles class Tile(object): """ Internal data object for all tiles. Stores the tile-``coord`` and the tile data. :ivar source: the data of this tile :type source: ImageSource """ def __init__(self, coord, source=None, cacheable=True): self.coord = coord self.source = source self.location = None self.stored = False self._cacheable = cacheable self.size = None self.timestamp = None def _cacheable_get(self): return CacheInfo(cacheable=self._cacheable, timestamp=self.timestamp, size=self.size) def _cacheable_set(self, cacheable): if isinstance(cacheable, bool): self._cacheable = cacheable else: # assume cacheable is CacheInfo self._cacheable = cacheable.cacheable self.timestamp = cacheable.timestamp self.size = cacheable.size cacheable = property(_cacheable_get, _cacheable_set) def source_buffer(self, *args, **kw): if self.source is not None: return self.source.as_buffer(*args, **kw) else: return None def source_image(self, *args, **kw): if self.source is not None: return self.source.as_image(*args, **kw) else: return None def is_missing(self): """ Returns ``True`` when the tile has no ``data``, except when the ``coord`` is ``None``. It doesn't check if the tile exists. >>> Tile((1, 2, 3)).is_missing() True >>> Tile((1, 2, 3), './tmp/foo').is_missing() False >>> Tile(None).is_missing() False """ if self.coord is None: return False return self.source is None def __eq__(self, other): """ >>> Tile((0, 0, 1)) == Tile((0, 0, 1)) True >>> Tile((0, 0, 1)) == Tile((1, 0, 1)) False >>> Tile((0, 0, 1)) == None False """ if isinstance(other, Tile): return (self.coord == other.coord and self.source == other.source) else: return NotImplemented def __ne__(self, other): """ >>> Tile((0, 0, 1)) != Tile((0, 0, 1)) False >>> Tile((0, 0, 1)) != Tile((1, 0, 1)) True >>> Tile((0, 0, 1)) != None True """ equal_result = self.__eq__(other) if equal_result is NotImplemented: return NotImplemented else: return not equal_result def __repr__(self): return 'Tile(%r, source=%r)' % (self.coord, self.source) class CacheInfo(object): def __init__(self, cacheable=True, timestamp=None, size=None): self.cacheable = cacheable self.timestamp = timestamp self.size = size def __bool__(self): return self.cacheable # PY2 compat __nonzero__ = __bool__ class TileCollection(object): def __init__(self, tile_coords): self.tiles = [Tile(coord) for coord in tile_coords] self.tiles_dict = {} for tile in self.tiles: self.tiles_dict[tile.coord] = tile def __getitem__(self, idx_or_coord): if isinstance(idx_or_coord, int): return self.tiles[idx_or_coord] if idx_or_coord in self.tiles_dict: return self.tiles_dict[idx_or_coord] return Tile(idx_or_coord) def __contains__(self, tile_or_coord): if isinstance(tile_or_coord, tuple): return tile_or_coord in self.tiles_dict if hasattr(tile_or_coord, 'coord'): return tile_or_coord.coord in self.tiles_dict return False def __len__(self): return len(self.tiles) def __iter__(self): return iter(self.tiles) @property def empty(self): """ Returns True if no tile in this collection contains a source. """ return all((t.source is None for t in self.tiles)) def __repr__(self): return 'TileCollection(%r)' % self.tiles def split_meta_tiles(meta_tile, tiles, tile_size, image_opts): try: # TODO png8 # if not self.transparent and format == 'png': # format = 'png8' splitter = TileSplitter(meta_tile, image_opts) except IOError: # TODO raise split_tiles = [] for tile in tiles: tile_coord, crop_coord = tile if tile_coord is None: continue data = splitter.get_tile(crop_coord, tile_size) new_tile = Tile(tile_coord, cacheable=meta_tile.cacheable) new_tile.source = data split_tiles.append(new_tile) return split_tiles mapproxy-1.11.0/mapproxy/client/000077500000000000000000000000001320454472400166135ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/client/__init__.py000066400000000000000000000000001320454472400207120ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/client/arcgis.py000066400000000000000000000057651320454472400204520ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.client.http import HTTPClient from mapproxy.client.wms import WMSInfoClient from mapproxy.srs import SRS from mapproxy.featureinfo import create_featureinfo_doc class ArcGISClient(object): def __init__(self, request_template, http_client=None): self.request_template = request_template self.http_client = http_client def retrieve(self, query, format): url = self._query_url(query, format) resp = self.http_client.open(url) return resp def _query_url(self, query, format): req = self.request_template.copy() req.params.format = format req.params.bbox = query.bbox req.params.size = query.size req.params.bboxSR = query.srs req.params.imageSR = query.srs req.params.transparent = query.transparent return req.complete_url def combined_client(self, other, query): return class ArcGISInfoClient(WMSInfoClient): def __init__(self, request_template, supported_srs=None, http_client=None, return_geometries=False, tolerance=5, ): self.request_template = request_template self.http_client = http_client or HTTPClient() if not supported_srs and self.request_template.params.srs is not None: supported_srs = [SRS(self.request_template.params.srs)] self.supported_srs = supported_srs or [] self.return_geometries = return_geometries self.tolerance = tolerance def get_info(self, query): if self.supported_srs and query.srs not in self.supported_srs: query = self._get_transformed_query(query) resp = self._retrieve(query) # always use query.info_format and not content-type from response (even esri example server aleays return text/plain) return create_featureinfo_doc(resp.read(), query.info_format) def _query_url(self, query): req = self.request_template.copy() req.params.bbox = query.bbox req.params.size = query.size req.params.pos = query.pos req.params.srs = query.srs.srs_code if query.info_format.startswith('text/html'): req.params['f'] = 'html' else: req.params['f'] = 'json' req.params['tolerance'] = self.tolerance req.params['returnGeometry'] = str(self.return_geometries).lower() return req.complete_url mapproxy-1.11.0/mapproxy/client/cgi.py000066400000000000000000000111001320454472400177200ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ HTTP client that directly calls CGI executable. """ import errno import os import re import time from mapproxy.source import SourceError from mapproxy.image import ImageSource from mapproxy.client.http import HTTPClientError from mapproxy.client.log import log_request from mapproxy.util.async import import_module from mapproxy.compat.modules import urlparse from mapproxy.compat import BytesIO subprocess = import_module('subprocess') def split_cgi_response(data): headers = [] prev_n = 0 while True: next_n = data.find(b'\n', prev_n) if next_n < 0: break next_line_begin = data[next_n+1:next_n+3] headers.append(data[prev_n:next_n].rstrip(b'\r')) if next_line_begin[0:1] == b'\n': return headers_dict(headers), data[next_n+2:] elif next_line_begin == b'\r\n': return headers_dict(headers), data[next_n+3:] prev_n = next_n+1 return {}, data def headers_dict(header_lines): headers = {} for line in header_lines: if b':' in line: key, value = line.split(b':', 1) value = value.strip() else: key = line value = None key = key.decode('latin-1') key = key[0].upper() + key[1:].lower() if value: value = value.decode('latin-1') headers[key] = value return headers class IOwithHeaders(object): def __init__(self, io, headers): self.io = io self.headers = headers def __getattr__(self, name): return getattr(self.io, name) class CGIClient(object): def __init__(self, script, no_headers=False, working_directory=None): self.script = script self.working_directory = working_directory self.no_headers = no_headers def open(self, url, data=None): assert data is None, 'POST requests not supported by CGIClient' parsed_url = urlparse.urlparse(url) environ = os.environ.copy() environ.update({ 'QUERY_STRING': parsed_url.query, 'REQUEST_METHOD': 'GET', 'GATEWAY_INTERFACE': 'CGI/1.1', 'SERVER_ADDR': '127.0.0.1', 'SERVER_NAME': 'localhost', 'SERVER_PROTOCOL': 'HTTP/1.0', 'SERVER_SOFTWARE': 'MapProxy', }) start_time = time.time() try: p = subprocess.Popen([self.script], env=environ, stdout=subprocess.PIPE, cwd=self.working_directory or os.path.dirname(self.script) ) except OSError as ex: if ex.errno == errno.ENOENT: raise SourceError('CGI script not found (%s)' % (self.script,)) elif ex.errno == errno.EACCES: raise SourceError('No permission for CGI script (%s)' % (self.script,)) else: raise stdout = p.communicate()[0] ret = p.wait() if ret != 0: raise HTTPClientError('Error during CGI call (exit code: %d)' % (ret, )) if self.no_headers: content = stdout headers = dict() else: headers, content = split_cgi_response(stdout) status_match = re.match('(\d\d\d) ', headers.get('Status', '')) if status_match: status_code = status_match.group(1) else: status_code = '-' size = len(content) content = IOwithHeaders(BytesIO(content), headers) log_request('%s:%s' % (self.script, parsed_url.query), status_code, size=size, method='CGI', duration=time.time()-start_time) return content def open_image(self, url, data=None): resp = self.open(url, data=data) if 'Content-type' in resp.headers: if not resp.headers['Content-type'].lower().startswith('image'): raise HTTPClientError('response is not an image: (%s)' % (resp.read())) return ImageSource(resp) mapproxy-1.11.0/mapproxy/client/http.py000066400000000000000000000237141320454472400201530ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010-2017 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Tile retrieval (WMS, TMS, etc.). """ import sys import time import warnings from mapproxy.version import version from mapproxy.image import ImageSource from mapproxy.util.py import reraise_exception from mapproxy.client.log import log_request from mapproxy.compat import PY2 from mapproxy.compat.modules import urlparse if PY2: import urllib2 from urllib2 import URLError, HTTPError import httplib else: from urllib import request as urllib2 from urllib.error import URLError, HTTPError from http import client as httplib import socket import ssl supports_ssl_default_context = False if hasattr(ssl, 'create_default_context'): # Python >=2.7.9 and >=3.4.0 supports_ssl_default_context = True class HTTPClientError(Exception): def __init__(self, arg, response_code=None): Exception.__init__(self, arg) self.response_code = response_code def build_https_handler(ssl_ca_certs, insecure): if supports_ssl_default_context: # python >=2.7.9 and >=3.4 supports ssl context in # HTTPSHandler use this if insecure: ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23) ctx.verify_mode = ssl.CERT_NONE elif ssl_ca_certs: ctx = ssl.create_default_context(cafile=ssl_ca_certs) else: ctx = ssl.create_default_context() return urllib2.HTTPSHandler(context=ctx) else: if insecure: return None else: connection_class = verified_https_connection_with_ca_certs( ssl_ca_certs) return VerifiedHTTPSHandler(connection_class=connection_class) class VerifiedHTTPSConnection(httplib.HTTPSConnection): def __init__(self, *args, **kw): self._ca_certs = kw.pop('ca_certs', None) httplib.HTTPSConnection.__init__(self, *args, **kw) def connect(self): # overrides the version in httplib so that we do # certificate verification if hasattr(socket, 'create_connection') and hasattr(self, 'source_address'): sock = socket.create_connection((self.host, self.port), self.timeout, self.source_address) else: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((self.host, self.port)) if hasattr(self, '_tunnel_host') and self._tunnel_host: # for Python >= 2.6 with proxy support self.sock = sock self._tunnel() # wrap the socket using verification with the root # certs in self.ca_certs_path self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, cert_reqs=ssl.CERT_REQUIRED, ca_certs=self._ca_certs) def verified_https_connection_with_ca_certs(ca_certs): """ Creates VerifiedHTTPSConnection classes with given ca_certs file. """ def wrapper(*args, **kw): kw['ca_certs'] = ca_certs return VerifiedHTTPSConnection(*args, **kw) return wrapper class VerifiedHTTPSHandler(urllib2.HTTPSHandler): def __init__(self, connection_class=VerifiedHTTPSConnection): self.specialized_conn_class = connection_class urllib2.HTTPSHandler.__init__(self) def https_open(self, req): return self.do_open(self.specialized_conn_class, req) class _URLOpenerCache(object): """ Creates custom URLOpener with BasicAuth and HTTPS handler. Caches and reuses opener if possible (i.e. if they share the same ssl_ca_certs). """ def __init__(self): self._opener = {} def __call__(self, ssl_ca_certs, url, username, password, insecure=False): cache_key = (ssl_ca_certs, insecure) if cache_key not in self._opener: handlers = [] https_handler = build_https_handler(ssl_ca_certs, insecure) if https_handler: handlers.append(https_handler) passman = urllib2.HTTPPasswordMgrWithDefaultRealm() authhandler = urllib2.HTTPBasicAuthHandler(passman) handlers.append(authhandler) authhandler = urllib2.HTTPDigestAuthHandler(passman) handlers.append(authhandler) opener = urllib2.build_opener(*handlers) opener.addheaders = [('User-agent', 'MapProxy-%s' % (version,))] self._opener[cache_key] = (opener, passman) else: opener, passman = self._opener[cache_key] if url is not None and username is not None and password is not None: passman.add_password(None, url, username, password) return opener create_url_opener = _URLOpenerCache() class HTTPClient(object): def __init__(self, url=None, username=None, password=None, insecure=False, ssl_ca_certs=None, timeout=None, headers=None): self._timeout = timeout if url and url.startswith('https'): if insecure: ssl_ca_certs = None elif ssl_ca_certs is None and not supports_ssl_default_context: raise HTTPClientError('No ca_certs file set (http.ssl_ca_certs). ' 'Set file or disable verification with http.ssl_no_cert_checks option.') self.opener = create_url_opener(ssl_ca_certs, url, username, password, insecure=insecure) self.header_list = headers.items() if headers else [] def open(self, url, data=None): code = None result = None try: req = urllib2.Request(url, data=data) except ValueError as e: reraise_exception(HTTPClientError('URL not correct "%s": %s' % (url, e.args[0])), sys.exc_info()) for key, value in self.header_list: req.add_header(key, value) try: start_time = time.time() if self._timeout is not None: result = self.opener.open(req, timeout=self._timeout) else: result = self.opener.open(req) except HTTPError as e: code = e.code reraise_exception(HTTPClientError('HTTP Error "%s": %d' % (url, e.code), response_code=code), sys.exc_info()) except URLError as e: if isinstance(e.reason, ssl.SSLError): e = HTTPClientError('Could not verify connection to URL "%s": %s' % (url, e.reason.args[1])) reraise_exception(e, sys.exc_info()) try: reason = e.reason.args[1] except (AttributeError, IndexError): reason = e.reason reraise_exception(HTTPClientError('No response from URL "%s": %s' % (url, reason)), sys.exc_info()) except ValueError as e: reraise_exception(HTTPClientError('URL not correct "%s": %s' % (url, e.args[0])), sys.exc_info()) except Exception as e: reraise_exception(HTTPClientError('Internal HTTP error "%s": %r' % (url, e)), sys.exc_info()) else: code = getattr(result, 'code', 200) if code == 204: raise HTTPClientError('HTTP Error "204 No Content"', response_code=204) return result finally: log_request(url, code, result, duration=time.time()-start_time, method=req.get_method()) def open_image(self, url, data=None): resp = self.open(url, data=data) if 'content-type' in resp.headers: if not resp.headers['content-type'].lower().startswith('image'): raise HTTPClientError('response is not an image: (%s)' % (resp.read())) return ImageSource(resp) def auth_data_from_url(url): """ >>> auth_data_from_url('http://localhost/bar') ('http://localhost/bar', (None, None)) >>> auth_data_from_url('http://bar@localhost/bar') ('http://localhost/bar', ('bar', None)) >>> auth_data_from_url('http://bar:baz@localhost/bar') ('http://localhost/bar', ('bar', 'baz')) >>> auth_data_from_url('http://bar:b:az@@localhost/bar') ('http://localhost/bar', ('bar', 'b:az@')) >>> auth_data_from_url('http://bar foo; foo@bar:b:az@@localhost/bar') ('http://localhost/bar', ('bar foo; foo@bar', 'b:az@')) >>> auth_data_from_url('https://bar:foo#;%$@localhost/bar') ('https://localhost/bar', ('bar', 'foo#;%$')) """ username = password = None if '@' in url: head, url = url.rsplit('@', 1) schema, auth_data = head.split('//', 1) url = schema + '//' + url if ':' in auth_data: username, password = auth_data.split(':', 1) else: username = auth_data return url, (username, password) _http_client = HTTPClient() def open_url(url): return _http_client.open(url) retrieve_url = open_url def retrieve_image(url, client=None): """ Retrive an image from `url`. :return: the image as a file object (with url .header and .info) :raise HTTPClientError: if response content-type doesn't start with image """ resp = open_url(url) if not resp.headers['content-type'].startswith('image'): raise HTTPClientError('response is not an image: (%s)' % (resp.read())) return ImageSource(resp) mapproxy-1.11.0/mapproxy/client/log.py000066400000000000000000000023241320454472400177470ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging logger = logging.getLogger('mapproxy.source.request') def log_request(url, status, result=None, size=None, method='GET', duration=None): if not logger.isEnabledFor(logging.INFO): return if not size and result is not None: size = result.headers.get('Content-length') if size: size = '%.1f' % (int(size)/1024.0, ) else: size = '-' if not status: status = '-' duration = '%d' % (duration*1000) if duration else '-' logger.info('%s %s %s %s %s', method, url.replace(' ', ''), status, size, duration) mapproxy-1.11.0/mapproxy/client/tile.py000066400000000000000000000130441320454472400201240ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.client.http import retrieve_image class TMSClient(object): def __init__(self, url, format='png', http_client=None): self.url = url self.http_client = http_client self.format = format def get_tile(self, tile_coord, format=None): x, y, z = tile_coord url = '%s/%d/%d/%d.%s' % (self.url, z, x, y, format or self.format) if self.http_client: return self.http_client.open_image(url) else: return retrieve_image(url) def __repr__(self): return '%s(%r, %r)' % (self.__class__.__name__, self.url, self.format) class TileClient(object): def __init__(self, url_template, http_client=None, grid=None): self.url_template = url_template self.http_client = http_client self.grid = grid def get_tile(self, tile_coord, format=None): url = self.url_template.substitute(tile_coord, format, self.grid) if self.http_client: return self.http_client.open_image(url) else: return retrieve_image(url) def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.url_template) class TileURLTemplate(object): """ >>> t = TileURLTemplate('http://foo/tiles/%(z)s/%(x)d/%(y)s.png') >>> t.substitute((7, 4, 3)) 'http://foo/tiles/3/7/4.png' >>> t = TileURLTemplate('http://foo/tiles/%(z)s/%(x)d/%(y)s.png') >>> t.substitute((7, 4, 3)) 'http://foo/tiles/3/7/4.png' >>> t = TileURLTemplate('http://foo/tiles/%(tc_path)s.png') >>> t.substitute((7, 4, 3)) 'http://foo/tiles/03/000/000/007/000/000/004.png' >>> t = TileURLTemplate('http://foo/tms/1.0.0/%(tms_path)s.%(format)s') >>> t.substitute((7, 4, 3)) 'http://foo/tms/1.0.0/3/7/4.png' >>> t = TileURLTemplate('http://foo/tms/1.0.0/lyr/%(tms_path)s.%(format)s') >>> t.substitute((7, 4, 3), 'jpeg') 'http://foo/tms/1.0.0/lyr/3/7/4.jpeg' """ def __init__(self, template, format='png'): self.template= template self.format = format self.with_quadkey = True if '%(quadkey)' in template else False self.with_tc_path = True if '%(tc_path)' in template else False self.with_tms_path = True if '%(tms_path)' in template else False self.with_arcgiscache_path = True if '%(arcgiscache_path)' in template else False self.with_bbox = True if '%(bbox)' in template else False def substitute(self, tile_coord, format=None, grid=None): x, y, z = tile_coord data = dict(x=x, y=y, z=z) data['format'] = format or self.format if self.with_quadkey: data['quadkey'] = quadkey(tile_coord) if self.with_tc_path: data['tc_path'] = tilecache_path(tile_coord) if self.with_tms_path: data['tms_path'] = tms_path(tile_coord) if self.with_arcgiscache_path: data['arcgiscache_path'] = arcgiscache_path(tile_coord) if self.with_bbox: data['bbox'] = bbox(tile_coord, grid) return self.template % data def __repr__(self): return '%s(%r, format=%r)' % ( self.__class__.__name__, self.template, self.format) def tilecache_path(tile_coord): """ >>> tilecache_path((1234567, 87654321, 9)) '09/001/234/567/087/654/321' """ x, y, z = tile_coord parts = ("%02d" % z, "%03d" % int(x / 1000000), "%03d" % (int(x / 1000) % 1000), "%03d" % (int(x) % 1000), "%03d" % int(y / 1000000), "%03d" % (int(y / 1000) % 1000), "%03d" % (int(y) % 1000)) return '/'.join(parts) def quadkey(tile_coord): """ >>> quadkey((0, 0, 1)) '0' >>> quadkey((1, 0, 1)) '1' >>> quadkey((1, 2, 2)) '21' """ x, y, z = tile_coord quadKey = "" for i in range(z,0,-1): digit = 0 mask = 1 << (i-1) if (x & mask) != 0: digit += 1 if (y & mask) != 0: digit += 2 quadKey += str(digit) return quadKey def tms_path(tile_coord): """ >>> tms_path((1234567, 87654321, 9)) '9/1234567/87654321' """ return '%d/%d/%d' % (tile_coord[2], tile_coord[0], tile_coord[1]) def arcgiscache_path(tile_coord): """ >>> arcgiscache_path((1234567, 87654321, 9)) 'L09/R05397fb1/C0012d687' """ return 'L%02d/R%08x/C%08x' % (tile_coord[2], tile_coord[1], tile_coord[0]) def bbox(tile_coord, grid): """ >>> from mapproxy.grid import tile_grid >>> grid = tile_grid(4326, bbox=(0, -15, 10, -5)) >>> bbox((0, 0, 0), grid) '0.00000000,-15.00000000,10.00000000,-5.00000000' >>> bbox((0, 0, 1), grid) '0.00000000,-15.00000000,5.00000000,-10.00000000' >>> grid = tile_grid(4326, bbox=(0, -15, 10, -5), origin='nw') >>> bbox((0, 0, 1), grid) '0.00000000,-10.00000000,5.00000000,-5.00000000' """ return '%.8f,%.8f,%.8f,%.8f' % grid.tile_bbox(tile_coord) mapproxy-1.11.0/mapproxy/client/wms.py000066400000000000000000000213131320454472400177730ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ WMS clients for maps and information. """ from mapproxy.compat import text_type from mapproxy.request.base import split_mime_type from mapproxy.layer import InfoQuery from mapproxy.source import SourceError from mapproxy.client.http import HTTPClient from mapproxy.srs import make_lin_transf, SRS from mapproxy.image import ImageSource from mapproxy.image.opts import ImageOptions from mapproxy.featureinfo import create_featureinfo_doc import logging log = logging.getLogger('mapproxy.source.wms') class WMSClient(object): def __init__(self, request_template, http_client=None, http_method=None, lock=None, fwd_req_params=None): self.request_template = request_template self.http_client = http_client or HTTPClient() self.http_method = http_method self.lock = lock self.fwd_req_params = fwd_req_params or set() def retrieve(self, query, format): if self.http_method == 'POST': request_method = 'POST' elif self.http_method == 'GET': request_method = 'GET' else: # 'AUTO' if 'sld_body' in self.request_template.params: request_method = 'POST' else: request_method = 'GET' if request_method == 'POST': url, data = self._query_data(query, format) if isinstance(data, text_type): data = data.encode('utf-8') else: url = self._query_url(query, format) data = None if self.lock: with self.lock(): resp = self.http_client.open(url, data=data) else: resp = self.http_client.open(url, data=data) self._check_resp(resp, url) return resp def _check_resp(self, resp, url): if not resp.headers.get('Content-type', 'image/').startswith('image/'): # log response depending on content-type if resp.headers['Content-type'].startswith(('text/', 'application/vnd.ogc')): log_size = 8000 # larger xml exception else: log_size = 100 # image? data = resp.read(log_size) if len(data) == log_size: data += '... truncated' log.warn("no image returned from source WMS: %s, response was: %s" % (url, data)) raise SourceError('no image returned from source WMS: %s' % (url, )) def _query_url(self, query, format): return self._query_req(query, format).complete_url def _query_data(self, query, format): req = self._query_req(query, format) return req.url.rstrip('?'), req.query_string def _query_req(self, query, format): req = self.request_template.copy() req.params.bbox = query.bbox req.params.size = query.size req.params.srs = query.srs.srs_code req.params.format = format # also forward dimension request params if available in the query req.params.update(query.dimensions_for_params(self.fwd_req_params)) return req def combined_client(self, other, query): """ Return a new WMSClient that combines this request with the `other`. Returns ``None`` if the clients are not combinable (e.g. different URLs). """ if self.request_template.url != other.request_template.url: return None new_req = self.request_template.copy() new_req.params.layers = new_req.params.layers + other.request_template.params.layers return WMSClient(new_req, http_client=self.http_client, http_method=self.http_method, fwd_req_params=self.fwd_req_params) class WMSInfoClient(object): def __init__(self, request_template, supported_srs=None, http_client=None): self.request_template = request_template self.http_client = http_client or HTTPClient() if not supported_srs and self.request_template.params.srs is not None: supported_srs = [SRS(self.request_template.params.srs)] self.supported_srs = supported_srs or [] def get_info(self, query): if self.supported_srs and query.srs not in self.supported_srs: query = self._get_transformed_query(query) resp = self._retrieve(query) info_format = resp.headers.get('Content-type', None) if not info_format: info_format = query.info_format return create_featureinfo_doc(resp.read(), info_format) def _get_transformed_query(self, query): """ Handle FI requests for unsupported SRS. """ req_srs = query.srs req_bbox = query.bbox req_coord = make_lin_transf((0, 0, query.size[0], query.size[1]), req_bbox)(query.pos) info_srs = self._best_supported_srs(req_srs) info_bbox = req_srs.transform_bbox_to(info_srs, req_bbox) # calculate new info_size to keep square pixels after transform_bbox_to info_aratio = (info_bbox[3] - info_bbox[1])/(info_bbox[2] - info_bbox[0]) info_size = query.size[0], int(info_aratio*query.size[0]) info_coord = req_srs.transform_to(info_srs, req_coord) info_pos = make_lin_transf((info_bbox), (0, 0, info_size[0], info_size[1]))(info_coord) info_pos = int(round(info_pos[0])), int(round(info_pos[1])) info_query = InfoQuery( bbox=info_bbox, size=info_size, srs=info_srs, pos=info_pos, info_format=query.info_format, feature_count=query.feature_count, ) return info_query def _best_supported_srs(self, srs): # always choose the first, distortion should not matter return self.supported_srs[0] def _retrieve(self, query): url = self._query_url(query) return self.http_client.open(url) def _query_url(self, query): req = self.request_template.copy() req.params.bbox = query.bbox req.params.size = query.size req.params.pos = query.pos if query.feature_count: req.params['feature_count'] = query.feature_count req.params['query_layers'] = req.params['layers'] if not 'info_format' in req.params and query.info_format: req.params['info_format'] = query.info_format if not req.params.format: req.params.format = query.format or 'image/png' req.params.srs = query.srs.srs_code return req.complete_url class WMSLegendClient(object): def __init__(self, request_template, http_client=None): self.request_template = request_template self.http_client = http_client or HTTPClient() def get_legend(self, query): resp = self._retrieve(query) format = split_mime_type(query.format)[1] self._check_resp(resp) return ImageSource(resp, image_opts=ImageOptions(format=format)) def _retrieve(self, query): url = self._query_url(query) return self.http_client.open(url) def _check_resp(self, resp): if not resp.headers.get('Content-type', 'image/').startswith('image/'): raise SourceError('no image returned from source WMS') def _query_url(self, query): req = self.request_template.copy() if not req.params.format: req.params.format = query.format or 'image/png' if query.scale: req.params['scale'] = query.scale return req.complete_url @property def identifier(self): return (self.request_template.url, self.request_template.params.layer) class WMSLegendURLClient(object): def __init__(self, static_url, http_client=None): self.url = static_url self.http_client = http_client or HTTPClient() def get_legend(self, query): resp = self.http_client.open(self.url) format = split_mime_type(query.format)[1] self._check_resp(resp) return ImageSource(resp, image_opts=ImageOptions(format=format)) def _check_resp(self, resp): if not resp.headers.get('Content-type', 'image/').startswith('image/'): raise SourceError('no image returned from static LegendURL') @property def identifier(self): return (self.url, None) mapproxy-1.11.0/mapproxy/compat/000077500000000000000000000000001320454472400166205ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/compat/__init__.py000066400000000000000000000015031320454472400207300ustar00rootroot00000000000000import sys PY2 = sys.version_info[0] == 2 PY3 = not PY2 if PY2: numeric_types = (float, int, long) string_type = basestring text_type = unicode # unichr = chr else: numeric_types = (float, int) string_type = str text_type = str # unichr = unichr if PY2: def iteritems(d): return d.iteritems() def iterkeys(d): return d.iterkeys() def itervalues(d): return d.itervalues() else: def iteritems(d): return d.items() def iterkeys(d): return iter(d.keys()) def itervalues(d): return d.values() if PY2: try: from cStringIO import StringIO as BytesIO except ImportError: from StringIO import StringIO as BytesIO else: from io import BytesIO if PY2: raw_input = raw_input else: raw_input = input mapproxy-1.11.0/mapproxy/compat/image.py000066400000000000000000000055051320454472400202610ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import warnings __all__ = ['Image', 'ImageColor', 'ImageDraw', 'ImageFont', 'ImagePalette', 'ImageChops', 'quantize'] try: import PIL from PIL import Image, ImageColor, ImageDraw, ImageFont, ImagePalette, ImageChops, ImageMath # prevent pyflakes warnings Image, ImageColor, ImageDraw, ImageFont, ImagePalette, ImageChops, ImageMath except ImportError: # allow MapProxy to start without PIL (for tilecache only). # issue warning and raise ImportError on first use of # a function that requires PIL warnings.warn('PIL is not available') class NoPIL(object): def __getattr__(self, name): if name.startswith('__'): raise AttributeError() raise ImportError('PIL is not available') ImageDraw = ImageFont = ImagePalette = ImageChops = NoPIL() # add some dummy stuff required on import/load time Image = NoPIL() Image.NEAREST = Image.BILINEAR = Image.BICUBIC = 1 Image.Image = NoPIL ImageColor = NoPIL() ImageColor.getrgb = lambda x: x def has_alpha_composite_support(): return hasattr(Image, 'alpha_composite') def transform_uses_center(): # transformation behavior changed with Pillow 3.4 # https://github.com/python-pillow/Pillow/commit/5232361718bae0f0ccda76bfd5b390ebf9179b18 if hasattr(PIL, 'PILLOW_VERSION'): if not PIL.PILLOW_VERSION.startswith(('1.', '2.', '3.0', '3.1', '3.2', '3.3')): return True return False def quantize_pil(img, colors=256, alpha=False, defaults=None): if hasattr(Image, 'FASTOCTREE'): if not alpha: img = img.convert('RGB') img = img.quantize(colors, Image.FASTOCTREE) else: if alpha: img.load() # split might fail if image is not loaded alpha = img.split()[3] img = img.convert('RGB').convert('P', palette=Image.ADAPTIVE, colors=colors-1) mask = Image.eval(alpha, lambda a: 255 if a <=128 else 0) img.paste(255, mask) if defaults is not None: defaults['transparency'] = 255 else: img = img.convert('RGB').convert('P', palette=Image.ADAPTIVE, colors=colors) return img quantize = quantize_pilmapproxy-1.11.0/mapproxy/compat/itertools.py000066400000000000000000000006511320454472400212200ustar00rootroot00000000000000from __future__ import absolute_import import sys PY2 = sys.version_info[0] == 2 PY3 = not PY2 if PY2: from itertools import ( izip, izip_longest, imap, islice, chain, groupby, cycle, ) else: izip = zip imap = map from itertools import ( zip_longest as izip_longest, islice, chain, groupby, cycle, ) mapproxy-1.11.0/mapproxy/compat/modules.py000066400000000000000000000002271320454472400206430ustar00rootroot00000000000000import sys PY2 = sys.version_info[0] == 2 __all__ = ['urlparse'] if PY2: import urlparse; urlparse else: from urllib import parse as urlparsemapproxy-1.11.0/mapproxy/config/000077500000000000000000000000001320454472400166025ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/config/__init__.py000066400000000000000000000016611320454472400207170ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.config.config import ( base_config, abspath, load_base_config, load_default_config, finish_base_config, Options, local_base_config, ) __all__ = ['base_config', 'abspath', 'load_base_config', 'load_default_config', 'finish_base_config', 'Options', 'local_base_config']mapproxy-1.11.0/mapproxy/config/config.py000066400000000000000000000147641320454472400204350ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ System-wide configuration. """ import os import copy import contextlib from mapproxy.util.yaml import load_yaml_file from mapproxy.util.ext.local import LocalStack from mapproxy.compat import iteritems class Options(dict): """ Dictionary with attribute style access. >>> o = Options(bar='foo') >>> o.bar 'foo' """ def __repr__(self): return '%s(%s)' % (self.__class__.__name__, dict.__repr__(self)) def __getattr__(self, name): if name in self: return self[name] else: raise AttributeError(name) __setattr__ = dict.__setitem__ def __delattr__(self, name): if name in self: del self[name] else: raise AttributeError(name) def update(self, other=None, **kw): if other is not None: if hasattr(other, 'iteritems'): it = other.iteritems() elif hasattr(other, 'items'): it = other.items() else: it = iter(other) else: it = iter(kw) for key, value in it: if key in self and isinstance(self[key], Options): self[key].update(value) else: self[key] = value def __deepcopy__(self, memo): return Options(copy.deepcopy(list(self.items()), memo)) _config = LocalStack() def base_config(): """ Returns the context-local system-wide configuration. """ config = _config.top if config is None: import warnings import sys if 'nosetests' not in sys.argv[0]: warnings.warn("calling un-configured base_config", DeprecationWarning, stacklevel=2) config = load_default_config() config.conf_base_dir = os.getcwd() finish_base_config(config) _config.push(config) return config @contextlib.contextmanager def local_base_config(conf): """ Temporarily set the global configuration (mapproxy.config.base_config). The global mapproxy.config.base_config object is thread-local and is set per-request in the MapProxyApp. Use `local_base_config` to set base_config outside of a request context (e.g. system loading or seeding). """ import mapproxy.config.config mapproxy.config.config._config.push(conf) try: yield finally: mapproxy.config.config._config.pop() def _to_options_map(mapping): if isinstance(mapping, dict): opt = Options() for key, value in iteritems(mapping): opt[key] = _to_options_map(value) return opt elif isinstance(mapping, list): return [_to_options_map(m) for m in mapping] else: return mapping def abspath(path, base_path=None): """ Convert path to absolute path. Uses ``conf_base_dir`` as base, if path is relative and ``base_path`` is not set. """ if base_path: return os.path.abspath(os.path.join(base_path, path)) return os.path.join(base_config().conf_base_dir, path) def finish_base_config(bc=None): bc = bc or base_config() if 'srs' in bc: # build union of default axis_order_xx_ and the user configured axis_order_xx default_ne = bc.srs.axis_order_ne_ default_en = bc.srs.axis_order_en_ # remove from default to allow overwrites default_ne.difference_update(set(bc.srs.axis_order_en)) default_en.difference_update(set(bc.srs.axis_order_ne)) bc.srs.axis_order_ne = default_ne.union(set(bc.srs.axis_order_ne)) bc.srs.axis_order_en = default_en.union(set(bc.srs.axis_order_en)) if 'proj_data_dir' in bc.srs: bc.srs.proj_data_dir = os.path.join(bc.conf_base_dir, bc.srs.proj_data_dir) if 'wms' in bc: bc.wms.srs = set(bc.wms.srs) if 'conf_base_dir' in bc: if 'cache' in bc: if 'base_dir' in bc.cache: bc.cache.base_dir = os.path.join(bc.conf_base_dir, bc.cache.base_dir) if 'lock_dir' in bc.cache: bc.cache.lock_dir = os.path.join(bc.conf_base_dir, bc.cache.lock_dir) def load_base_config(config_file=None, clear_existing=False): """ Load system wide base configuration. :param config_file: the file name of the mapproxy.yaml configuration. if ``None``, load the internal proxylib/default.yaml conf :param clear_existing: if ``True`` remove the existing configuration settings, else overwrite the settings. """ if config_file is None: from mapproxy.config import defaults config_dict = {} for k, v in iteritems(defaults.__dict__): if k.startswith('_'): continue config_dict[k] = v conf_base_dir = os.getcwd() load_config(base_config(), config_dict=config_dict, clear_existing=clear_existing) else: conf_base_dir = os.path.abspath(os.path.dirname(config_file)) load_config(base_config(), config_file=config_file, clear_existing=clear_existing) bc = base_config() finish_base_config(bc) bc.conf_base_dir = conf_base_dir def load_default_config(): from mapproxy.config import defaults config_dict = {} for k, v in iteritems(defaults.__dict__): if k.startswith('_'): continue config_dict[k] = v default_conf = Options() load_config(default_conf, config_dict=config_dict) return default_conf def load_config(config, config_file=None, config_dict=None, clear_existing=False): if clear_existing: for key in list(config.keys()): del config[key] if config_dict is None: config_dict = load_yaml_file(config_file) defaults = _to_options_map(config_dict) if defaults: for key, value in iteritems(defaults): if key in config and hasattr(config[key], 'update'): config[key].update(value) else: config[key] = value mapproxy-1.11.0/mapproxy/config/coverage.py000066400000000000000000000074231320454472400207550ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import re from mapproxy.srs import SRS from mapproxy.config import abspath from mapproxy.util.geom import ( load_datasource, load_ogr_datasource, load_polygons, load_expire_tiles, require_geom_support, build_multipolygon, ) from mapproxy.util.coverage import ( coverage, diff_coverage, union_coverage, intersection_coverage, ) from mapproxy.compat import string_type bbox_string_re = re.compile(r'[-+]?\d*.?\d+,[-+]?\d*.?\d+,[-+]?\d*.?\d+,[-+]?\d*.?\d+') def load_coverage(conf, base_path=None): clip = False if 'clip' in conf: clip = conf['clip'] if 'union' in conf: parts = [] for cov in conf['union']: parts.append(load_coverage(cov)) return union_coverage(parts, clip=clip) elif 'intersection' in conf: parts = [] for cov in conf['intersection']: parts.append(load_coverage(cov)) return intersection_coverage(parts, clip=clip) elif 'difference' in conf: parts = [] for cov in conf['difference']: parts.append(load_coverage(cov)) return diff_coverage(parts, clip=clip) elif 'ogr_datasource' in conf: require_geom_support() srs = conf['ogr_srs'] datasource = conf['ogr_datasource'] if not re.match(r'^\w{2,}:', datasource): # looks like a file and not PG:, MYSQL:, etc # make absolute path datasource = abspath(datasource, base_path=base_path) where = conf.get('ogr_where', None) geom = load_ogr_datasource(datasource, where) bbox, geom = build_multipolygon(geom, simplify=True) elif 'polygons' in conf: require_geom_support() srs = conf['polygons_srs'] geom = load_polygons(abspath(conf['polygons'], base_path=base_path)) bbox, geom = build_multipolygon(geom, simplify=True) elif 'bbox' in conf: srs = conf.get('bbox_srs') or conf['srs'] bbox = conf['bbox'] if isinstance(bbox, string_type): bbox = [float(x) for x in bbox.split(',')] geom = None elif 'datasource' in conf: require_geom_support() datasource = conf['datasource'] srs = conf['srs'] if isinstance(datasource, (list, tuple)): bbox = datasource geom = None elif bbox_string_re.match(datasource): bbox = [float(x) for x in datasource.split(',')] geom = None else: if not re.match(r'^\w{2,}:', datasource): # looks like a file and not PG:, MYSQL:, etc # make absolute path datasource = abspath(datasource, base_path=base_path) where = conf.get('where', None) geom = load_datasource(datasource, where) bbox, geom = build_multipolygon(geom, simplify=True) elif 'expire_tiles' in conf: require_geom_support() filename = abspath(conf['expire_tiles']) geom = load_expire_tiles(filename) _, geom = build_multipolygon(geom, simplify=False) return coverage(geom, SRS(3857)) else: return None return coverage(geom or bbox, SRS(srs), clip=clip) mapproxy-1.11.0/mapproxy/config/defaults.py000066400000000000000000000051631320454472400207700ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. server = ['wms', 'tms', 'kml'] wms = dict( image_formats = ['image/png', 'image/jpeg', 'image/gif', 'image/GeoTIFF', 'image/tiff'], srs = set(['EPSG:4326', 'EPSG:4258', 'CRS:84', 'EPSG:900913', 'EPSG:3857']), strict = False, request_parser = 'default', client_request = 'default', concurrent_layer_renderer = 1, max_output_pixels = 4000*4000, ) debug_mode = False srs = dict( # user sets axis_order_ne = set(), axis_order_en = set(), # default sets, both will be combined in config:load_base_config axis_order_ne_ = set(['EPSG:4326', 'EPSG:4258', 'EPSG:31466', 'EPSG:31467', 'EPSG:31468']), axis_order_en_ = set(['CRS:84', 'EPSG:900913', 'EPSG:25831', 'EPSG:25832', 'EPSG:25833']), ) image = dict( # nearest, bilinear, bicubic resampling_method = 'bicubic', jpeg_quality = 90, stretch_factor = 1.15, max_shrink_factor = 4.0, paletted = True, transparent_color_tolerance = 5, font_dir = None, ) # number of concurrent requests to a tile source services_conf = 'services.yaml' log_conf = 'log.ini' # directory with mapproxy/service/templates/* files template_dir = None cache = dict( base_dir = './cache_data', lock_dir = './cache_data/tile_locks', max_tile_limit = 500, concurrent_tile_creators = 2, meta_size = (4, 4), meta_buffer = 80, minimize_meta_requests = False, link_single_color_images = False, sqlite_timeout = 30, ) grid = dict( tile_size = (256, 256), ) grids = dict( GLOBAL_GEODETIC=dict( srs='EPSG:4326', origin='sw', name='GLOBAL_GEODETIC' ), GLOBAL_MERCATOR=dict( srs='EPSG:900913', origin='sw', name='GLOBAL_MERCATOR' ), GLOBAL_WEBMERCATOR=dict( srs='EPSG:3857', origin='nw', name='GLOBAL_WEBMERCATOR' ) ) tiles = dict( expires_hours = 72, ) http = dict( ssl_ca_certs = None, ssl_no_cert_checks = False, client_timeout = 60, concurrent_requests = 0, method = 'AUTO', access_control_allow_origin = '*', ) mapproxy-1.11.0/mapproxy/config/loader.py000066400000000000000000002365371320454472400204420ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010-2016 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Configuration loading and system initializing. """ from __future__ import division import os import sys import hashlib import warnings from copy import deepcopy, copy from functools import partial import logging log = logging.getLogger('mapproxy.config') from mapproxy.config import load_default_config, finish_base_config, defaults from mapproxy.config.validator import validate_references from mapproxy.config.spec import validate_options from mapproxy.util.py import memoize from mapproxy.util.ext.odict import odict from mapproxy.util.yaml import load_yaml_file, YAMLError from mapproxy.util.fs import find_exec from mapproxy.compat.modules import urlparse from mapproxy.compat import string_type, iteritems class ConfigurationError(Exception): pass class ProxyConfiguration(object): def __init__(self, conf, conf_base_dir=None, seed=False, renderd=False): self.configuration = conf self.seed = seed self.renderd = renderd if conf_base_dir is None: conf_base_dir = os.getcwd() self.load_globals(conf_base_dir=conf_base_dir) self.load_grids() self.load_caches() self.load_sources() self.load_wms_root_layer() self.load_tile_layers() self.load_services() def load_globals(self, conf_base_dir): self.globals = GlobalConfiguration(conf_base_dir=conf_base_dir, conf=self.configuration.get('globals') or {}, context=self) def load_grids(self): self.grids = {} grid_configs = dict(defaults.grids) grid_configs.update(self.configuration.get('grids') or {}) for grid_name, grid_conf in iteritems(grid_configs): grid_conf.setdefault('name', grid_name) self.grids[grid_name] = GridConfiguration(grid_conf, context=self) def load_caches(self): self.caches = odict() caches_conf = self.configuration.get('caches') if not caches_conf: return if isinstance(caches_conf, list): caches_conf = list_of_dicts_to_ordered_dict(caches_conf) for cache_name, cache_conf in iteritems(caches_conf): cache_conf['name'] = cache_name self.caches[cache_name] = CacheConfiguration(conf=cache_conf, context=self) def load_sources(self): self.sources = SourcesCollection() for source_name, source_conf in iteritems((self.configuration.get('sources') or {})): self.sources[source_name] = SourceConfiguration.load(conf=source_conf, context=self) def load_tile_layers(self): self.layers = odict() layers_conf = deepcopy(self._layers_conf_dict()) if layers_conf is None: return layers = self._flatten_layers_conf_dict(layers_conf) for layer_name, layer_conf in iteritems(layers): layer_conf['name'] = layer_name self.layers[layer_name] = LayerConfiguration(conf=layer_conf, context=self) def _legacy_layers_conf_dict(self): """ Read old style layer configuration with a dictionary where the key is the layer name. Optionally: a list an each layer is wrapped in such dictionary. :: layers: foo: title: xxx sources: [] bar: title: xxx sources: [] or :: layers: - foo: title: xxx sources: [] - bar: title: xxx sources: [] """ warnings.warn('old layer configuration syntax is deprecated since 1.4.0. ' 'use list of dictionaries as documented', RuntimeWarning) layers = [] layers_conf = self.configuration.get('layers') if not layers_conf: return None # TODO config error if isinstance(layers_conf, list): layers_conf = list_of_dicts_to_ordered_dict(layers_conf) for layer_name, layer_conf in iteritems(layers_conf): layer_conf['name'] = layer_name layers.append(layer_conf) return dict(title=None, layers=layers) def _layers_conf_dict(self): """ Returns (recursive) layer configuration as a dictionary in unified structure: :: { title: 'xxx', # required, might be None name: 'xxx', # optional # sources or layers or both are required sources: [], layers: [ {..., ...} # more layers like this ] } Multiple layers will be wrapped in an unnamed root layer, if the first level starts with multiple layers. """ layers_conf = self.configuration.get('layers') if layers_conf is None: return if isinstance(layers_conf, list): if isinstance(layers_conf[0], dict) and len(layers_conf[0].keys()) == 1: # looks like ordered legacy config layers_conf = self._legacy_layers_conf_dict() elif len(layers_conf) == 1 and ( 'layers' in layers_conf[0] or 'sources' in layers_conf[0] or 'tile_sources' in layers_conf[0]): # single root layer in list -> remove list layers_conf = layers_conf[0] else: # layer list without root -> wrap in root layer layers_conf = dict(title=None, layers=layers_conf) if len(set(layers_conf.keys()) & set('layers name title sources'.split())) < 2: # looks like unordered legacy config layers_conf = self._legacy_layers_conf_dict() return layers_conf def _flatten_layers_conf_dict(self, layers_conf, _layers=None): """ Returns a dictionary with all layers that have a name and sources. Flattens the layer tree. """ layers = _layers if _layers is not None else odict() if 'layers' in layers_conf: for layer in layers_conf.pop('layers'): self._flatten_layers_conf_dict(layer, layers) if 'name' in layers_conf and ('sources' in layers_conf or 'tile_sources' in layers_conf): layers[layers_conf['name']] = layers_conf return layers def load_wms_root_layer(self): self.wms_root_layer = None layers_conf = self._layers_conf_dict() if layers_conf is None: return self.wms_root_layer = WMSLayerConfiguration(layers_conf, context=self) def load_services(self): self.services = ServiceConfiguration(self.configuration.get('services', {}), context=self) def configured_services(self): with self: return self.services.services() def __enter__(self): # push local base_config onto config stack import mapproxy.config.config mapproxy.config.config._config.push(self.base_config) def __exit__(self, type, value, traceback): # pop local base_config from config stack import mapproxy.config.config mapproxy.config.config._config.pop() @property def base_config(self): return self.globals.base_config def config_files(self): """ Returns a dictionary with all configuration filenames and there timestamps. Contains any included files as well (see `base` option). """ return self.configuration.get('__config_files__', {}) def list_of_dicts_to_ordered_dict(dictlist): """ >>> d = list_of_dicts_to_ordered_dict([{'a': 1}, {'b': 2}, {'c': 3}]) >>> list(d.items()) [('a', 1), ('b', 2), ('c', 3)] """ result = odict() for d in dictlist: for k, v in iteritems(d): result[k] = v return result class ConfigurationBase(object): """ Base class for all configurations. """ defaults = {} def __init__(self, conf, context): """ :param conf: the configuration part for this configurator :param context: the complete proxy configuration :type context: ProxyConfiguration """ self.conf = conf self.context = context for k, v in iteritems(self.defaults): if k not in self.conf: self.conf[k] = v class GridConfiguration(ConfigurationBase): @memoize def tile_grid(self): from mapproxy.grid import tile_grid if 'base' in self.conf: base_grid_name = self.conf['base'] if not base_grid_name in self.context.grids: raise ConfigurationError('unknown base %s for grid %s' % (base_grid_name, self.conf['name'])) conf = self.context.grids[base_grid_name].conf.copy() conf.update(self.conf) conf.pop('base') self.conf = conf else: conf = self.conf align_with = None if 'align_resolutions_with' in self.conf: align_with_grid_name = self.conf['align_resolutions_with'] align_with = self.context.grids[align_with_grid_name].tile_grid() tile_size = self.context.globals.get_value('tile_size', conf, global_key='grid.tile_size') conf['tile_size'] = tuple(tile_size) tile_size = tuple(tile_size) stretch_factor = self.context.globals.get_value('stretch_factor', conf, global_key='image.stretch_factor') max_shrink_factor = self.context.globals.get_value('max_shrink_factor', conf, global_key='image.max_shrink_factor') if conf.get('origin') is None: log.warn('grid %s does not have an origin. default origin will change from sw (south/west) to nw (north-west) with MapProxy 2.0', conf['name'], ) grid = tile_grid( name=conf['name'], srs=conf.get('srs'), tile_size=tile_size, min_res=conf.get('min_res'), max_res=conf.get('max_res'), res=conf.get('res'), res_factor=conf.get('res_factor', 2.0), threshold_res=conf.get('threshold_res'), bbox=conf.get('bbox'), bbox_srs=conf.get('bbox_srs'), num_levels=conf.get('num_levels'), stretch_factor=stretch_factor, max_shrink_factor=max_shrink_factor, align_with=align_with, origin=conf.get('origin') ) return grid class GlobalConfiguration(ConfigurationBase): def __init__(self, conf_base_dir, conf, context): ConfigurationBase.__init__(self, conf, context) self.base_config = load_default_config() self._copy_conf_values(self.conf, self.base_config) self.base_config.conf_base_dir = conf_base_dir finish_base_config(self.base_config) self.image_options = ImageOptionsConfiguration(self.conf.get('image', {}), context) self.renderd_address = self.get_value('renderd.address') def _copy_conf_values(self, d, target): for k, v in iteritems(d): if v is None: continue if (hasattr(v, 'iteritems') or hasattr(v, 'items')) and k in target: self._copy_conf_values(v, target[k]) else: target[k] = v def get_value(self, key, local={}, global_key=None, default_key=None): result = dotted_dict_get(key, local) if result is None: result = dotted_dict_get(global_key or key, self.conf) if result is None: result = dotted_dict_get(default_key or global_key or key, self.base_config) return result def get_path(self, key, local, global_key=None, default_key=None): value = self.get_value(key, local, global_key, default_key) if value is not None: value = self.abspath(value) return value def abspath(self, path): return os.path.join(self.base_config.conf_base_dir, path) default_image_options = { } class ImageOptionsConfiguration(ConfigurationBase): def __init__(self, conf, context): ConfigurationBase.__init__(self, conf, context) self._init_formats() def _init_formats(self): self.formats = {} formats_config = default_image_options.copy() for format, conf in iteritems(self.conf.get('formats', {})): if format in formats_config: tmp = formats_config[format].copy() tmp.update(conf) conf = tmp if 'resampling_method' in conf: conf['resampling'] = conf.pop('resampling_method') if 'encoding_options' in conf: self._check_encoding_options(conf['encoding_options']) if 'merge_method' in conf: warnings.warn('merge_method now defaults to composite. option no longer required', DeprecationWarning) formats_config[format] = conf for format, conf in iteritems(formats_config): if 'format' not in conf and format.startswith('image/'): conf['format'] = format self.formats[format] = conf def _check_encoding_options(self, options): if not options: return options = options.copy() jpeg_quality = options.pop('jpeg_quality', None) if jpeg_quality and not isinstance(jpeg_quality, int): raise ConfigurationError('jpeg_quality is not an integer') quantizer = options.pop('quantizer', None) if quantizer and quantizer not in ('fastoctree', 'mediancut'): raise ConfigurationError('unknown quantizer') if options: raise ConfigurationError('unknown encoding_options: %r' % options) def image_opts(self, image_conf, format): from mapproxy.image.opts import ImageOptions if not image_conf: image_conf = {} conf = {} if format in self.formats: conf = self.formats[format].copy() resampling = image_conf.get('resampling_method') or conf.get('resampling') if resampling is None: resampling = self.context.globals.get_value('image.resampling_method', {}) transparent = image_conf.get('transparent') opacity = image_conf.get('opacity') img_format = image_conf.get('format') colors = image_conf.get('colors') mode = image_conf.get('mode') encoding_options = image_conf.get('encoding_options') if 'merge_method' in image_conf: warnings.warn('merge_method now defaults to composite. option no longer required', DeprecationWarning) self._check_encoding_options(encoding_options) # only overwrite default if it is not None for k, v in iteritems(dict(transparent=transparent, opacity=opacity, resampling=resampling, format=img_format, colors=colors, mode=mode, encoding_options=encoding_options, )): if v is not None: conf[k] = v if 'format' not in conf and format and format.startswith('image/'): conf['format'] = format # caches shall be able to store png and jpeg tiles with mixed format if format == 'mixed': conf['format'] = format # force 256 colors for image.paletted for backwards compat paletted = self.context.globals.get_value('image.paletted', self.conf) if conf.get('colors') is None and 'png' in conf.get('format', '') and paletted: conf['colors'] = 256 opts = ImageOptions(**conf) return opts def dotted_dict_get(key, d): """ >>> dotted_dict_get('foo', {'foo': {'bar': 1}}) {'bar': 1} >>> dotted_dict_get('foo.bar', {'foo': {'bar': 1}}) 1 >>> dotted_dict_get('bar', {'foo': {'bar': 1}}) """ parts = key.split('.') try: while parts and d: d = d[parts.pop(0)] except KeyError: return None if parts: # not completely resolved return None return d class SourcesCollection(dict): """ Collection of SourceConfigurations. Allows access to tagged WMS sources, e.g. ``sc['source_name:lyr,lyr2']`` will return the source with ``source_name`` and set ``req.layers`` to ``lyr1,lyr2``. """ def __getitem__(self, key): layers = None source_name = key if ':' in source_name: source_name, layers = source_name.split(':', 1) source = dict.__getitem__(self, source_name) if not layers: return source if source.conf.get('type') not in ('wms', 'mapserver', 'mapnik'): raise ConfigurationError("found ':' in: '%s'." " tagged sources only supported for WMS/Mapserver/Mapnik" % key) uses_req = source.conf.get('type') != 'mapnik' source = copy(source) source.conf = deepcopy(source.conf) if uses_req: supported_layers = source.conf['req'].get('layers', []) else: supported_layers = source.conf.get('layers', []) supported_layer_set = SourcesCollection.layer_set(supported_layers) layer_set = SourcesCollection.layer_set(layers) if supported_layer_set and not layer_set.issubset(supported_layer_set): raise ConfigurationError('layers (%s) not supported by source (%s)' % ( layers, ','.join(supported_layer_set))) if uses_req: source.conf['req']['layers'] = layers else: source.conf['layers'] = layers return source def __contains__(self, key): source_name = key if ':' in source_name: source_name, _ = source_name.split(':', 1) return dict.__contains__(self, source_name) @staticmethod def layer_set(layers): if isinstance(layers, (list, tuple)): return set(layers) return set(layers.split(',')) class SourceConfiguration(ConfigurationBase): supports_meta_tiles = True @classmethod def load(cls, conf, context): source_type = conf['type'] subclass = source_configuration_types.get(source_type) if not subclass: raise ConfigurationError("unknown source type '%s'" % source_type) return subclass(conf, context) @memoize def coverage(self): if not 'coverage' in self.conf: return None from mapproxy.config.coverage import load_coverage return load_coverage(self.conf['coverage']) def image_opts(self, format=None): if 'transparent' in self.conf: self.conf.setdefault('image', {})['transparent'] = self.conf['transparent'] return self.context.globals.image_options.image_opts(self.conf.get('image', {}), format) def http_client(self, url): from mapproxy.client.http import auth_data_from_url, HTTPClient http_client = None url, (username, password) = auth_data_from_url(url) insecure = ssl_ca_certs = None if 'https' in url: insecure = self.context.globals.get_value('http.ssl_no_cert_checks', self.conf) ssl_ca_certs = self.context.globals.get_path('http.ssl_ca_certs', self.conf) timeout = self.context.globals.get_value('http.client_timeout', self.conf) headers = self.context.globals.get_value('http.headers', self.conf) http_client = HTTPClient(url, username, password, insecure=insecure, ssl_ca_certs=ssl_ca_certs, timeout=timeout, headers=headers) return http_client, url @memoize def on_error_handler(self): if not 'on_error' in self.conf: return None from mapproxy.source.error import HTTPSourceErrorHandler error_handler = HTTPSourceErrorHandler() for status_code, response_conf in iteritems(self.conf['on_error']): if not isinstance(status_code, int) and status_code != 'other': raise ConfigurationError("invalid error code %r in on_error", status_code) cacheable = response_conf.get('cache', False) color = response_conf.get('response', 'transparent') if color == 'transparent': color = (255, 255, 255, 0) else: color = parse_color(color) error_handler.add_handler(status_code, color, cacheable) return error_handler def resolution_range(conf): from mapproxy.grid import resolution_range as _resolution_range if 'min_res' in conf or 'max_res' in conf: return _resolution_range(min_res=conf.get('min_res'), max_res=conf.get('max_res')) if 'min_scale' in conf or 'max_scale' in conf: return _resolution_range(min_scale=conf.get('min_scale'), max_scale=conf.get('max_scale')) class ArcGISSourceConfiguration(SourceConfiguration): source_type = ('arcgis',) def __init__(self, conf, context): SourceConfiguration.__init__(self, conf, context) def source(self, params=None): from mapproxy.client.arcgis import ArcGISClient from mapproxy.source.arcgis import ArcGISSource from mapproxy.srs import SRS from mapproxy.request.arcgis import create_request if not self.conf.get('opts', {}).get('map', True): return None if not self.context.seed and self.conf.get('seed_only'): from mapproxy.source import DummySource return DummySource(coverage=self.coverage()) # Get the supported SRS codes and formats from the configuration. supported_srs = [SRS(code) for code in self.conf.get("supported_srs", [])] supported_formats = [file_ext(f) for f in self.conf.get("supported_formats", [])] # Construct the parameters if params is None: params = {} request_format = self.conf['req'].get('format') if request_format: params['format'] = request_format request = create_request(self.conf["req"], params) http_client, request.url = self.http_client(request.url) coverage = self.coverage() res_range = resolution_range(self.conf) client = ArcGISClient(request, http_client) image_opts = self.image_opts(format=params.get('format')) return ArcGISSource(client, image_opts=image_opts, coverage=coverage, res_range=res_range, supported_srs=supported_srs, supported_formats=supported_formats or None) def fi_source(self, params=None): from mapproxy.client.arcgis import ArcGISInfoClient from mapproxy.request.arcgis import create_identify_request from mapproxy.source.arcgis import ArcGISInfoSource from mapproxy.srs import SRS if params is None: params = {} request_format = self.conf['req'].get('format') if request_format: params['format'] = request_format supported_srs = [SRS(code) for code in self.conf.get('supported_srs', [])] fi_source = None if self.conf.get('opts', {}).get('featureinfo', False): opts = self.conf['opts'] tolerance = opts.get('featureinfo_tolerance', 5) return_geometries = opts.get('featureinfo_return_geometries', False) fi_request = create_identify_request(self.conf['req'], params) http_client, fi_request.url = self.http_client(fi_request.url) fi_client = ArcGISInfoClient(fi_request, supported_srs=supported_srs, http_client=http_client, tolerance=tolerance, return_geometries=return_geometries, ) fi_source = ArcGISInfoSource(fi_client) return fi_source class WMSSourceConfiguration(SourceConfiguration): source_type = ('wms',) @staticmethod def static_legend_source(url, context): from mapproxy.cache.legend import LegendCache from mapproxy.client.wms import WMSLegendURLClient from mapproxy.source.wms import WMSLegendSource cache_dir = os.path.join(context.globals.get_path('cache.base_dir', {}), 'legends') if url.startswith('file://') and not url.startswith('file:///'): prefix = 'file://' url = prefix + context.globals.abspath(url[7:]) lg_client = WMSLegendURLClient(url) legend_cache = LegendCache(cache_dir=cache_dir) return WMSLegendSource([lg_client], legend_cache, static=True) def fi_xslt_transformer(self, conf, context): from mapproxy.featureinfo import XSLTransformer, has_xslt_support fi_transformer = None fi_xslt = conf.get('featureinfo_xslt') if fi_xslt: if not has_xslt_support: raise ValueError('featureinfo_xslt requires lxml. Please install.') fi_xslt = context.globals.abspath(fi_xslt) fi_transformer = XSLTransformer(fi_xslt) return fi_transformer def image_opts(self, format=None): if 'transparent' not in (self.conf.get('image') or {}): transparent = self.conf['req'].get('transparent') if transparent is not None: transparent = bool(str(transparent).lower() == 'true') self.conf.setdefault('image', {})['transparent'] = transparent return SourceConfiguration.image_opts(self, format=format) def source(self, params=None): from mapproxy.client.wms import WMSClient from mapproxy.request.wms import create_request from mapproxy.source.wms import WMSSource from mapproxy.srs import SRS if not self.conf.get('wms_opts', {}).get('map', True): return None if not self.context.seed and self.conf.get('seed_only'): from mapproxy.source import DummySource return DummySource(coverage=self.coverage()) if params is None: params = {} request_format = self.conf['req'].get('format') if request_format: params['format'] = request_format image_opts = self.image_opts(format=params.get('format')) supported_srs = [SRS(code) for code in self.conf.get('supported_srs', [])] supported_formats = [file_ext(f) for f in self.conf.get('supported_formats', [])] version = self.conf.get('wms_opts', {}).get('version', '1.1.1') lock = None concurrent_requests = self.context.globals.get_value('concurrent_requests', self.conf, global_key='http.concurrent_requests') if concurrent_requests: from mapproxy.util.lock import SemLock lock_dir = self.context.globals.get_path('cache.lock_dir', self.conf) lock_timeout = self.context.globals.get_value('http.client_timeout', self.conf) url = urlparse.urlparse(self.conf['req']['url']) md5 = hashlib.md5(url.netloc.encode('ascii')) lock_file = os.path.join(lock_dir, md5.hexdigest() + '.lck') lock = lambda: SemLock(lock_file, concurrent_requests, timeout=lock_timeout) coverage = self.coverage() res_range = resolution_range(self.conf) transparent_color = (self.conf.get('image') or {}).get('transparent_color') transparent_color_tolerance = self.context.globals.get_value( 'image.transparent_color_tolerance', self.conf) if transparent_color: transparent_color = parse_color(transparent_color) http_method = self.context.globals.get_value('http.method', self.conf) fwd_req_params = set(self.conf.get('forward_req_params', [])) request = create_request(self.conf['req'], params, version=version, abspath=self.context.globals.abspath) http_client, request.url = self.http_client(request.url) client = WMSClient(request, http_client=http_client, http_method=http_method, lock=lock, fwd_req_params=fwd_req_params) return WMSSource(client, image_opts=image_opts, coverage=coverage, res_range=res_range, transparent_color=transparent_color, transparent_color_tolerance=transparent_color_tolerance, supported_srs=supported_srs, supported_formats=supported_formats or None, fwd_req_params=fwd_req_params) def fi_source(self, params=None): from mapproxy.client.wms import WMSInfoClient from mapproxy.request.wms import create_request from mapproxy.source.wms import WMSInfoSource from mapproxy.srs import SRS if params is None: params = {} request_format = self.conf['req'].get('format') if request_format: params['format'] = request_format supported_srs = [SRS(code) for code in self.conf.get('supported_srs', [])] fi_source = None if self.conf.get('wms_opts', {}).get('featureinfo', False): wms_opts = self.conf['wms_opts'] version = wms_opts.get('version', '1.1.1') if 'featureinfo_format' in wms_opts: params['info_format'] = wms_opts['featureinfo_format'] fi_request = create_request(self.conf['req'], params, req_type='featureinfo', version=version, abspath=self.context.globals.abspath) fi_transformer = self.fi_xslt_transformer(self.conf.get('wms_opts', {}), self.context) http_client, fi_request.url = self.http_client(fi_request.url) fi_client = WMSInfoClient(fi_request, supported_srs=supported_srs, http_client=http_client) fi_source = WMSInfoSource(fi_client, fi_transformer=fi_transformer) return fi_source def lg_source(self, params=None): from mapproxy.cache.legend import LegendCache from mapproxy.client.wms import WMSLegendClient from mapproxy.request.wms import create_request from mapproxy.source.wms import WMSLegendSource if params is None: params = {} request_format = self.conf['req'].get('format') if request_format: params['format'] = request_format lg_source = None cache_dir = os.path.join(self.context.globals.get_path('cache.base_dir', {}), 'legends') if self.conf.get('wms_opts', {}).get('legendurl', False): lg_url = self.conf.get('wms_opts', {}).get('legendurl') lg_source = WMSSourceConfiguration.static_legend_source(lg_url, self.context) elif self.conf.get('wms_opts', {}).get('legendgraphic', False): version = self.conf.get('wms_opts', {}).get('version', '1.1.1') lg_req = self.conf['req'].copy() lg_clients = [] lg_layers = str(lg_req['layers']).split(',') del lg_req['layers'] for lg_layer in lg_layers: lg_req['layer'] = lg_layer lg_request = create_request(lg_req, params, req_type='legendgraphic', version=version, abspath=self.context.globals.abspath) http_client, lg_request.url = self.http_client(lg_request.url) lg_client = WMSLegendClient(lg_request, http_client=http_client) lg_clients.append(lg_client) legend_cache = LegendCache(cache_dir=cache_dir) lg_source = WMSLegendSource(lg_clients, legend_cache) return lg_source class MapServerSourceConfiguration(WMSSourceConfiguration): source_type = ('mapserver',) def __init__(self, conf, context): WMSSourceConfiguration.__init__(self, conf, context) self.script = self.context.globals.get_path('mapserver.binary', self.conf) if not self.script: self.script = find_exec('mapserv') if not self.script or not os.path.isfile(self.script): raise ConfigurationError('could not find mapserver binary (%r)' % (self.script, )) # set url to dummy script name, required as identifier # for concurrent_request self.conf['req']['url'] = 'mapserver://' + self.script mapfile = self.context.globals.abspath(self.conf['req']['map']) self.conf['req']['map'] = mapfile def http_client(self, url): working_dir = self.context.globals.get_path('mapserver.working_dir', self.conf) if working_dir and not os.path.isdir(working_dir): raise ConfigurationError('could not find mapserver working_dir (%r)' % (working_dir, )) from mapproxy.client.cgi import CGIClient client = CGIClient(script=self.script, working_directory=working_dir) return client, url class MapnikSourceConfiguration(SourceConfiguration): source_type = ('mapnik',) def source(self, params=None): if not self.context.seed and self.conf.get('seed_only'): from mapproxy.source import DummySource return DummySource(coverage=self.coverage()) image_opts = self.image_opts() lock = None concurrent_requests = self.context.globals.get_value('concurrent_requests', self.conf, global_key='http.concurrent_requests') if concurrent_requests: from mapproxy.util.lock import SemLock lock_dir = self.context.globals.get_path('cache.lock_dir', self.conf) md5 = hashlib.md5(self.conf['mapfile']) lock_file = os.path.join(lock_dir, md5.hexdigest() + '.lck') lock = lambda: SemLock(lock_file, concurrent_requests) coverage = self.coverage() res_range = resolution_range(self.conf) scale_factor = self.conf.get('scale_factor', None) layers = self.conf.get('layers', None) if isinstance(layers, string_type): layers = layers.split(',') mapfile = self.context.globals.abspath(self.conf['mapfile']) if self.conf.get('use_mapnik2', False): warnings.warn('use_mapnik2 option is no longer needed for Mapnik 2 support', DeprecationWarning) from mapproxy.source.mapnik import MapnikSource, mapnik as mapnik_api if mapnik_api is None: raise ConfigurationError('Could not import Mapnik, please verify it is installed!') if self.context.renderd: # only renderd guarantees that we have a single proc/thread # that accesses the same mapnik map object reuse_map_objects = True else: reuse_map_objects = False return MapnikSource(mapfile, layers=layers, image_opts=image_opts, coverage=coverage, res_range=res_range, lock=lock, reuse_map_objects=reuse_map_objects, scale_factor=scale_factor) class TileSourceConfiguration(SourceConfiguration): supports_meta_tiles = False source_type = ('tile',) defaults = {} def source(self, params=None): from mapproxy.client.tile import TileClient, TileURLTemplate from mapproxy.source.tile import TiledSource if not self.context.seed and self.conf.get('seed_only'): from mapproxy.source import DummySource return DummySource(coverage=self.coverage()) if params is None: params = {} url = self.conf['url'] if self.conf.get('origin'): warnings.warn('origin for tile sources is deprecated since 1.3.0 ' 'and will be ignored. use grid with correct origin.', RuntimeWarning) http_client, url = self.http_client(url) grid_name = self.conf.get('grid') if grid_name is None: log.warn("tile source for %s does not have a grid configured and defaults to GLOBAL_MERCATOR. default will change with MapProxy 2.0", url) grid_name = "GLOBAL_MERCATOR" grid = self.context.grids[grid_name].tile_grid() coverage = self.coverage() res_range = resolution_range(self.conf) image_opts = self.image_opts() error_handler = self.on_error_handler() format = file_ext(params['format']) client = TileClient(TileURLTemplate(url, format=format), http_client=http_client, grid=grid) return TiledSource(grid, client, coverage=coverage, image_opts=image_opts, error_handler=error_handler, res_range=res_range) def file_ext(mimetype): from mapproxy.request.base import split_mime_type _mime_class, format, _options = split_mime_type(mimetype) return format class DebugSourceConfiguration(SourceConfiguration): source_type = ('debug',) required_keys = set('type'.split()) def source(self, params=None): from mapproxy.source import DebugSource return DebugSource() source_configuration_types = { 'wms': WMSSourceConfiguration, 'arcgis': ArcGISSourceConfiguration, 'tile': TileSourceConfiguration, 'debug': DebugSourceConfiguration, 'mapserver': MapServerSourceConfiguration, 'mapnik': MapnikSourceConfiguration, } class CacheConfiguration(ConfigurationBase): defaults = {'format': 'image/png'} @memoize def cache_dir(self): cache_dir = self.conf.get('cache', {}).get('directory') if cache_dir: if self.conf.get('cache_dir'): log.warn('found cache.directory and cache_dir option for %s, ignoring cache_dir', self.conf['name']) return self.context.globals.abspath(cache_dir) return self.context.globals.get_path('cache_dir', self.conf, global_key='cache.base_dir') @memoize def has_multiple_grids(self): return len(self.grid_confs()) > 1 def lock_dir(self): lock_dir = self.context.globals.get_path('cache.tile_lock_dir', self.conf) if not lock_dir: lock_dir = os.path.join(self.cache_dir(), 'tile_locks') return lock_dir def _file_cache(self, grid_conf, file_ext): from mapproxy.cache.file import FileCache cache_dir = self.cache_dir() directory_layout = self.conf.get('cache', {}).get('directory_layout', 'tc') if self.conf.get('cache', {}).get('directory'): if self.has_multiple_grids(): raise ConfigurationError( "using single directory for cache with multiple grids in %s" % (self.conf['name']), ) pass elif self.conf.get('cache', {}).get('use_grid_names'): cache_dir = os.path.join(cache_dir, self.conf['name'], grid_conf.tile_grid().name) else: suffix = grid_conf.conf['srs'].replace(':', '') cache_dir = os.path.join(cache_dir, self.conf['name'] + '_' + suffix) link_single_color_images = self.context.globals.get_value('link_single_color_images', self.conf, global_key='cache.link_single_color_images') if link_single_color_images and sys.platform == 'win32': log.warn('link_single_color_images not supported on windows') link_single_color_images = False return FileCache( cache_dir, file_ext=file_ext, directory_layout=directory_layout, link_single_color_images=link_single_color_images, ) def _mbtiles_cache(self, grid_conf, file_ext): from mapproxy.cache.mbtiles import MBTilesCache filename = self.conf['cache'].get('filename') if not filename: filename = self.conf['name'] + '.mbtiles' if filename.startswith('.' + os.sep): mbfile_path = self.context.globals.abspath(filename) else: mbfile_path = os.path.join(self.cache_dir(), filename) sqlite_timeout = self.context.globals.get_value('cache.sqlite_timeout', self.conf) wal = self.context.globals.get_value('cache.sqlite_wal', self.conf) return MBTilesCache( mbfile_path, timeout=sqlite_timeout, wal=wal, ) def _geopackage_cache(self, grid_conf, file_ext): from mapproxy.cache.geopackage import GeopackageCache, GeopackageLevelCache filename = self.conf['cache'].get('filename') table_name = self.conf['cache'].get('table_name') or \ "{}_{}".format(self.conf['name'], grid_conf.tile_grid().name) levels = self.conf['cache'].get('levels') if not filename: filename = self.conf['name'] + '.gpkg' if filename.startswith('.' + os.sep): gpkg_file_path = self.context.globals.abspath(filename) else: gpkg_file_path = os.path.join(self.cache_dir(), filename) cache_dir = self.conf['cache'].get('directory') if cache_dir: cache_dir = os.path.join( self.context.globals.abspath(cache_dir), grid_conf.tile_grid().name ) else: cache_dir = self.cache_dir() cache_dir = os.path.join( cache_dir, self.conf['name'], grid_conf.tile_grid().name ) if levels: return GeopackageLevelCache( cache_dir, grid_conf.tile_grid(), table_name ) else: return GeopackageCache( gpkg_file_path, grid_conf.tile_grid(), table_name ) def _s3_cache(self, grid_conf, file_ext): from mapproxy.cache.s3 import S3Cache bucket_name = self.context.globals.get_value('cache.bucket_name', self.conf, global_key='cache.s3.bucket_name') if not bucket_name: raise ConfigurationError("no bucket_name configured for s3 cache %s" % self.conf['name']) profile_name = self.context.globals.get_value('cache.profile_name', self.conf, global_key='cache.s3.profile_name') directory_layout = self.conf['cache'].get('directory_layout', 'tms') base_path = self.conf['cache'].get('directory', None) if base_path is None: base_path = os.path.join(self.conf['name'], grid_conf.tile_grid().name) return S3Cache( base_path=base_path, file_ext=file_ext, directory_layout=directory_layout, bucket_name=bucket_name, profile_name=profile_name, ) def _sqlite_cache(self, grid_conf, file_ext): from mapproxy.cache.mbtiles import MBTilesLevelCache cache_dir = self.conf.get('cache', {}).get('directory') if cache_dir: cache_dir = os.path.join( self.context.globals.abspath(cache_dir), grid_conf.tile_grid().name ) else: cache_dir = self.cache_dir() cache_dir = os.path.join( cache_dir, self.conf['name'], grid_conf.tile_grid().name ) sqlite_timeout = self.context.globals.get_value('cache.sqlite_timeout', self.conf) wal = self.context.globals.get_value('cache.sqlite_wal', self.conf) return MBTilesLevelCache( cache_dir, timeout=sqlite_timeout, wal=wal, ) def _couchdb_cache(self, grid_conf, file_ext): from mapproxy.cache.couchdb import CouchDBCache, CouchDBMDTemplate db_name = self.conf['cache'].get('db_name') if not db_name: suffix = grid_conf.conf['srs'].replace(':', '') db_name = self.conf['name'] + '_' + suffix url = self.conf['cache'].get('url') if not url: url = 'http://127.0.0.1:5984' md_template = CouchDBMDTemplate(self.conf['cache'].get('tile_metadata', {})) tile_id = self.conf['cache'].get('tile_id') return CouchDBCache(url=url, db_name=db_name, file_ext=file_ext, tile_grid=grid_conf.tile_grid(), md_template=md_template, tile_id_template=tile_id) def _riak_cache(self, grid_conf, file_ext): from mapproxy.cache.riak import RiakCache default_ports = self.conf['cache'].get('default_ports', {}) default_pb_port = default_ports.get('pb', 8087) default_http_port = default_ports.get('http', 8098) nodes = self.conf['cache'].get('nodes') if not nodes: nodes = [{'host': '127.0.0.1'}] for n in nodes: if 'pb_port' not in n: n['pb_port'] = default_pb_port if 'http_port' not in n: n['http_port'] = default_http_port protocol = self.conf['cache'].get('protocol', 'pbc') bucket = self.conf['cache'].get('bucket') if not bucket: suffix = grid_conf.tile_grid().name bucket = self.conf['name'] + '_' + suffix use_secondary_index = self.conf['cache'].get('secondary_index', False) timeout = self.context.globals.get_value('http.client_timeout', self.conf) return RiakCache(nodes=nodes, protocol=protocol, bucket=bucket, tile_grid=grid_conf.tile_grid(), use_secondary_index=use_secondary_index, timeout=timeout ) def _redis_cache(self, grid_conf, file_ext): from mapproxy.cache.redis import RedisCache host = self.conf['cache'].get('host', '127.0.0.1') port = self.conf['cache'].get('port', 6379) db = self.conf['cache'].get('db', 0) ttl = self.conf['cache'].get('default_ttl', 3600) prefix = self.conf['cache'].get('prefix') if not prefix: prefix = self.conf['name'] + '_' + grid_conf.tile_grid().name return RedisCache( host=host, port=port, db=db, prefix=prefix, ttl=ttl, ) def _compact_cache(self, grid_conf, file_ext): from mapproxy.cache.compact import CompactCacheV1, CompactCacheV2 cache_dir = self.cache_dir() if self.conf.get('cache', {}).get('directory'): if self.has_multiple_grids(): raise ConfigurationError( "using single directory for cache with multiple grids in %s" % (self.conf['name']), ) pass else: cache_dir = os.path.join(cache_dir, self.conf['name'], grid_conf.tile_grid().name) version = self.conf['cache']['version'] if version == 1: return CompactCacheV1(cache_dir=cache_dir) elif version == 2: return CompactCacheV2(cache_dir=cache_dir) raise ConfigurationError("compact cache only supports version 1 or 2") def _tile_cache(self, grid_conf, file_ext): if self.conf.get('disable_storage', False): from mapproxy.cache.dummy import DummyCache return DummyCache() grid_conf.tile_grid() #create to resolve `base` in grid_conf.conf cache_type = self.conf.get('cache', {}).get('type', 'file') return getattr(self, '_%s_cache' % cache_type)(grid_conf, file_ext) def _tile_filter(self): filters = [] if 'watermark' in self.conf: from mapproxy.tilefilter import create_watermark_filter if self.conf['watermark'].get('color'): self.conf['watermark']['color'] = parse_color(self.conf['watermark']['color']) f = create_watermark_filter(self.conf, self.context) if f: filters.append(f) return filters @memoize def image_opts(self): from mapproxy.image.opts import ImageFormat format = None if 'format' not in self.conf.get('image', {}): format = self.conf.get('format') or self.conf.get('request_format') image_opts = self.context.globals.image_options.image_opts(self.conf.get('image', {}), format) if image_opts.format is None: if format is not None and format.startswith('image/'): image_opts.format = ImageFormat(format) else: image_opts.format = ImageFormat('image/png') return image_opts def supports_tiled_only_access(self, params=None, tile_grid=None): caches = self.caches() if len(caches) > 1: return False cache_grid, extent, tile_manager = caches[0] image_opts = self.image_opts() if (tile_grid.is_subset_of(cache_grid) and params.get('format') == image_opts.format): return True return False def source(self, params=None, tile_grid=None, tiled_only=False): from mapproxy.source.tile import CacheSource from mapproxy.layer import map_extent_from_grid caches = self.caches() if len(caches) > 1: # cache with multiple grids/sources source = self.map_layer() source.supports_meta_tiles = True return source cache_grid, extent, tile_manager = caches[0] image_opts = self.image_opts() cache_extent = map_extent_from_grid(tile_grid) cache_extent = extent.intersection(cache_extent) source = CacheSource(tile_manager, extent=cache_extent, image_opts=image_opts, tiled_only=tiled_only) return source def _sources_for_grid(self, source_names, grid_conf, request_format): sources = [] source_image_opts = [] # a cache can directly access source tiles when _all_ sources are caches too # and when they have compatible grids by using tiled_only on the CacheSource # check if all sources support tiled_only tiled_only = True for source_name in source_names: if source_name in self.context.sources: tiled_only = False break elif source_name in self.context.caches: cache_conf = self.context.caches[source_name] tiled_only = cache_conf.supports_tiled_only_access( params={'format': request_format}, tile_grid=grid_conf.tile_grid(), ) if not tiled_only: break for source_name in source_names: if source_name in self.context.sources: source_conf = self.context.sources[source_name] source = source_conf.source({'format': request_format}) elif source_name in self.context.caches: cache_conf = self.context.caches[source_name] source = cache_conf.source( params={'format': request_format}, tile_grid=grid_conf.tile_grid(), tiled_only=tiled_only, ) else: raise ConfigurationError('unknown source %s' % source_name) if source: sources.append(source) source_image_opts.append(source.image_opts) return sources, source_image_opts def _sources_for_band_merge(self, sources_conf, grid_conf, request_format): from mapproxy.image.merge import BandMerger source_names = [] for band, band_sources in iteritems(sources_conf): for source in band_sources: name = source['source'] if name in source_names: idx = source_names.index(name) else: source_names.append(name) idx = len(source_names) - 1 source["src_idx"] = idx sources, source_image_opts = self._sources_for_grid( source_names=source_names, grid_conf=grid_conf, request_format=request_format, ) if 'l' in sources_conf: mode = 'L' elif 'a' in sources_conf: mode = 'RGBA' else: mode = 'RGB' band_merger = BandMerger(mode=mode) available_bands = {'r': 0, 'g': 1, 'b': 2, 'a': 3, 'l': 0} for band, band_sources in iteritems(sources_conf): band_idx = available_bands.get(band) if band_idx is None: raise ConfigurationError("unsupported band '%s' for cache %s" % (band, self.conf['name'])) for source in band_sources: band_merger.add_ops( dst_band=band_idx, src_img=source['src_idx'], src_band=source['band'], factor=source.get('factor', 1.0), ) return band_merger, sources, source_image_opts @memoize def caches(self): from mapproxy.cache.dummy import DummyCache, DummyLocker from mapproxy.cache.tile import TileManager from mapproxy.cache.base import TileLocker from mapproxy.image.opts import compatible_image_options from mapproxy.layer import map_extent_from_grid, merge_layer_extents base_image_opts = self.image_opts() if self.conf.get('format') == 'mixed' and not self.conf.get('request_format') == 'image/png': raise ConfigurationError('request_format must be set to image/png if mixed mode is enabled') request_format = self.conf.get('request_format') or self.conf.get('format') if '/' in request_format: request_format_ext = request_format.split('/', 1)[1] else: request_format_ext = request_format caches = [] meta_buffer = self.context.globals.get_value('meta_buffer', self.conf, global_key='cache.meta_buffer') meta_size = self.context.globals.get_value('meta_size', self.conf, global_key='cache.meta_size') bulk_meta_tiles = self.context.globals.get_value('bulk_meta_tiles', self.conf, global_key='cache.bulk_meta_tiles') minimize_meta_requests = self.context.globals.get_value('minimize_meta_requests', self.conf, global_key='cache.minimize_meta_requests') concurrent_tile_creators = self.context.globals.get_value('concurrent_tile_creators', self.conf, global_key='cache.concurrent_tile_creators') renderd_address = self.context.globals.get_value('renderd.address', self.conf) band_merger = None for grid_name, grid_conf in self.grid_confs(): if isinstance(self.conf['sources'], dict): band_merger, sources, source_image_opts = self._sources_for_band_merge( self.conf['sources'], grid_conf=grid_conf, request_format=request_format, ) else: sources, source_image_opts = self._sources_for_grid( self.conf['sources'], grid_conf=grid_conf, request_format=request_format, ) if not sources: from mapproxy.source import DummySource sources = [DummySource()] source_image_opts.append(sources[0].image_opts) tile_grid = grid_conf.tile_grid() tile_filter = self._tile_filter() image_opts = compatible_image_options(source_image_opts, base_opts=base_image_opts) cache = self._tile_cache(grid_conf, image_opts.format.ext) identifier = self.conf['name'] + '_' + tile_grid.name tile_creator_class = None use_renderd = bool(renderd_address) if self.context.renderd: # we _are_ renderd use_renderd = False if self.conf.get('disable_storage', False): # can't ask renderd to create tiles that shouldn't be cached use_renderd = False if use_renderd: from mapproxy.cache.renderd import RenderdTileCreator, has_renderd_support if not has_renderd_support(): raise ConfigurationError("renderd requires requests library") if self.context.seed: priority = 10 else: priority = 100 cache_dir = self.cache_dir() lock_dir = self.context.globals.get_value('cache.tile_lock_dir') if not lock_dir: lock_dir = os.path.join(cache_dir, 'tile_locks') lock_timeout = self.context.globals.get_value('http.client_timeout', {}) locker = TileLocker(lock_dir, lock_timeout, identifier + '_renderd') # TODO band_merger tile_creator_class = partial(RenderdTileCreator, renderd_address, priority=priority, tile_locker=locker) else: from mapproxy.cache.tile import TileCreator tile_creator_class = partial(TileCreator, image_merger=band_merger) if isinstance(cache, DummyCache): locker = DummyLocker() else: locker = TileLocker( lock_dir=self.lock_dir(), lock_timeout=self.context.globals.get_value('http.client_timeout', {}), lock_cache_id=cache.lock_cache_id, ) mgr = TileManager(tile_grid, cache, sources, image_opts.format.ext, locker=locker, image_opts=image_opts, identifier=identifier, request_format=request_format_ext, meta_size=meta_size, meta_buffer=meta_buffer, minimize_meta_requests=minimize_meta_requests, concurrent_tile_creators=concurrent_tile_creators, pre_store_filter=tile_filter, tile_creator_class=tile_creator_class, bulk_meta_tiles=bulk_meta_tiles, ) extent = merge_layer_extents(sources) if extent.is_default: extent = map_extent_from_grid(tile_grid) caches.append((tile_grid, extent, mgr)) return caches @memoize def grid_confs(self): grid_names = self.conf.get('grids') if grid_names is None: log.warn('cache %s does not have any grids. default will change from [GLOBAL_MERCATOR] to [GLOBAL_WEBMERCATOR] with MapProxy 2.0', self.conf['name']) grid_names = ['GLOBAL_MERCATOR'] return [(g, self.context.grids[g]) for g in grid_names] @memoize def map_layer(self): from mapproxy.layer import CacheMapLayer, SRSConditional, ResolutionConditional image_opts = self.image_opts() max_tile_limit = self.context.globals.get_value('max_tile_limit', self.conf, global_key='cache.max_tile_limit') caches = [] main_grid = None for grid, extent, tile_manager in self.caches(): if main_grid is None: main_grid = grid caches.append((CacheMapLayer(tile_manager, extent=extent, image_opts=image_opts, max_tile_limit=max_tile_limit), (grid.srs,))) if len(caches) == 1: layer = caches[0][0] else: layer = SRSConditional(caches, caches[0][0].extent, opacity=image_opts.opacity) if 'use_direct_from_level' in self.conf: self.conf['use_direct_from_res'] = main_grid.resolution(self.conf['use_direct_from_level']) if 'use_direct_from_res' in self.conf: if len(self.conf['sources']) != 1: raise ValueError('use_direct_from_level/res only supports single sources') source_conf = self.context.sources[self.conf['sources'][0]] layer = ResolutionConditional(layer, source_conf.source(), self.conf['use_direct_from_res'], main_grid.srs, layer.extent, opacity=image_opts.opacity) return layer class WMSLayerConfiguration(ConfigurationBase): @memoize def wms_layer(self): from mapproxy.service.wms import WMSGroupLayer layers = [] this_layer = None if 'layers' in self.conf: layers_conf = self.conf['layers'] for layer_conf in layers_conf: lyr = WMSLayerConfiguration(layer_conf, self.context).wms_layer() if lyr: layers.append(lyr) if 'sources' in self.conf or 'legendurl' in self.conf: this_layer = LayerConfiguration(self.conf, self.context).wms_layer() if not layers and not this_layer: return None if not layers: layer = this_layer else: layer = WMSGroupLayer(name=self.conf.get('name'), title=self.conf.get('title'), this=this_layer, layers=layers, md=self.conf.get('md')) return layer def cache_source_names(context, cache): """ Return all sources for a cache, even if a caches uses another cache. """ source_names = [] for src in context.caches[cache].conf['sources']: if src in context.caches and src not in context.sources: source_names.extend(cache_source_names(context, src)) else: source_names.append(src) return source_names class LayerConfiguration(ConfigurationBase): @memoize def wms_layer(self): from mapproxy.service.wms import WMSLayer sources = [] fi_sources = [] lg_sources = [] lg_sources_configured = False if self.conf.get('legendurl'): legend_url = self.conf['legendurl'] lg_sources.append(WMSSourceConfiguration.static_legend_source(legend_url, self.context)) lg_sources_configured = True for source_name in self.conf.get('sources', []): fi_source_names = [] lg_source_names = [] if source_name in self.context.caches: map_layer = self.context.caches[source_name].map_layer() fi_source_names = cache_source_names(self.context, source_name) lg_source_names = cache_source_names(self.context, source_name) elif source_name in self.context.sources: source_conf = self.context.sources[source_name] if not source_conf.supports_meta_tiles: raise ConfigurationError('source "%s" of layer "%s" does not support un-tiled access' % (source_name, self.conf.get('name'))) map_layer = source_conf.source() fi_source_names = [source_name] lg_source_names = [source_name] else: raise ConfigurationError('source/cache "%s" not found' % source_name) if map_layer: sources.append(map_layer) for fi_source_name in fi_source_names: if fi_source_name not in self.context.sources: continue if not hasattr(self.context.sources[fi_source_name], 'fi_source'): continue fi_source = self.context.sources[fi_source_name].fi_source() if fi_source: fi_sources.append(fi_source) if not lg_sources_configured: for lg_source_name in lg_source_names: if lg_source_name not in self.context.sources: continue if not hasattr(self.context.sources[lg_source_name], 'lg_source'): continue lg_source = self.context.sources[lg_source_name].lg_source() if lg_source: lg_sources.append(lg_source) res_range = resolution_range(self.conf) layer = WMSLayer(self.conf.get('name'), self.conf.get('title'), sources, fi_sources, lg_sources, res_range=res_range, md=self.conf.get('md')) return layer @memoize def dimensions(self): from mapproxy.layer import Dimension dimensions = {} for dimension, conf in iteritems(self.conf.get('dimensions', {})): values = [str(val) for val in conf.get('values', ['default'])] default = conf.get('default', values[-1]) dimensions[dimension.lower()] = Dimension(dimension, values, default=default) return dimensions @memoize def tile_layers(self, grid_name_as_path=False): from mapproxy.service.tile import TileLayer from mapproxy.cache.dummy import DummyCache sources = [] if 'tile_sources' in self.conf: sources = self.conf['tile_sources'] else: for source_name in self.conf.get('sources', []): # we only support caches for tiled access... if not source_name in self.context.caches: if source_name in self.context.sources: src_conf = self.context.sources[source_name].conf # but we ignore debug layers for convenience if src_conf['type'] == 'debug': continue # and WMS layers with map: False (i.e. FeatureInfo only sources) if src_conf['type'] == 'wms' and src_conf.get('wms_opts', {}).get('map', True) == False: continue return [] sources.append(source_name) if len(sources) > 1: return [] dimensions = self.dimensions() tile_layers = [] for cache_name in sources: for grid, extent, cache_source in self.context.caches[cache_name].caches(): if dimensions and not isinstance(cache_source.cache, DummyCache): # caching of dimension layers is not supported yet raise ConfigurationError( "caching of dimension layer (%s) is not supported yet." " need to `disable_storage: true` on %s cache" % (self.conf['name'], cache_name) ) md = {} md['title'] = self.conf['title'] md['name'] = self.conf['name'] md['grid_name'] = grid.name if grid_name_as_path: md['name_path'] = (md['name'], md['grid_name']) else: md['name_path'] = (md['name'], grid.srs.srs_code.replace(':', '').upper()) md['name_internal'] = md['name_path'][0] + '_' + md['name_path'][1] md['format'] = self.context.caches[cache_name].image_opts().format md['cache_name'] = cache_name md['extent'] = extent tile_layers.append(TileLayer(self.conf['name'], self.conf['title'], md, cache_source, dimensions=dimensions)) return tile_layers def fi_xslt_transformers(conf, context): from mapproxy.featureinfo import XSLTransformer, has_xslt_support fi_transformers = {} fi_xslt = conf.get('featureinfo_xslt') if fi_xslt: if not has_xslt_support: raise ValueError('featureinfo_xslt requires lxml. Please install.') for info_type, fi_xslt in fi_xslt.items(): fi_xslt = context.globals.abspath(fi_xslt) fi_transformers[info_type] = XSLTransformer(fi_xslt) return fi_transformers def extents_for_srs(bbox_srs): from mapproxy.layer import DefaultMapExtent, MapExtent from mapproxy.srs import SRS extents = {} for srs in bbox_srs: if isinstance(srs, str): bbox = DefaultMapExtent() else: srs, bbox = srs['srs'], srs['bbox'] bbox = MapExtent(bbox, SRS(srs)) extents[srs] = bbox return extents class ServiceConfiguration(ConfigurationBase): def __init__(self, conf, context): if 'wms' in conf: if conf['wms'] is None: conf['wms'] = {} if 'md' not in conf['wms']: conf['wms']['md'] = {'title': 'MapProxy WMS'} ConfigurationBase.__init__(self, conf, context) def services(self): services = [] ows_services = [] for service_name, service_conf in iteritems(self.conf): creator = getattr(self, service_name + '_service', None) if not creator: raise ValueError('unknown service: %s' % service_name) new_services = creator(service_conf or {}) # a creator can return a list of services... if not isinstance(new_services, (list, tuple)): new_services = [new_services] for new_service in new_services: if getattr(new_service, 'service', None): ows_services.append(new_service) else: services.append(new_service) if ows_services: from mapproxy.service.ows import OWSServer services.append(OWSServer(ows_services)) return services def tile_layers(self, conf, use_grid_names=False): layers = odict() for layer_name, layer_conf in iteritems(self.context.layers): for tile_layer in layer_conf.tile_layers(grid_name_as_path=use_grid_names): if not tile_layer: continue if use_grid_names: layers[tile_layer.md['name_path']] = tile_layer else: layers[tile_layer.md['name_internal']] = tile_layer return layers def kml_service(self, conf): from mapproxy.service.kml import KMLServer md = self.context.services.conf.get('wms', {}).get('md', {}).copy() md.update(conf.get('md', {})) max_tile_age = self.context.globals.get_value('tiles.expires_hours') max_tile_age *= 60 * 60 # seconds use_grid_names = conf.get('use_grid_names', False) layers = self.tile_layers(conf, use_grid_names=use_grid_names) return KMLServer(layers, md, max_tile_age=max_tile_age, use_dimension_layers=use_grid_names) def tms_service(self, conf): from mapproxy.service.tile import TileServer md = self.context.services.conf.get('wms', {}).get('md', {}).copy() md.update(conf.get('md', {})) max_tile_age = self.context.globals.get_value('tiles.expires_hours') max_tile_age *= 60 * 60 # seconds origin = conf.get('origin') use_grid_names = conf.get('use_grid_names', False) layers = self.tile_layers(conf, use_grid_names=use_grid_names) return TileServer(layers, md, max_tile_age=max_tile_age, use_dimension_layers=use_grid_names, origin=origin) def wmts_service(self, conf): from mapproxy.service.wmts import WMTSServer, WMTSRestServer md = self.context.services.conf.get('wms', {}).get('md', {}).copy() md.update(conf.get('md', {})) layers = self.tile_layers(conf, use_grid_names=True) kvp = conf.get('kvp') restful = conf.get('restful') max_tile_age = self.context.globals.get_value('tiles.expires_hours') max_tile_age *= 60 * 60 # seconds if kvp is None and restful is None: kvp = restful = True services = [] if kvp: services.append(WMTSServer(layers, md, max_tile_age=max_tile_age)) if restful: template = conf.get('restful_template') if template and '{{' in template: # TODO remove warning in 1.6 log.warn("double braces in WMTS restful_template are deprecated {{x}} -> {x}") services.append(WMTSRestServer(layers, md, template=template, max_tile_age=max_tile_age)) return services def wms_service(self, conf): from mapproxy.service.wms import WMSServer from mapproxy.request.wms import Version md = conf.get('md', {}) inspire_md = conf.get('inspire_md', {}) tile_layers = self.tile_layers(conf) attribution = conf.get('attribution') strict = self.context.globals.get_value('strict', conf, global_key='wms.strict') on_source_errors = self.context.globals.get_value('on_source_errors', conf, global_key='wms.on_source_errors') root_layer = self.context.wms_root_layer.wms_layer() if not root_layer: raise ConfigurationError("found no WMS layer") if not root_layer.title: # set title of root layer to WMS title root_layer.title = md.get('title') concurrent_layer_renderer = self.context.globals.get_value( 'concurrent_layer_renderer', conf, global_key='wms.concurrent_layer_renderer') image_formats_names = self.context.globals.get_value('image_formats', conf, global_key='wms.image_formats') image_formats = odict() for format in image_formats_names: opts = self.context.globals.image_options.image_opts({}, format) if opts.format in image_formats: log.warn('duplicate mime-type for WMS image_formats: "%s" already configured, will use last format', opts.format) image_formats[opts.format] = opts info_types = conf.get('featureinfo_types') srs = self.context.globals.get_value('srs', conf, global_key='wms.srs') self.context.globals.base_config.wms.srs = srs srs_extents = extents_for_srs(conf.get('bbox_srs', [])) versions = conf.get('versions') if versions: versions = sorted([Version(v) for v in versions]) versions = conf.get('versions') if versions: versions = sorted([Version(v) for v in versions]) max_output_pixels = self.context.globals.get_value('max_output_pixels', conf, global_key='wms.max_output_pixels') if isinstance(max_output_pixels, list): max_output_pixels = max_output_pixels[0] * max_output_pixels[1] max_tile_age = self.context.globals.get_value('tiles.expires_hours') max_tile_age *= 60 * 60 # seconds server = WMSServer(root_layer, md, attribution=attribution, image_formats=image_formats, info_types=info_types, srs=srs, tile_layers=tile_layers, strict=strict, on_error=on_source_errors, concurrent_layer_renderer=concurrent_layer_renderer, max_output_pixels=max_output_pixels, srs_extents=srs_extents, max_tile_age=max_tile_age, versions=versions, inspire_md=inspire_md, ) server.fi_transformers = fi_xslt_transformers(conf, self.context) return server def demo_service(self, conf): from mapproxy.service.demo import DemoServer services = list(self.context.services.conf.keys()) md = self.context.services.conf.get('wms', {}).get('md', {}).copy() md.update(conf.get('md', {})) layers = odict() for layer_name, layer_conf in iteritems(self.context.layers): lyr = layer_conf.wms_layer() if lyr: layers[layer_name] = lyr image_formats = self.context.globals.get_value('image_formats', conf, global_key='wms.image_formats') srs = self.context.globals.get_value('srs', conf, global_key='wms.srs') tms_conf = self.context.services.conf.get('tms', {}) or {} use_grid_names = tms_conf.get('use_grid_names', False) tile_layers = self.tile_layers(tms_conf, use_grid_names=use_grid_names) # WMTS restful template wmts_conf = self.context.services.conf.get('wmts', {}) or {} from mapproxy.service.wmts import WMTSRestServer if wmts_conf: restful_template = wmts_conf.get('restful_template', WMTSRestServer.default_template) else: restful_template = WMTSRestServer.default_template if 'wmts' in self.context.services.conf: kvp = wmts_conf.get('kvp') restful = wmts_conf.get('restful') if kvp is None and restful is None: kvp = restful = True if kvp: services.append('wmts_kvp') if restful: services.append('wmts_restful') if 'wms' in self.context.services.conf: versions = self.context.services.conf['wms'].get('versions', ['1.1.1']) if '1.1.1' in versions: # demo service only supports 1.1.1, use wms_111 as an indicator services.append('wms_111') return DemoServer(layers, md, tile_layers=tile_layers, image_formats=image_formats, srs=srs, services=services, restful_template=restful_template) def load_configuration(mapproxy_conf, seed=False, ignore_warnings=True, renderd=False): conf_base_dir = os.path.abspath(os.path.dirname(mapproxy_conf)) # A configuration is checked/validated four times, each step has a different # focus and returns different errors. The steps are: # 1. YAML loading: checks YAML syntax like tabs vs. space, indention errors, etc. # 2. Options: checks all options agains the spec and validates their types, # e.g is disable_storage a bool, is layers a list, etc. # 3. References: checks if all referenced caches, sources and grids exist # 4. Initialization: creates all MapProxy objects, returns on first error try: conf_dict = load_configuration_file([os.path.basename(mapproxy_conf)], conf_base_dir) except YAMLError as ex: raise ConfigurationError(ex) errors, informal_only = validate_options(conf_dict) for error in errors: log.warn(error) if not informal_only or (errors and not ignore_warnings): raise ConfigurationError('invalid configuration') errors = validate_references(conf_dict) for error in errors: log.warn(error) return ProxyConfiguration(conf_dict, conf_base_dir=conf_base_dir, seed=seed, renderd=renderd) def load_configuration_file(files, working_dir): """ Return configuration dict from imported files """ # record all config files with timestamp for reloading conf_dict = {'__config_files__': {}} for conf_file in files: conf_file = os.path.normpath(os.path.join(working_dir, conf_file)) log.info('reading: %s' % conf_file) current_dict = load_yaml_file(conf_file) conf_dict['__config_files__'][os.path.abspath(conf_file)] = os.path.getmtime(conf_file) if 'base' in current_dict: current_working_dir = os.path.dirname(conf_file) base_files = current_dict.pop('base') if isinstance(base_files, string_type): base_files = [base_files] imported_dict = load_configuration_file(base_files, current_working_dir) current_dict = merge_dict(current_dict, imported_dict) conf_dict = merge_dict(conf_dict, current_dict) return conf_dict def merge_dict(conf, base): """ Return `base` dict with values from `conf` merged in. """ for k, v in iteritems(conf): if k not in base: base[k] = v else: if isinstance(base[k], dict): merge_dict(v, base[k]) else: base[k] = v return base def parse_color(color): """ >>> parse_color((100, 12, 55)) (100, 12, 55) >>> parse_color('0xff0530') (255, 5, 48) >>> parse_color('#FF0530') (255, 5, 48) >>> parse_color('#FF053080') (255, 5, 48, 128) """ if isinstance(color, (list, tuple)) and 3 <= len(color) <= 4: return tuple(color) if not isinstance(color, string_type): raise ValueError('color needs to be a tuple/list or 0xrrggbb/#rrggbb(aa) string, got %r' % color) if color.startswith('0x'): color = color[2:] if color.startswith('#'): color = color[1:] r, g, b = map(lambda x: int(x, 16), [color[:2], color[2:4], color[4:6]]) if len(color) == 8: a = int(color[6:8], 16) return r, g, b, a return r, g, b mapproxy-1.11.0/mapproxy/config/spec.py000066400000000000000000000403061320454472400201110ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function import datetime from mapproxy.util.ext.dictspec.validator import validate, ValidationError from mapproxy.util.ext.dictspec.spec import one_of, anything, number from mapproxy.util.ext.dictspec.spec import recursive, required, type_spec, combined from mapproxy.compat import string_type def validate_options(conf_dict): """ Validate `conf_dict` agains mapproxy.yaml spec. Returns tuple with a list of errors and a bool. The list is empty when no errors where found. The bool is True when the errors are informal and not critical. """ try: validate(mapproxy_yaml_spec, conf_dict) except ValidationError as ex: return ex.errors, ex.informal_only else: return [], True coverage = recursive({ 'polygons': str(), 'polygons_srs': str(), 'bbox': one_of(str(), [number()]), 'bbox_srs': str(), 'ogr_datasource': str(), 'ogr_where': str(), 'ogr_srs': str(), 'datasource': one_of(str(), [number()]), 'where': str(), 'srs': str(), 'expire_tiles': str(), 'union': [recursive()], 'difference': [recursive()], 'intersection': [recursive()], 'clip': bool(), }) image_opts = { 'mode': str(), 'colors': number(), 'transparent': bool(), 'resampling_method': str(), 'format': str(), 'encoding_options': { anything(): anything() }, 'merge_method': str(), } http_opts = { 'method': str(), 'client_timeout': number(), 'ssl_no_cert_checks': bool(), 'ssl_ca_certs': str(), 'headers': { anything(): str() }, } mapserver_opts = { 'binary': str(), 'working_dir': str(), } scale_hints = { 'max_scale': number(), 'min_scale': number(), 'max_res': number(), 'min_res': number(), } source_commons = combined( scale_hints, { 'concurrent_requests': int(), 'coverage': coverage, 'seed_only': bool(), } ) riak_node = { 'host': str(), 'pb_port': number(), 'http_port': number(), } cache_types = { 'file': { 'directory_layout': str(), 'use_grid_names': bool(), 'directory': str(), 'tile_lock_dir': str(), }, 'sqlite': { 'directory': str(), 'sqlite_timeout': number(), 'sqlite_wal': bool(), 'tile_lock_dir': str(), }, 'mbtiles': { 'filename': str(), 'sqlite_timeout': number(), 'sqlite_wal': bool(), 'tile_lock_dir': str(), }, 'geopackage': { 'filename': str(), 'directory': str(), 'tile_lock_dir': str(), 'table_name': str(), 'levels': bool(), }, 'couchdb': { 'url': str(), 'db_name': str(), 'tile_metadata': { anything(): anything() }, 'tile_id': str(), 'tile_lock_dir': str(), }, 's3': { 'bucket_name': str(), 'directory_layout': str(), 'directory': str(), 'profile_name': str(), 'tile_lock_dir': str(), }, 'riak': { 'nodes': [riak_node], 'protocol': one_of('pbc', 'http', 'https'), 'bucket': str(), 'default_ports': { 'pb': number(), 'http': number(), }, 'secondary_index': bool(), 'tile_lock_dir': str(), }, 'redis': { 'host': str(), 'port': int(), 'db': int(), 'prefix': str(), 'default_ttl': int(), }, 'compact': { 'directory': str(), required('version'): number(), 'tile_lock_dir': str(), }, } on_error = { anything(): { required('response'): one_of([int], str), 'cache': bool, } } inspire_md = { 'linked': { required('metadata_url'): { required('url'): str, required('media_type'): str, }, required('languages'): { required('default'): str, }, }, 'embedded': { required('resource_locators'): [{ required('url'): str, required('media_type'): str, }], required('temporal_reference'): { 'date_of_publication': one_of(str, datetime.date), 'date_of_creation': one_of(str, datetime.date), 'date_of_last_revision': one_of(str, datetime.date), }, required('conformities'): [{ 'title': string_type, 'uris': [str], 'date_of_publication': one_of(str, datetime.date), 'date_of_creation': one_of(str, datetime.date), 'date_of_last_revision': one_of(str, datetime.date), required('resource_locators'): [{ required('url'): str, required('media_type'): str, }], required('degree'): str, }], required('metadata_points_of_contact'): [{ 'organisation_name': string_type, 'email': str, }], required('mandatory_keywords'): [str], 'keywords': [{ required('title'): string_type, 'date_of_publication': one_of(str, datetime.date), 'date_of_creation': one_of(str, datetime.date), 'date_of_last_revision': one_of(str, datetime.date), 'uris': [str], 'resource_locators': [{ required('url'): str, required('media_type'): str, }], required('keyword_value'): string_type, }], required('metadata_date'): one_of(str, datetime.date), 'metadata_url': { required('url'): str, required('media_type'): str, }, required('languages'): { required('default'): str, }, }, } wms_130_layer_md = { 'abstract': string_type, 'keyword_list': [ { 'vocabulary': string_type, 'keywords': [string_type], } ], 'attribution': { 'title': string_type, 'url': str, 'logo': { 'url': str, 'width': int, 'height': int, 'format': string_type, } }, 'identifier': [ { 'url': str, 'name': string_type, 'value': string_type, } ], 'metadata': [ { 'url': str, 'type': str, 'format': str, }, ], 'data': [ { 'url': str, 'format': str, } ], 'feature_list': [ { 'url': str, 'format': str, } ], } grid_opts = { 'base': str(), 'name': str(), 'srs': str(), 'bbox': one_of(str(), [number()]), 'bbox_srs': str(), 'num_levels': int(), 'res': [number()], 'res_factor': one_of(number(), str()), 'max_res': number(), 'min_res': number(), 'stretch_factor': number(), 'max_shrink_factor': number(), 'align_resolutions_with': str(), 'origin': str(), 'tile_size': [int()], 'threshold_res': [number()], } ogc_service_md = { 'title': string_type, 'abstract': string_type, 'online_resource': string_type, 'contact': anything(), 'fees': string_type, 'access_constraints': string_type, 'keyword_list': [ { 'vocabulary': string_type, 'keywords': [string_type], } ], } band_source = { required('source'): str(), required('band'): int, 'factor': number(), } band_sources = { 'r': [band_source], 'g': [band_source], 'b': [band_source], 'a': [band_source], 'l': [band_source], } mapproxy_yaml_spec = { '__config_files__': anything(), # only used internaly 'globals': { 'image': { 'resampling_method': 'method', 'paletted': bool(), 'stretch_factor': number(), 'max_shrink_factor': number(), 'jpeg_quality': number(), 'formats': { anything(): image_opts, }, 'font_dir': str(), 'merge_method': str(), }, 'http': combined( http_opts, { 'access_control_allow_origin': one_of(str(), {}), } ), 'cache': { 'base_dir': str(), 'lock_dir': str(), 'tile_lock_dir': str(), 'meta_size': [number()], 'meta_buffer': number(), 'bulk_meta_tiles': bool(), 'max_tile_limit': number(), 'minimize_meta_requests': bool(), 'concurrent_tile_creators': int(), 'link_single_color_images': bool(), 's3': { 'bucket_name': str(), 'profile_name': str(), }, }, 'grid': { 'tile_size': [int()], }, 'srs': { 'axis_order_ne': [str()], 'axis_order_en': [str()], 'proj_data_dir': str(), }, 'tiles': { 'expires_hours': number(), }, 'mapserver': mapserver_opts, 'renderd': { 'address': str(), } }, 'grids': { anything(): grid_opts, }, 'caches': { anything(): { required('sources'): one_of([string_type], band_sources), 'name': str(), 'grids': [str()], 'cache_dir': str(), 'meta_size': [number()], 'meta_buffer': number(), 'bulk_meta_tiles': bool(), 'minimize_meta_requests': bool(), 'concurrent_tile_creators': int(), 'disable_storage': bool(), 'format': str(), 'image': image_opts, 'request_format': str(), 'use_direct_from_level': number(), 'use_direct_from_res': number(), 'link_single_color_images': bool(), 'watermark': { 'text': string_type, 'font_size': number(), 'color': one_of(str(), [number()]), 'opacity': number(), 'spacing': str(), }, 'cache': type_spec('type', cache_types) } }, 'services': { 'demo': {}, 'kml': { 'use_grid_names': bool(), }, 'tms': { 'use_grid_names': bool(), 'origin': str(), }, 'wmts': { 'kvp': bool(), 'restful': bool(), 'restful_template': str(), 'md': ogc_service_md, }, 'wms': { 'srs': [str()], 'bbox_srs': [one_of(str(), {'bbox': [number()], 'srs': str()})], 'image_formats': [str()], 'attribution': { 'text': string_type, }, 'featureinfo_types': [str()], 'featureinfo_xslt': { anything(): str() }, 'on_source_errors': str(), 'max_output_pixels': one_of(number(), [number()]), 'strict': bool(), 'md': ogc_service_md, 'inspire_md': type_spec('type', inspire_md), 'versions': [str()], }, }, 'sources': { anything(): type_spec('type', { 'wms': combined(source_commons, { 'wms_opts': { 'version': str(), 'map': bool(), 'featureinfo': bool(), 'legendgraphic': bool(), 'legendurl': str(), 'featureinfo_format': str(), 'featureinfo_xslt': str(), }, 'image': combined(image_opts, { 'opacity':number(), 'transparent_color': one_of(str(), [number()]), 'transparent_color_tolerance': number(), }), 'supported_formats': [str()], 'supported_srs': [str()], 'http': http_opts, 'forward_req_params': [str()], required('req'): { required('url'): str(), anything(): anything() } }), 'mapserver': combined(source_commons, { 'wms_opts': { 'version': str(), 'map': bool(), 'featureinfo': bool(), 'legendgraphic': bool(), 'legendurl': str(), 'featureinfo_format': str(), 'featureinfo_xslt': str(), }, 'image': combined(image_opts, { 'opacity':number(), 'transparent_color': one_of(str(), [number()]), 'transparent_color_tolerance': number(), }), 'supported_formats': [str()], 'supported_srs': [str()], 'forward_req_params': [str()], required('req'): { required('map'): str(), anything(): anything() }, 'mapserver': mapserver_opts, }), 'tile': combined(source_commons, { required('url'): str(), 'transparent': bool(), 'image': image_opts, 'grid': str(), 'request_format': str(), 'origin': str(), # TODO: remove with 1.5 'http': http_opts, 'on_error': on_error, }), 'mapnik': combined(source_commons, { required('mapfile'): str(), 'transparent': bool(), 'image': image_opts, 'layers': one_of(str(), [str()]), 'use_mapnik2': bool(), 'scale_factor': number(), }), 'arcgis': combined(source_commons, { required('req'): { required('url'): str(), 'dpi': int(), 'layers': str(), 'transparent': bool(), 'time': str() }, 'opts': { 'featureinfo': bool(), 'featureinfo_tolerance': number(), 'featureinfo_return_geometries': bool(), }, 'supported_srs': [str()], 'http': http_opts }), 'debug': { }, }) }, 'layers': one_of( { anything(): combined(scale_hints, { 'sources': [string_type], required('title'): string_type, 'legendurl': str(), 'md': wms_130_layer_md, }) }, recursive([combined(scale_hints, { 'sources': [string_type], 'tile_sources': [string_type], 'name': str(), required('title'): string_type, 'legendurl': str(), 'layers': recursive(), 'md': wms_130_layer_md, 'dimensions': { anything(): { required('values'): [one_of(string_type, float, int)], 'default': one_of(string_type, float, int), } } })]) ), # `parts` can be used for partial configurations that are referenced # from other sections (e.g. coverages, dimensions, etc.) 'parts': anything(), } if __name__ == '__main__': import sys import yaml for f in sys.argv[1:]: data = yaml.load(open(f)) try: validate(mapproxy_yaml_spec, data) except ValidationError as ex: for err in ex.errors: print('%s: %s' % (f, err)) mapproxy-1.11.0/mapproxy/config/validator.py000066400000000000000000000212571320454472400211500ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2015 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os.path from mapproxy.compat import string_type, iteritems import logging log = logging.getLogger('mapproxy.config') import mapproxy.config.defaults TAGGED_SOURCE_TYPES = [ 'wms', 'mapserver', 'mapnik' ] def validate_references(conf_dict): validator = Validator(conf_dict) return validator.validate() class Validator(object): def __init__(self, conf_dict): self.sources_conf = conf_dict.get('sources', {}) self.caches_conf = conf_dict.get('caches', {}) self.layers_conf = conf_dict.get('layers') self.services_conf = conf_dict.get('services') self.grids_conf = conf_dict.get('grids') self.globals_conf = conf_dict.get('globals') self.errors = [] self.known_grids = set(mapproxy.config.defaults.grids.keys()) if self.grids_conf: self.known_grids.update(self.grids_conf.keys()) def validate(self): if not self.layers_conf: self.errors.append("Missing layers section") if isinstance(self.layers_conf, dict): return [] if not self.services_conf: self.errors.append("Missing services section") if len(self.errors) > 0: return self.errors for layer in self.layers_conf: self._validate_layer(layer) return self.errors def _validate_layer(self, layer): layer_sources = layer.get('sources', []) tile_sources = layer.get('tile_sources', []) child_layers = layer.get('layers', []) if not layer_sources and not child_layers and not tile_sources: self.errors.append( "Missing sources for layer '%s'" % layer.get('name') ) for child_layer in child_layers: self._validate_layer(child_layer) for source in layer_sources: if source in self.caches_conf: self._validate_cache(source, self.caches_conf[source]) continue if source in self.sources_conf: source, layers = self._split_tagged_source(source) self._validate_source(source, self.sources_conf[source], layers) continue self.errors.append( "Source '%s' for layer '%s' not in cache or source section" % ( source, layer['name'] ) ) for source in tile_sources: if source in self.caches_conf: self._validate_cache(source, self.caches_conf[source]) continue self.errors.append( "Tile source '%s' for layer '%s' not in cache section" % ( source, layer['name'] ) ) def _split_tagged_source(self, source_name): layers = None if ':' in str(source_name): source_name, layers = str(source_name).split(':', 1) layers = layers.split(',') if layers is not None else None return source_name, layers def _validate_source(self, name, source, layers): source_type = source.get('type') if source_type == 'wms': self._validate_wms_source(name, source, layers) if source_type == 'mapserver': self._validate_mapserver_source(name, source, layers) if source_type == 'mapnik': self._validate_mapnik_source(name, source, layers) def _validate_wms_source(self, name, source, layers): if source['req'].get('layers') is None and layers is None: self.errors.append("Missing 'layers' for source '%s'" % ( name )) if source['req'].get('layers') is not None and layers is not None: self._validate_tagged_layer_source( name, source['req'].get('layers'), layers ) def _validate_mapserver_source(self, name, source, layers): mapserver = source.get('mapserver') if mapserver is None: if ( not self.globals_conf or not self.globals_conf.get('mapserver') or not self.globals_conf['mapserver'].get('binary') ): self.errors.append("Missing mapserver binary for source '%s'" % ( name )) elif not os.path.isfile(self.globals_conf['mapserver']['binary']): self.errors.append("Could not find mapserver binary (%s)" % ( self.globals_conf['mapserver'].get('binary') )) elif mapserver is None or not source['mapserver'].get('binary'): self.errors.append("Missing mapserver binary for source '%s'" % ( name )) elif not os.path.isfile(source['mapserver']['binary']): self.errors.append("Could not find mapserver binary (%s)" % ( source['mapserver']['binary'] )) if source['req'].get('layers') and layers is not None: self._validate_tagged_layer_source( name, source['req'].get('layers'), layers ) def _validate_mapnik_source(self, name, source, layers): if source.get('layers') and layers is not None: self._validate_tagged_layer_source(name, source.get('layers'), layers) def _validate_tagged_layer_source(self, name, supported_layers, requested_layers): if isinstance(supported_layers, string_type): supported_layers = [supported_layers] if not set(requested_layers).issubset(set(supported_layers)): self.errors.append( "Supported layers for source '%s' are '%s' but tagged source requested " "layers '%s'" % ( name, ', '.join(supported_layers), ', '.join(requested_layers) )) def _validate_cache(self, name, cache): if isinstance(cache.get('sources', []), dict): self._validate_bands(name, set(cache['sources'].keys())) for band, confs in iteritems(cache['sources']): for conf in confs: band_source = conf['source'] self._validate_cache_source(name, band_source) else: for cache_source in cache.get('sources', []): self._validate_cache_source(name, cache_source) for grid in cache.get('grids', []): if grid not in self.known_grids: self.errors.append( "Grid '%s' for cache '%s' not found in config" % ( grid, name ) ) def _validate_cache_source(self, cache_name, source_name): source_name, layers = self._split_tagged_source(source_name) if self.sources_conf and source_name in self.sources_conf: source = self.sources_conf.get(source_name) if ( layers is not None and source.get('type') not in TAGGED_SOURCE_TYPES ): self.errors.append( "Found tagged source '%s' in cache '%s' but tagged sources only " "supported for '%s' sources" % ( source_name, cache_name, ', '.join(TAGGED_SOURCE_TYPES) ) ) return self._validate_source(source_name, source, layers) return if self.caches_conf and source_name in self.caches_conf: self._validate_cache(source_name, self.caches_conf[source_name]) return self.errors.append( "Source '%s' for cache '%s' not found in config" % ( source_name, cache_name ) ) def _validate_bands(self, cache_name, bands): if 'l' in bands and len(bands) > 1: self.errors.append( "Cannot combine 'l' band with bands in cache '%s'" % ( cache_name ) ) mapproxy-1.11.0/mapproxy/config_template/000077500000000000000000000000001320454472400204755ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/config_template/__init__.py000066400000000000000000000007031320454472400226060ustar00rootroot00000000000000try: from paste.util.template import paste_script_template_renderer from paste.script.templates import Template #, var class PasterConfigurationTemplate(Template): _template_dir = 'paster' summary = "MapProxy configuration template" vars = [ # var('varname', 'help text', default='value'), ] template_renderer = staticmethod(paste_script_template_renderer) except ImportError: passmapproxy-1.11.0/mapproxy/config_template/base_config/000077500000000000000000000000001320454472400227345ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/config_template/base_config/config.wsgi000066400000000000000000000006001320454472400250700ustar00rootroot00000000000000# WSGI module for use with Apache mod_wsgi or gunicorn # # uncomment the following lines for logging # # create a log.ini with `mapproxy-util create -t log-ini` # from logging.config import fileConfig # import os.path # fileConfig(r'%(here)s/log.ini', {'here': os.path.dirname(__file__)}) from mapproxy.wsgiapp import make_wsgi_app application = make_wsgi_app(r'%(mapproxy_conf)s') mapproxy-1.11.0/mapproxy/config_template/base_config/full_example.yaml000066400000000000000000000427311320454472400263040ustar00rootroot00000000000000# ##################################################################### # MapProxy example configuration # ##################################################################### # # This is _not_ a runnable configuration, but it contains most # available options in meaningful combinations. # # Use this file in addition to the documentation to see where and how # things can be configured. services: demo: kml: # use the actual name of the grid as the grid identifier # instead of the SRS code, e.g. /kml/mylayer/mygrid/ use_grid_names: true tms: # use the actual name of the grid as the grid identifier # instead of the SRS code, e.g. /tms/1.0.0/mylayer/mygrid/ use_grid_names: true # sets the tile origin to the north west corner, only works for # tileservice at /tiles. TMS at /tms/1.0.0/ will still use # south west as defined by the standard origin: 'nw' wmts: # use restful access to WMTS restful: true # this is the default template for MapProxy restful_template: '/{Layer}/{TileMatrixSet}/{TileMatrix}/{TileCol}/{TileRow}.{Format}' # and also allow KVP requests kvp: true md: # metadata used in capabilities documents for WMTS # if the md option is not set, the metadata of the WMS will be used title: MapProxy WMS Proxy abstract: This is the fantastic MapProxy. online_resource: http://mapproxy.org/ contact: person: Your Name Here position: Technical Director organization: address: Fakestreet 123 city: Somewhere postcode: 12345 country: Germany phone: +49(0)000-000000-0 fax: +49(0)000-000000-0 email: info@omniscale.de # multiline strings are possible with the right indention access_constraints: Insert license and copyright information for this service. fees: 'None' wms: # only offer WMS 1.1.1 versions: ['1.1.1'] # supported SRS for this WMS srs: ['EPSG:4326', 'EPSG:900913', 'EPSG:25832'] # force the layer extents (BBOX) to be displayed in this SRS bbox_srs: ['EPSG:4326'] # limit the supported image formats. image_formats: ['image/jpeg', 'image/png', 'image/gif', 'image/GeoTIFF', 'image/tiff'] # add attribution text in the lower-right corner. attribution: text: '(c) Omniscale' # return an OGC service exception when one or more sources return errors # or no response at all (e.g. timeout) on_source_errors: raise # maximum output size for a WMS requests in pixel, default is 4000 x 4000 # compares the product, eg. 3000x1000 pixel < 2000x2000 pixel and is still # permitted max_output_pixels: [2000, 2000] # some WMS clients do not send all required parameters in feature info # requests, MapProxy ignores these errors unless you set strict to true. strict: true # list of feature info types the server should offer featureinfo_types: ['text', 'html', 'xml'] md: # metadata used in capabilities documents title: MapProxy WMS Proxy abstract: This is the fantastic MapProxy. online_resource: http://mapproxy.org/ contact: person: Your Name Here position: Technical Director organization: address: Fakestreet 123 city: Somewhere postcode: 12345 country: Germany phone: +49(0)000-000000-0 fax: +49(0)000-000000-0 email: info@omniscale.de # multiline strings are possible with the right indention access_constraints: Insert license and copyright information for this service. fees: 'None' layers: # layer with minimal options - name: osm title: Omniscale OSM WMS - osm.omniscale.net sources: [osm_cache] # layer with multiple sources - name: merged_layer title: Omniscale OSM WMS - osm.omniscale.net sources: [osm_cache, osm_cache_full_example] # these layers supports the GetLegendGraphicRequest - name: wms_legend title: Layer with legendgraphic support # legend graphics will work for cache sources and direct sources sources: [legend_wms] - name: wms_legend_static title: Layer with a static LegendURL # MapProxy ignores the legends from the sources of this layer # if you configure a legendurl here legendurl: http://localhost:42423/staticlegend_layer.png # local legend images are supported as well # legendurl: file://relative/staticlegend_layer.png # legendurl: file:///absulute/staticlegend_layer.png sources: [legend_wms] # this layer uses extended metadata - name: md_layer title: WMS layer with extended metadata sources: [osm_cache] md: abstract: Some abstract keyword_list: - vocabulary: Name of the vocabulary keywords: [keyword1, keyword2] - vocabulary: Name of another vocabulary keywords: [keyword1, keyword2] - keywords: ["keywords without vocabulary"] attribution: title: My attribution title url: http://example.org/ logo: url: http://example.org/logo.jpg width: 100 height: 100 format: image/jpeg identifier: - url: http://example.org/ name: HKU1234 value: Some value metadata: - url: http://example.org/metadata2.xml type: INSPIRE format: application/xml - url: http://example.org/metadata2.xml type: ISO19115:2003 format: application/xml data: - url: http://example.org/datasets/test.shp format: application/octet-stream - url: http://example.org/datasets/test.gml format: text/xml; subtype=gml/3.2.1 feature_list: - url: http://example.org/datasets/test.pdf format: application/pdf # defines a layer with a min and max resolution. requests outside of the # resolution result in a blank image - name: resolution title: Cache Layer with min/max resolution # xx_res in meter/pixel min_res: 10000 max_res: 10 sources: [osm_cache] # nested/grouped layers # 'Group Layer' has no name and GIS clients should display all sub-layers # in this group. # layer2 combines both layer2a and layer2b - title: Group Layer layers: - name: layer1 title: layer 1 sources: [osm_cache] - name: layer2 title: layer 2 layers: - name: layer2a title: layer 2a sources: [osm_cache] - name: layer2b title: layer 2b sources: [osm_cache] # the childs of this group layer all use the same WMS. # reference the layer as tagged source - title: Example with tagged sources layers: - name: landusage title: Landusage sources: ['wms_source:landusage'] - name: roads title: Roads and railways sources: ['wms_source:roads,railways'] - name: buildings title: Buildings sources: ['wms_source:buildings'] # this layer will be reprojected from the source - name: osm_utm title: OSM in UTM sources: [osm_utm_cache] # layer with a mixed_mode cache image-format - name: mixed_mode title: cache with PNG and JPEG sources: [mixed_cache] # feature information layer - name: feature_layer title: feature information from source layers # map images from osm_cache, feature info from feature_info_source sources: [osm_cache, feature_info_source] caches: osm_cache: # cache the results in two grids/projections grids: [GLOBAL_MERCATOR, global_geodetic_sqrt2] sources: [osm_wms] osm_cache_full_example: # request a meta tile, that consists of m x n tiles meta_size: [5, 5] # increase the size of each meta-tile request by n pixel in each direction # this can solve cases where labels are cut-off at the edge of tiles meta_buffer: 20 # image format for the cache, default format is image/png format: image/jpeg # the source will be requested in this format request_format: image/tiff # if set to true, MapProxy will store tiles that only # contain a single color once # not available on Windows link_single_color_images: true # allow to make 2 parallel requests to the sources for missing tiles concurrent_tile_creators: 2 # level 0 - 13 will be cached, others are served directly from the source use_direct_from_level: 14 grids: [grid_full_example] # a list with all sources for this cache, MapProxy will merge multiple # sources from left (bottom) to right (top) sources: [osm_wms, overlay_full_example] # add a watermark to each tile watermark: text: 'my watermark' opacity: 100 font_size: 30 # mixed image mode cache mixed_mode_cache: # images with transparency will be stored as PNG, fully opaque images as JPEG. # you need to set the request_format to image/png when using mixed-mode format: mixed request_format: image/png # the source images should have transparency to make use of this # feature, but any source will do sources: [legend_wms] # cache for reprojecting tiles osm_utm_cache: grids: [utm32n] meta_size: [4, 4] sources: [osm_cache_in] osm_cache_in: grids: [osm_grid] # cache will not be stored locally disable_storage: true # a tile source you want to reproject sources: [osm_source] # mbtile cache: mbtile_cache: # leave the source-list empty if you use an existing MBTiles file # and don't have a source sources: [] grids: [GLOBAL_MERCATOR] cache: type: mbtiles filename: /path/to/bluemarble.mbtiles # filecache with a directory option. file_cache: cache: type: file # Directory where MapProxy should directly store the tiles # You can use this option to point MapProxy to an existing tile collection # This option does not add the cache or grid name to the path directory: /path/to/preferred_dir/ # use a custom image format defined below format: custom_format grids: [GLOBAL_MERCATOR] # multiple sources, use the secure_source as overlay sources: [osm_wms, secure_source] # couchdb cache couchdb_cache: cache: type: couchdb url: http://localhost:5984 db_name: couchdb_cache tile_id: "%(grid_name)s-%(z)d-%(x)d-%(y)d" # additional metadata that will be stored with each tile tile_metadata: mydata: myvalue tile_col: '{{x}}' tile_row: '{{y}}' tile_level: '{{z}}' created_ts: '{{timestamp}}' created: '{{utc_iso}}' center: '{{wgs_tile_centroid}}' grids: [GLOBAL_MERCATOR] sources: [osm_wms] riak_cache: grid: [GLOBAL_MERCATOR] sources: [osm_wms] cache: type: riak bucket: tile_bucket protocol: pbc default_ports: pb: 8087 http: 8098 nodes: - host: 1.example.com pb_port: 9999 - host: 2.example.com - host: 3.example.com http_port: 8888 sources: # minimal WMS source osm_wms: type: wms req: url: http://osm.omniscale.net/proxy/service? layers: osm # WMS source for use with tagged sources wms_source: type: wms req: url: http://example.org/service? # you can remove `layer` when using this source as # tagged source, or you can list all available layers. # in this case MapProxy will check the layernames when # you reference this source. layers: roads,railways,landusage,buildings # source with GetLegendGraphic support legend_wms: type: wms # requests for other SRS will be reprojected from these SRS supported_srs: ['EPSG:3857', 'EPSG:4326'] wms_opts: # request the source with the specific version version: '1.3.0' # enable legend graphic legendgraphic: True req: url: http://localhost:42423/service? layers: foo,bar # tile-based source, use the type tile to request data from from existing # tile servers like TileCache and GeoWebCache. osm_source: type: tile grid: osm_grid url: http://a.tile.openstreetmap.org/%(z)s/%(x)s/%(y)s.png # limit the source to the given min and max resolution or scale. # MapProxy will return a blank image for requests outside of these boundaries wms_resolution: type: wms min_res: 10000 max_res: 10 req: url: http://localhost:42423/service? layers: scalelayer # with coverages you can define areas where data is available # or where data you are interested in is coverage_source: type: wms req: url: http://localhost:42423/service? layers: base coverage: bbox: [5, 50, 10, 55] srs: 'EPSG:4326' # you can also use Shapefile/GeoJSON/PostGIS/etc. # coverage: # datasource: path/to/shapefile.shp # where: "COUNTRY = 'Germany'" # srs: 'EPSG:4326' # WMS source that requires authentication, MapProxy has support for # HTTP Basic Authentication and HTTP Digest Authentication secure_source: type: wms http: # You can either disable the certificate verification fro HTTPS ssl_no_cert_checks: true # or point MapProxy to the SSL certificate chain on your system # ssl_ca_certs: /etc/ssl/certs/ca-certificates.crt req: # username and password are extracted from the URL and do not show # up in log files url: https://username:mypassword@example.org/service? transparent: true layers: securelayer feature_info_source: type: wms wms_opts: # just query feature informations and no map map: false featureinfo: true req: url: http://localhost:42423/service? layers: foo,bar,baz mapserver_source: type: mapserver req: # path to Mapserver mapfile instead of URL map: /path/to/my.map layers: base mapserver: binary: /usr/cgi-bin/mapserv working_dir: /path/to mapnik_source: type: mapnik mapfile: /path/to/mapnik.xml layers: foo, bar transparent: true # source used as overlay for different layers overlay_full_example: type: wms # allow up to 4 concurrent requests to this source concurrent_requests: 4 wms_opts: version: 1.3.0 featureinfo: true supported_srs: ['EPSG:4326', 'EPSG:31467'] supported_formats: ['image/tiff', 'image/jpeg'] http: # defines how long MapProxy should wait for data from source servers client_timeout: 600 # seconds # add additional HTTP headers to all requests to your sources. headers: my-header: value req: url: https://user:password@example.org:81/service? layers: roads,rails transparent: true # additional options passed to the WMS source styles: base,base map: /home/map/mapserver.map grids: global_geodetic_sqrt2: # base the grid on the options of another grid you already defined base: GLOBAL_GEODETIC res_factor: 'sqrt2' utm32n: srs: 'EPSG:25832' bbox: [4, 46, 16, 56] # let MapProxy transform the bbox to the grid SRS bbox_srs: 'EPSG:4326' origin: 'nw' # resolution of level 0 min_res: 5700 num_levels: 14 osm_grid: base: GLOBAL_MERCATOR srs: 'EPSG:3857' origin: nw grid_full_example: # default tile size is 256 x 256 pixel tile_size: [512, 512] srs: 'EPSG:3857' bbox: [5, 45, 15, 55] bbox_srs: 'EPSG:4326' # the resolution of the first and last level min_res: 2000 #m/px max_res: 50 #m/px align_resolutions_with: GLOBAL_MERCATOR res_grid: srs: 'EPSG:4326' bbox: [4, 46, 16, 56] origin: nw # resolutions created from scales with # % mapproxy-util scales --unit d --as-res-config --dpi 72 100000 50000 25000 12500 8000 5000 res: [ # res level scale @72.0 DPI 0.0003169057, # 0 100000.00000000 0.0001584528, # 1 50000.00000000 0.0000792264, # 2 25000.00000000 0.0000396132, # 3 12500.00000000 0.0000253525, # 4 8000.00000000 0.0000158453, # 5 5000.00000000 ] globals: srs: # override system projection file proj_data_dir: '/path to dir that contains epsg file' # cache options cache: # where to store the cached images base_dir: './cache_data' # where to store lockfiles for concurrent_requests lock_dir: './cache_data/locks' # where to store lockfiles for tile creation tile_lock_dir: './cache_data/tile_locks' # request x*y tiles in one step meta_size: [4, 4] # add a buffer on all sides (in pixel) when requesting # new images meta_buffer: 80 # image/transformation options image: # use best resampling for vector data resampling_method: bicubic # nearest/bilinear # stretch cached images by this factor before # using the next level stretch_factor: 1.15 # shrink cached images up to this factor before # returning an empty image (for the first level) max_shrink_factor: 4.0 # Enable 24bit PNG images. Defaults to true (8bit PNG) paletted: false formats: custom_format: format: image/png # the custom format will be stored as 8bit PNG mode: P colors: 32 transparent: true encoding_options: # The algorithm used to quantize (reduce) the image colors quantizer: fastoctree # edit an existing format image/jpeg: encoding_options: # jpeg quality [0-100] jpeg_quality: 60 mapproxy-1.11.0/mapproxy/config_template/base_config/full_seed_example.yaml000066400000000000000000000044201320454472400272750ustar00rootroot00000000000000# ##################################################################### # MapProxy example seed configuration # ##################################################################### # # This is _not_ a runnable configuration, but it contains most # available options in meaningful combinations. # # Use this file in addition to the documentation to see where and how # things can be configured. seeds: myseed1: # seed all grids of this cache caches: [osm_cache] levels: to: 10 refresh_before: # re-generate tiles older than this date time: 2013-10-10T12:35:00 myseed2: # seed two caches, but only GLOBAL_GEODETIC grid caches: [cache1, cache2] grids: [GLOBAL_GEODETIC] levels: to: 14 refresh_before: # re-generate tiles older than the modification time # of this file. on linux/unix use `touch` to change the time. mtime: ./reseed.time cleanups: cleanup_older_tiles: caches: [osm_cache] remove_before: days: 30 levels: from: 16 remove_complete_levels: caches: [cache1] # remove all tiles regardless of the timestamp. # will remove the complete level directory for `file` caches remove_all: true levels: [14, 18, 19, 20] remove_changes: caches: [cache1] # be careful when using cleanup with coverages, since it needs to check # every possible tile in this coverage (as reported by # `mapproxy-util grids --coverage`). only use small coverages and/or limit # levels coverages: [changed_area] # without remove_before: remove all tiles created before you called # mapproxy-seed. i.e. tiles created before with in this seed run # are not removed levels: from: 14 to: 17 coverages: germany: # any source supported by OGR datasource: 'shps/world_boundaries_m.shp' where: 'CNTRY_NAME = "Germany"' srs: 'EPSG:3857' austria: # simple bbox bbox: [9.36, 46.33, 17.28, 49.09] srs: "EPSG:4326" switzerland: # text file with WKT (Multi)Polygons datasource: 'polygons/SZ.txt' srs: "EPSG:3857" changed_area: # example with PostGIS query datasource: "PG: dbname='db' host='host' user='user' password='password'" where: "select * from last_changes" srs: 'EPSG:3857' mapproxy-1.11.0/mapproxy/config_template/base_config/log.ini000066400000000000000000000012111320454472400242110ustar00rootroot00000000000000[loggers] keys=root,source_requests [handlers] keys=mapproxy,source_requests [formatters] keys=default,requests [logger_root] level=INFO handlers=mapproxy [logger_source_requests] level=INFO qualname=mapproxy.source.request # propagate=0 -> do not show up in logger_root propagate=0 handlers=source_requests [handler_mapproxy] class=FileHandler formatter=default args=(r"%(here)s/mapproxy.log", "a") [handler_source_requests] class=FileHandler formatter=requests args=(r"%(here)s/source-requests.log", "a") [formatter_default] format=%(asctime)s - %(levelname)s - %(name)s - %(message)s [formatter_requests] format=[%(asctime)s] %(message)s mapproxy-1.11.0/mapproxy/config_template/base_config/mapproxy.yaml000066400000000000000000000027251320454472400255050ustar00rootroot00000000000000# ------------------------------- # MapProxy example configuration. # ------------------------------- # # This is a minimal MapProxy configuration. # See full_example.yaml and the documentation for more options. # # Starts the following services: # Demo: # http://localhost:8080/demo # WMS: # capabilities: http://localhost:8080/service?REQUEST=GetCapabilities # WMTS: # capabilities: http://localhost:8080/wmts/1.0.0/WMTSCapabilities.xml # first tile: http://localhost:8080/wmts/osm/webmercator/0/0/0.png # Tile service (compatible with OSM/etc.) # first tile: http://localhost:8080/tiles/osm/webmercator/0/0/0.png # TMS: # note: TMS is not compatible with OSM/Google Maps/etc. # fist tile: http://localhost:8080/tms/1.0.0/osm/webmercator/0/0/0.png # KML: # initial doc: http://localhost:8080/kml/osm/webmercator services: demo: tms: use_grid_names: true # origin for /tiles service origin: 'nw' kml: use_grid_names: true wmts: wms: md: title: MapProxy WMS Proxy abstract: This is a minimal MapProxy example. layers: - name: osm title: Omniscale OSM WMS - osm.omniscale.net sources: [osm_cache] caches: osm_cache: grids: [webmercator] sources: [osm_wms] sources: osm_wms: type: wms req: # use of this source is only permitted for testing url: http://osm.omniscale.net/proxy/service? layers: osm grids: webmercator: base: GLOBAL_WEBMERCATOR globals: mapproxy-1.11.0/mapproxy/config_template/base_config/seed.yaml000066400000000000000000000010211320454472400245320ustar00rootroot00000000000000# --------------------------------------- # MapProxy example seeding configuration. # --------------------------------------- # # This is a minimal MapProxy seeding configuration. # See full_seed_example.yaml and the documentation for more options. # seeds: myseed1: caches: [osm_cache] # grids: [] # coverages: [] levels: to: 10 refresh_before: time: 2013-10-10T12:35:00 cleanups: myclean1: caches: [osm_cache] remove_before: days: 14 levels: from: 11 coverages: mapproxy-1.11.0/mapproxy/config_template/paster/000077500000000000000000000000001320454472400217735ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/config_template/paster/etc/000077500000000000000000000000001320454472400225465ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/config_template/paster/etc/config.ini000066400000000000000000000006421320454472400245160ustar00rootroot00000000000000[app:main] use = egg:MapProxy#app mapproxy_conf = %(here)s/mapproxy.yaml log_conf = %(here)s/log_deploy.ini [server:main] use = egg:Flup#fcgi_fork ## connect via socket socket = %(here)s/../var/fcgi-socket # webserver runs as other user umask = 000 # webserver runs in same group/user # umask = 002 maxRequests = 500 minSpare = 4 maxSpare = 16 maxChildren = 64 ## connect via tcp/ip # host = 127.0.0.1 # port = 5050 mapproxy-1.11.0/mapproxy/config_template/paster/etc/config.wsgi000066400000000000000000000002511320454472400247040ustar00rootroot00000000000000# WSGI module for use with Apache mod_wsgi import os from paste.deploy import loadapp application = loadapp('config:config.ini', relative_to=os.path.dirname(__file__))mapproxy-1.11.0/mapproxy/config_template/paster/etc/develop.ini000066400000000000000000000011441320454472400247050ustar00rootroot00000000000000[app:main] use = egg:MapProxy#app mapproxy_conf = %(here)s/mapproxy.yaml log_conf = reload_files = %(here)s/*.* filter-with = translogger [server:main] ## connect via tcp/ip use = egg:Paste#http host = 0.0.0.0 port = 8080 [filter:translogger] use = egg:Paste#translogger # logging configuration [loggers] keys=root [handlers] keys=console [formatters] keys=default [logger_root] level=INFO qualname=root handlers=console [handler_console] class=StreamHandler formatter=default args=(sys.stdout, ) [formatter_default] format=%(asctime)s - %(levelname)s - %(process)d:%(name)s:%(funcName)s - %(message)s mapproxy-1.11.0/mapproxy/config_template/paster/etc/log_deploy.ini000066400000000000000000000013361320454472400254070ustar00rootroot00000000000000[loggers] keys=root,mapproxy,client [handlers] keys=console,mapproxy,client [formatters] keys=default,client [logger_root] level=WARN qualname=root handlers=console [logger_mapproxy] level=INFO qualname=mapproxy handlers=mapproxy [logger_client] level=INFO qualname=mapproxy.client.http propagate=0 handlers=client [handler_console] class=StreamHandler formatter=default args=(sys.stdout, ) [handler_mapproxy] class=FileHandler formatter=default args=(r"%(here)s/../var/proxy.log", "a") [handler_client] class=FileHandler formatter=client args=(r"%(here)s/../var/client.log", "a") [formatter_default] format=%(asctime)s - %(levelname)s - %(process)d:%(name)s:%(funcName)s - %(message)s [formatter_client] format=%(message)s mapproxy-1.11.0/mapproxy/config_template/paster/etc/mapproxy.yaml000066400000000000000000000075041320454472400253170ustar00rootroot00000000000000services: demo: kml: tms: # needs no arguments wms: # srs: ['EPSG:4326', 'EPSG:900913'] # image_formats: ['image/jpeg', 'image/png'] md: # metadata used in capabilities documents title: MapProxy WMS Proxy abstract: This is the fantastic MapProxy. online_resource: http://mapproxy.org/ contact: person: Your Name Here position: Technical Director organization: address: Fakestreet 123 city: Somewhere postcode: 12345 country: Germany phone: +49(0)000-000000-0 fax: +49(0)000-000000-0 email: info@omniscale.de access_constraints: Insert license and copyright information for this service. fees: 'None' layers: - name: osm title: Omniscale OSM WMS - osm.omniscale.net sources: [osm_cache] # - name: osm_full_example # title: Omniscale OSM WMS - osm.omniscale.net # sources: [osm_cache_full_example] caches: osm_cache: grids: [GLOBAL_MERCATOR, global_geodetic_sqrt2] sources: [osm_wms] # osm_cache_full_example: # meta_buffer: 20 # meta_size: [5, 5] # format: image/png # request_format: image/tiff # link_single_color_images: true # use_direct_from_level: 5 # grids: [grid_full_example] # sources: [osm_wms, overlay_full_example] sources: osm_wms: type: wms req: url: http://osm.omniscale.net/proxy/service? layers: osm # overlay_full_example: # type: wms # concurrent_requests: 4 # wms_opts: # version: 1.3.0 # featureinfo: true # supported_srs: ['EPSG:4326', 'EPSG:31467'] # supported_formats: ['image/tiff', 'image/jpeg'] # http: # ssl_no_cert_checks: true # req: # url: https://user:password@example.org:81/service? # layers: roads,rails # styles: base,base # transparent: true # # # always request in this format # # format: image/png # map: /home/map/mapserver.map grids: global_geodetic_sqrt2: base: GLOBAL_GEODETIC res_factor: 'sqrt2' # grid_full_example: # tile_size: [512, 512] # srs: 'EPSG:900913' # bbox: [5, 45, 15, 55] # bbox_srs: 'EPSG:4326' # min_res: 2000 #m/px # max_res: 50 #m/px # align_resolutions_with: GLOBAL_MERCATOR # another_grid_full_example: # srs: 'EPSG:900913' # bbox: [5, 45, 15, 55] # bbox_srs: 'EPSG:4326' # res_factor: 1.5 # num_levels: 25 globals: # # coordinate transformation options # srs: # # WMS 1.3.0 requires all coordiates in the correct axis order, # # i.e. lon/lat or lat/lon. Use the following settings to # # explicitly set a CRS to either North/East or East/North # # ordering. # axis_order_ne: ['EPSG:9999', 'EPSG:9998'] # axis_order_en: ['EPSG:0000', 'EPSG:0001'] # # you can set the proj4 data dir here, if you need custom # # epsg definitions. the path must contain a file named 'epsg' # # the format of the file is: # # <4326> +proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs <> # proj_data_dir: '/path to dir that contains epsg file' # # cache options # cache: # # where to store the cached images # base_dir: '../var/cache_data' # # where to store lockfiles # lock_dir: '../tmp/tile_locks' # # request x*y tiles in one step # meta_size: [4, 4] # # add a buffer on all sides (in pixel) when requesting # # new images # meta_buffer: 80 # image/transformation options image: resampling_method: nearest # resampling_method: bilinear # resampling_method: bicubic # jpeg_quality: 90 # # stretch cached images by this factor before # # using the next level # stretch_factor: 1.15 # # shrink cached images up to this factor before # # returning an empty image (for the first level) # max_shrink_factor: 4.0 mapproxy-1.11.0/mapproxy/config_template/paster/etc/seed.yaml000066400000000000000000000020571320454472400243560ustar00rootroot00000000000000seeds: myseed1: caches: [osm_cache] grids: [GLOBAL_MERCATOR] coverages: [austria] levels: to: 10 refresh_before: time: 2010-10-21T12:35:00 # dach: # caches: [osm_roads] # coverages: [germany, austria, switzerland] # grids: [GLOBAL_MERCATOR, GLOBAL_GEODETIC] # refresh_before: # weeks: 1 # levels: # from: 11 # to: 15 cleanups: clean1: caches: [osm_cache] grids: [GLOBAL_MERCATOR] remove_before: days: 7 hours: 3 levels: [2,3,5,7] # clean2: # caches: [osm_roads] # grids: [GLOBAL_MERCATOR] # coverages: [germany, austria, switzerland] # remove_before: # time: 2011-01-31T12:00:00 # levels: # from: 11 # to: 14 coverages: austria: bbox: [9.36, 46.33, 17.28, 49.09] bbox_srs: EPSG:4326 # germany: # ogr_datasource: 'shps/world_boundaries_m.shp' # ogr_where: 'CNTRY_NAME = "Germany"' # ogr_srs: 'EPSG:900913' # switzerland: # polygons: 'polygons/SZ.txt' # polygons_srs: EPSG:900913mapproxy-1.11.0/mapproxy/config_template/paster/tmp/000077500000000000000000000000001320454472400225735ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/config_template/paster/tmp/.empty000066400000000000000000000000001320454472400237200ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/config_template/paster/var/000077500000000000000000000000001320454472400225635ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/config_template/paster/var/.empty000066400000000000000000000000001320454472400237100ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/exception.py000066400000000000000000000106561320454472400177150ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Service exception handling (WMS exceptions, XML, in_image, etc.). """ import cgi from mapproxy.response import Response class RequestError(Exception): """ Exception for all request related errors. :ivar internal: True if the error was an internal error, ie. the request itself was valid (e.g. the source server is unreachable """ def __init__(self, message, code=None, request=None, internal=False, status=None): Exception.__init__(self, message) self.msg = message self.code = code self.request = request self.internal = internal self.status = status def render(self): """ Return a response with the rendered exception. The rendering is delegated to the ``exception_handler`` that issued the ``RequestError``. :rtype: `Response` """ if self.request is not None: handler = self.request.exception_handler return handler.render(self) elif self.status is not None: return Response(self.msg, status=self.status) else: return Response('internal error: %s' % self.msg, status=500) def __str__(self): return 'RequestError("%s", code=%r, request=%r)' % (self.msg, self.code, self.request) class ExceptionHandler(object): """ Base class for exception handler. """ def render(self, request_error): """ Return a response with the rendered exception. :param request_error: the exception to render :type request_error: `RequestError` :rtype: `Response` """ raise NotImplementedError() def _not_implemented(*args, **kw): raise NotImplementedError() class XMLExceptionHandler(ExceptionHandler): """ Mixin class for tempita-based template renderer. """ template_file = None """The filename of the tempita xml template""" content_type = None """ The mime type of the exception response (use this or mimetype). The content_type is sent as defined here. """ status_code = 200 """ The HTTP status code. """ status_codes = {} """ Mapping of exceptionCodes to status_codes. If not defined status_code is used. """ mimetype = None """ The mime type of the exception response. (use this or content_type). A character encoding might be added to the mimetype (like text/xml;charset=UTF-8) """ template_func = _not_implemented """ Function that returns the named template. """ def render(self, request_error): """ Render the template of this exception handler. Passes the ``request_error.msg`` and ``request_error.code`` to the template. :type request_error: `RequestError` """ status_code = self.status_codes.get(request_error.code, self.status_code) # escape &<> in error message (e.g. URL params) msg = cgi.escape(request_error.msg) result = self.template.substitute(exception=msg, code=request_error.code) return Response(result, mimetype=self.mimetype, content_type=self.content_type, status=status_code) @property def template(self): """ The template for this ExceptionHandler. """ return self.template_func(self.template_file) class PlainExceptionHandler(ExceptionHandler): mimetype = 'text/plain' status_code = 404 def render(self, request_error): if request_error.internal: self.status_code = 500 return Response(request_error.msg, status=self.status_code, mimetype=self.mimetype) mapproxy-1.11.0/mapproxy/featureinfo.py000066400000000000000000000127661320454472400202320ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import copy import json from functools import reduce from io import StringIO from mapproxy.compat import string_type, PY2, BytesIO, iteritems try: from lxml import etree, html has_xslt_support = True etree, html # prevent pyflakes warning except ImportError: has_xslt_support = False etree = html = None class FeatureInfoDoc(object): content_type = None def as_etree(self): raise NotImplementedError() def as_string(self): raise NotImplementedError() class TextFeatureInfoDoc(FeatureInfoDoc): info_type = 'text' def __init__(self, content): self.content = content def as_string(self): return self.content @classmethod def combine(cls, docs): result_content = [doc.as_string() for doc in docs] return cls(b'\n'.join(result_content)) class XMLFeatureInfoDoc(FeatureInfoDoc): info_type = 'xml' def __init__(self, content): if isinstance(content, (string_type, bytes)): self._str_content = content self._etree = None else: self._str_content = None if hasattr(content, 'getroottree'): content = content.getroottree() self._etree = content assert hasattr(content, 'getroot'), "expected etree like object" def as_string(self): if self._str_content is None: self._str_content = self._serialize_etree() return self._str_content def as_etree(self): if self._etree is None: self._etree = self._parse_content() return self._etree def _serialize_etree(self): return etree.tostring(self._etree) def _parse_content(self): doc = as_io(self._str_content) return etree.parse(doc) @classmethod def combine(cls, docs): if etree is None: return TextFeatureInfoDoc.combine(docs) doc = docs.pop(0) result_tree = copy.deepcopy(doc.as_etree()) for doc in docs: tree = doc.as_etree() result_tree.getroot().extend(tree.getroot().iterchildren()) return cls(result_tree) class HTMLFeatureInfoDoc(XMLFeatureInfoDoc): info_type = 'html' def _parse_content(self): root = html.document_fromstring(self._str_content) return root def _serialize_etree(self): return html.tostring(self._etree) @classmethod def combine(cls, docs): if etree is None: return TextFeatureInfoDoc.combine(docs) doc = docs.pop(0) result_tree = copy.deepcopy(doc.as_etree()) for doc in docs: tree = doc.as_etree() try: body = tree.body.getchildren() except IndexError: body = tree.getchildren() result_tree.body.extend(body) return cls(result_tree) class JSONFeatureInfoDoc(FeatureInfoDoc): info_type = 'json' def __init__(self, content): self.content = content def as_string(self): return self.content @classmethod def combine(cls, docs): contents = [json.loads(d.content) for d in docs] combined = reduce(lambda a, b: merge_dict(a, b), contents) return cls(json.dumps(combined)) def merge_dict(base, other): """ Return `base` dict with values from `conf` merged in. """ for k, v in iteritems(other): if k not in base: base[k] = v else: if isinstance(base[k], dict): merge_dict(base[k], v) elif isinstance(base[k], list): base[k].extend(v) else: base[k] = v return base def create_featureinfo_doc(content, info_format): info_format = info_format.split(';', 1)[0].strip() # remove mime options like charset if info_format in ('text/xml', 'application/vnd.ogc.gml'): return XMLFeatureInfoDoc(content) if info_format == 'text/html': return HTMLFeatureInfoDoc(content) if info_format == 'application/json': return JSONFeatureInfoDoc(content) return TextFeatureInfoDoc(content) class XSLTransformer(object): def __init__(self, xsltscript): self.xsltscript = xsltscript def transform(self, input_doc): input_tree = input_doc.as_etree() xslt_tree = etree.parse(self.xsltscript) transform = etree.XSLT(xslt_tree) output_tree = transform(input_tree) return XMLFeatureInfoDoc(output_tree) __call__ = transform def as_io(doc): if PY2: return BytesIO(doc) else: if isinstance(doc, str): return StringIO(doc) else: return BytesIO(doc) def combined_inputs(input_docs): doc = input_docs.pop(0) input_tree = etree.parse(as_io(doc)) for doc in input_docs: doc_tree = etree.parse(as_io(doc)) input_tree.getroot().extend(doc_tree.getroot().iterchildren()) return input_tree mapproxy-1.11.0/mapproxy/grid.py000066400000000000000000001144701320454472400166430ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ (Meta-)Tile grids (data and calculations). """ from __future__ import division import math from mapproxy.srs import SRS, get_epsg_num, merge_bbox, bbox_equals from mapproxy.util.collections import ImmutableDictList from mapproxy.compat import string_type, iteritems geodetic_epsg_codes = [4326] class GridError(Exception): pass class NoTiles(GridError): pass def get_resolution(bbox, size): """ Calculate the highest resolution needed to draw the bbox into an image with given size. >>> get_resolution((-180,-90,180,90), (256, 256)) 0.703125 :returns: the resolution :rtype: float """ w = abs(bbox[0] - bbox[2]) h = abs(bbox[1] - bbox[3]) return min(w/size[0], h/size[1]) def tile_grid_for_epsg(epsg, bbox=None, tile_size=(256, 256), res=None): """ Create a tile grid that matches the given epsg code: :param epsg: the epsg code :type epsg: 'EPSG:0000', '0000' or 0000 :param bbox: the bbox of the grid :param tile_size: the size of each tile :param res: a list with all resolutions """ epsg = get_epsg_num(epsg) if epsg in geodetic_epsg_codes: return TileGrid(epsg, is_geodetic=True, bbox=bbox, tile_size=tile_size, res=res) return TileGrid(epsg, bbox=bbox, tile_size=tile_size, res=res) # defer loading of default bbox since custom proj settings # are not loaded on import time class _default_bboxs(object): _defaults = { 4326: (-180, -90, 180, 90), } for epsg_num in (900913, 3857, 102100, 102113): _defaults[epsg_num] = (-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244) defaults = None def get(self, key, default=None): try: return self[key] except KeyError: return default def __getitem__(self, key): if self.defaults is None: defaults = {} for epsg, bbox in iteritems(self._defaults): defaults[SRS(epsg)] = bbox self.defaults = defaults return self.defaults[key] default_bboxs = _default_bboxs() def tile_grid(srs=None, bbox=None, bbox_srs=None, tile_size=(256, 256), res=None, res_factor=2.0, threshold_res=None, num_levels=None, min_res=None, max_res=None, stretch_factor=1.15, max_shrink_factor=4.0, align_with=None, origin='ll', name=None ): """ This function creates a new TileGrid. """ if srs is None: srs = 'EPSG:900913' srs = SRS(srs) if not bbox: bbox = default_bboxs.get(srs) if not bbox: raise ValueError('need a bbox for grid with %s' % srs) bbox = grid_bbox(bbox, srs=srs, bbox_srs=bbox_srs) if res: if isinstance(res, list): if isinstance(res[0], (tuple, list)): # named resolutions res = sorted(res, key=lambda x: x[1], reverse=True) else: res = sorted(res, reverse=True) assert min_res is None assert max_res is None assert align_with is None else: raise ValueError("res is not a list, use res_factor for float values") elif align_with is not None: res = aligned_resolutions(min_res, max_res, res_factor, num_levels, bbox, tile_size, align_with) else: res = resolutions(min_res, max_res, res_factor, num_levels, bbox, tile_size) origin = origin_from_string(origin) return TileGrid(srs, bbox=bbox, tile_size=tile_size, res=res, threshold_res=threshold_res, stretch_factor=stretch_factor, max_shrink_factor=max_shrink_factor, origin=origin, name=name) ORIGIN_UL = 'ul' ORIGIN_LL = 'll' def origin_from_string(origin): if origin == None: origin = ORIGIN_LL elif origin.lower() in ('ll', 'sw'): origin = ORIGIN_LL elif origin.lower() in ('ul', 'nw'): origin = ORIGIN_UL else: raise ValueError("unknown origin value '%s'" % origin) return origin def aligned_resolutions(min_res=None, max_res=None, res_factor=2.0, num_levels=None, bbox=None, tile_size=(256, 256), align_with=None): alinged_res = align_with.resolutions res = list(alinged_res) if not min_res: width = bbox[2] - bbox[0] height = bbox[3] - bbox[1] min_res = max(width/tile_size[0], height/tile_size[1]) res = [r for r in res if r <= min_res] if max_res: res = [r for r in res if r >= max_res] if num_levels: res = res[:num_levels] factor_calculated = res[0]/res[1] if res_factor == 'sqrt2' and round(factor_calculated, 8) != round(math.sqrt(2), 8): if round(factor_calculated, 8) == 2.0: new_res = [] for r in res: new_res.append(r) new_res.append(r/math.sqrt(2)) res = new_res elif res_factor == 2.0 and round(factor_calculated, 8) != round(2.0, 8): if round(factor_calculated, 8) == round(math.sqrt(2), 8): res = res[::2] return res def resolutions(min_res=None, max_res=None, res_factor=2.0, num_levels=None, bbox=None, tile_size=(256, 256)): if res_factor == 'sqrt2': res_factor = math.sqrt(2) res = [] if not min_res: width = bbox[2] - bbox[0] height = bbox[3] - bbox[1] min_res = max(width/tile_size[0], height/tile_size[1]) if max_res: if num_levels: res_step = (math.log10(min_res) - math.log10(max_res)) / (num_levels-1) res = [10**(math.log10(min_res) - res_step*i) for i in range(num_levels)] else: res = [min_res] while True: next_res = res[-1]/res_factor if max_res >= next_res: break res.append(next_res) else: if not num_levels: num_levels = 20 if res_factor != math.sqrt(2) else 40 res = [min_res] while len(res) < num_levels: res.append(res[-1]/res_factor) return res def grid_bbox(bbox, bbox_srs, srs): bbox = bbox_tuple(bbox) if bbox_srs: bbox = SRS(bbox_srs).transform_bbox_to(srs, bbox) return bbox def bbox_tuple(bbox): """ >>> bbox_tuple('20,-30,40,-10') (20.0, -30.0, 40.0, -10.0) >>> bbox_tuple([20,-30,40,-10]) (20.0, -30.0, 40.0, -10.0) """ if isinstance(bbox, string_type): bbox = bbox.split(',') bbox = tuple(map(float, bbox)) return bbox def bbox_width(bbox): return bbox[2] - bbox[0] def bbox_height(bbox): return bbox[3] - bbox[1] def bbox_size(bbox): return bbox_width(bbox), bbox_height(bbox) class NamedGridList(ImmutableDictList): def __init__(self, items): tmp = [] for i, value in enumerate(items): if isinstance(value, (tuple, list)): name, value = value else: name = str('%02d' % i) tmp.append((name, value)) ImmutableDictList.__init__(self, tmp) class TileGrid(object): """ This class represents a regular tile grid. The first level (0) contains a single tile, the origin is bottom-left. :ivar levels: the number of levels :ivar tile_size: the size of each tile in pixel :type tile_size: ``int(with), int(height)`` :ivar srs: the srs of the grid :type srs: `SRS` :ivar bbox: the bbox of the grid, tiles may overlap this bbox """ spheroid_a = 6378137.0 # for 900913 flipped_y_axis = False def __init__(self, srs=900913, bbox=None, tile_size=(256, 256), res=None, threshold_res=None, is_geodetic=False, levels=None, stretch_factor=1.15, max_shrink_factor=4.0, origin='ll', name=None): """ :param stretch_factor: allow images to be scaled up by this factor before the next level will be selected :param max_shrink_factor: allow images to be scaled down by this factor before NoTiles is raised >>> grid = TileGrid(srs=900913) >>> [round(x, 2) for x in grid.bbox] [-20037508.34, -20037508.34, 20037508.34, 20037508.34] """ if isinstance(srs, (int, string_type)): srs = SRS(srs) self.srs = srs self.tile_size = tile_size self.origin = origin_from_string(origin) self.name = name if self.origin == 'ul': self.flipped_y_axis = True self.is_geodetic = is_geodetic self.stretch_factor = stretch_factor self.max_shrink_factor = max_shrink_factor if levels is None: self.levels = 20 else: self.levels = levels if bbox is None: bbox = self._calc_bbox() self.bbox = bbox factor = None if res is None: factor = 2.0 res = self._calc_res(factor=factor) elif res == 'sqrt2': if levels is None: self.levels = 40 factor = math.sqrt(2) res = self._calc_res(factor=factor) elif is_float(res): factor = float(res) res = self._calc_res(factor=factor) self.levels = len(res) self.resolutions = NamedGridList(res) self.threshold_res = None if threshold_res: self.threshold_res = sorted(threshold_res) self.grid_sizes = self._calc_grids() def _calc_grids(self): width = self.bbox[2] - self.bbox[0] height = self.bbox[3] - self.bbox[1] grids = [] for idx, res in self.resolutions.iteritems(): x = max(math.ceil(width // res / self.tile_size[0]), 1) y = max(math.ceil(height // res / self.tile_size[1]), 1) grids.append((idx, (int(x), int(y)))) return NamedGridList(grids) def _calc_bbox(self): if self.is_geodetic: return (-180.0, -90.0, 180.0, 90.0) else: circum = 2 * math.pi * self.spheroid_a offset = circum / 2.0 return (-offset, -offset, offset, offset) def _calc_res(self, factor=None): width = self.bbox[2] - self.bbox[0] height = self.bbox[3] - self.bbox[1] initial_res = max(width/self.tile_size[0], height/self.tile_size[1]) if factor is None: return pyramid_res_level(initial_res, levels=self.levels) else: return pyramid_res_level(initial_res, factor, levels=self.levels) def resolution(self, level): """ Returns the resolution of the `level` in units/pixel. :param level: the zoom level index (zero is top) >>> grid = TileGrid(SRS(900913)) >>> '%.5f' % grid.resolution(0) '156543.03393' >>> '%.5f' % grid.resolution(1) '78271.51696' >>> '%.5f' % grid.resolution(4) '9783.93962' """ return self.resolutions[level] def closest_level(self, res): """ Returns the level index that offers the required resolution. :param res: the required resolution :returns: the level with the requested or higher resolution >>> grid = TileGrid(SRS(900913)) >>> grid.stretch_factor = 1.1 >>> l1_res = grid.resolution(1) >>> [grid.closest_level(x) for x in (320000.0, 160000.0, l1_res+50, l1_res, \ l1_res-50, l1_res*0.91, l1_res*0.89, 8000.0)] [0, 0, 1, 1, 1, 1, 2, 5] """ prev_l_res = self.resolutions[0] threshold = None thresholds = [] if self.threshold_res: thresholds = self.threshold_res[:] threshold = thresholds.pop() # skip thresholds above first res while threshold > prev_l_res and thresholds: threshold = thresholds.pop() threshold_result = None for level, l_res in enumerate(self.resolutions): if threshold and prev_l_res > threshold >= l_res: if res > threshold: return level-1 elif res >= l_res: return level threshold = thresholds.pop() if thresholds else None if threshold_result is not None: # Use previous level that was within stretch_factor, # but only if this level res is smaller then res. # This fixes selection for resolutions that are closer together then stretch_factor. # if l_res < res: return threshold_result if l_res <= res*self.stretch_factor: # l_res within stretch_factor # remember this level, check for thresholds or better res in next loop threshold_result = level prev_l_res = l_res return level def tile(self, x, y, level): """ Returns the tile id for the given point. >>> grid = TileGrid(SRS(900913)) >>> grid.tile(1000, 1000, 0) (0, 0, 0) >>> grid.tile(1000, 1000, 1) (1, 1, 1) >>> grid = TileGrid(SRS(900913), tile_size=(512, 512)) >>> grid.tile(1000, 1000, 2) (2, 2, 2) """ res = self.resolution(level) x = x - self.bbox[0] if self.flipped_y_axis: y = self.bbox[3] - y else: y = y - self.bbox[1] tile_x = x/float(res*self.tile_size[0]) tile_y = y/float(res*self.tile_size[1]) return (int(math.floor(tile_x)), int(math.floor(tile_y)), level) def flip_tile_coord(self, tile_coord): """ Flip the tile coord on the y-axis. (Switch between bottom-left and top-left origin.) >>> grid = TileGrid(SRS(900913)) >>> grid.flip_tile_coord((0, 1, 1)) (0, 0, 1) >>> grid.flip_tile_coord((1, 3, 2)) (1, 0, 2) """ (x, y, z) = tile_coord return (x, self.grid_sizes[z][1]-1-y, z) def supports_access_with_origin(self, origin): if origin_from_string(origin) == self.origin: return True # check for each level if the top and bottom coordinates of the tiles # match the bbox of the grid. only in this case we can flip y-axis # without any issues # allow for some rounding errors in the _tiles_bbox calculations delta = max(abs(self.bbox[1]), abs(self.bbox[3])) / 1e12 for level, grid_size in enumerate(self.grid_sizes): level_bbox = self._tiles_bbox([(0, 0, level), (grid_size[0] - 1, grid_size[1] - 1, level)]) if abs(self.bbox[1] - level_bbox[1]) > delta or abs(self.bbox[3] - level_bbox[3]) > delta: return False return True def origin_tile(self, level, origin): assert self.supports_access_with_origin(origin), 'tile origins are incompatible' tile = (0, 0, level) if origin_from_string(origin) == self.origin: return tile return self.flip_tile_coord(tile) def get_affected_tiles(self, bbox, size, req_srs=None): """ Get a list with all affected tiles for a bbox and output size. :returns: the bbox, the size and a list with tile coordinates, sorted row-wise :rtype: ``bbox, (xs, yz), [(x, y, z), ...]`` >>> grid = TileGrid() >>> bbox = (-20037508.34, -20037508.34, 20037508.34, 20037508.34) >>> tile_size = (256, 256) >>> grid.get_affected_tiles(bbox, tile_size) ... #doctest: +NORMALIZE_WHITESPACE +ELLIPSIS ((-20037508.342789244, -20037508.342789244,\ 20037508.342789244, 20037508.342789244), (1, 1),\ ) """ src_bbox, level = self.get_affected_bbox_and_level(bbox, size, req_srs=req_srs) return self.get_affected_level_tiles(src_bbox, level) def get_affected_bbox_and_level(self, bbox, size, req_srs=None): if req_srs and req_srs != self.srs: src_bbox = req_srs.transform_bbox_to(self.srs, bbox) else: src_bbox = bbox if not bbox_intersects(self.bbox, src_bbox): raise NoTiles() res = get_resolution(src_bbox, size) level = self.closest_level(res) if res > self.resolutions[0]*self.max_shrink_factor: raise NoTiles() return src_bbox, level def get_affected_level_tiles(self, bbox, level): """ Get a list with all affected tiles for a `bbox` in the given `level`. :returns: the bbox, the size and a list with tile coordinates, sorted row-wise :rtype: ``bbox, (xs, yz), [(x, y, z), ...]`` >>> grid = TileGrid() >>> bbox = (-20037508.34, -20037508.34, 20037508.34, 20037508.34) >>> grid.get_affected_level_tiles(bbox, 0) ... #doctest: +NORMALIZE_WHITESPACE +ELLIPSIS ((-20037508.342789244, -20037508.342789244,\ 20037508.342789244, 20037508.342789244), (1, 1),\ ) """ # remove 1/10 of a pixel so we don't get a tiles we only touch delta = self.resolutions[level] / 10.0 x0, y0, _ = self.tile(bbox[0]+delta, bbox[1]+delta, level) x1, y1, _ = self.tile(bbox[2]-delta, bbox[3]-delta, level) try: return self._tile_iter(x0, y0, x1, y1, level) except IndexError: raise GridError('Invalid BBOX') def _tile_iter(self, x0, y0, x1, y1, level): xs = list(range(x0, x1+1)) if self.flipped_y_axis: y0, y1 = y1, y0 ys = list(range(y0, y1+1)) else: ys = list(range(y1, y0-1, -1)) ll = (xs[0], ys[-1], level) ur = (xs[-1], ys[0], level) abbox = self._tiles_bbox([ll, ur]) return (abbox, (len(xs), len(ys)), _create_tile_list(xs, ys, level, self.grid_sizes[level])) def _tiles_bbox(self, tiles): """ Returns the bbox of multiple tiles. The tiles should be ordered row-wise, bottom-up. :param tiles: ordered list of tiles :returns: the bbox of all tiles """ ll_bbox = self.tile_bbox(tiles[0]) ur_bbox = self.tile_bbox(tiles[-1]) return merge_bbox(ll_bbox, ur_bbox) def tile_bbox(self, tile_coord, limit=False): """ Returns the bbox of the given tile. >>> grid = TileGrid(SRS(900913)) >>> [round(x, 2) for x in grid.tile_bbox((0, 0, 0))] [-20037508.34, -20037508.34, 20037508.34, 20037508.34] >>> [round(x, 2) for x in grid.tile_bbox((1, 1, 1))] [0.0, 0.0, 20037508.34, 20037508.34] """ x, y, z = tile_coord res = self.resolution(z) x0 = self.bbox[0] + round(x * res * self.tile_size[0], 12) x1 = x0 + round(res * self.tile_size[0], 12) if self.flipped_y_axis: y1 = self.bbox[3] - round(y * res * self.tile_size[1], 12) y0 = y1 - round(res * self.tile_size[1], 12) else: y0 = self.bbox[1] + round(y * res * self.tile_size[1], 12) y1 = y0 + round(res * self.tile_size[1], 12) if limit: return ( max(x0, self.bbox[0]), max(y0, self.bbox[1]), min(x1, self.bbox[2]), min(y1, self.bbox[3]) ) return x0, y0, x1, y1 def limit_tile(self, tile_coord): """ Check if the `tile_coord` is in the grid. :returns: the `tile_coord` if it is within the ``grid``, otherwise ``None``. >>> grid = TileGrid(SRS(900913)) >>> grid.limit_tile((-1, 0, 2)) == None True >>> grid.limit_tile((1, 2, 1)) == None True >>> grid.limit_tile((1, 2, 2)) (1, 2, 2) """ x, y, z = tile_coord if isinstance(z, string_type): if z not in self.grid_sizes: return None elif z < 0 or z >= self.levels: return None grid = self.grid_sizes[z] if x < 0 or y < 0 or x >= grid[0] or y >= grid[1]: return None return x, y, z def __repr__(self): return '%s(%r, (%.4f, %.4f, %.4f, %.4f),...)' % (self.__class__.__name__, self.srs, self.bbox[0], self.bbox[1], self.bbox[2], self.bbox[3]) def is_subset_of(self, other): """ Returns ``True`` if every tile in `self` is present in `other`. Tile coordinates might differ and `other` may contain more tiles (more levels, larger bbox). """ if self.srs != other.srs: return False if self.tile_size != other.tile_size: return False # check if all level tiles from self align with (affected) # tiles from other for self_level, self_level_res in self.resolutions.iteritems(): level_size = ( self.grid_sizes[self_level][0] * self.tile_size[0], self.grid_sizes[self_level][1] * self.tile_size[1] ) level_bbox = self._tiles_bbox([ (0, 0, self_level), (self.grid_sizes[self_level][0] - 1, self.grid_sizes[self_level][1] - 1, self_level) ]) try: bbox, level = other.get_affected_bbox_and_level(level_bbox, level_size) except NoTiles: return False try: bbox, grid_size, tiles = other.get_affected_level_tiles(level_bbox, level) except GridError: return False if other.resolution(level) != self_level_res: return False if not bbox_equals(bbox, level_bbox): return False return True def _create_tile_list(xs, ys, level, grid_size): """ Returns an iterator tile_coords for the given tile ranges (`xs` and `ys`). If the one tile_coord is negative or out of the `grid_size` bound, the coord is None. """ x_limit = grid_size[0] y_limit = grid_size[1] for y in ys: for x in xs: if x < 0 or y < 0 or x >= x_limit or y >= y_limit: yield None else: yield x, y, level def is_float(x): try: float(x) return True except TypeError: return False def pyramid_res_level(initial_res, factor=2.0, levels=20): """ Return resolutions of an image pyramid. :param initial_res: the resolution of the top level (0) :param factor: the factor between each level, for tms access 2 :param levels: number of resolutions to generate >>> list(pyramid_res_level(10000, levels=5)) [10000.0, 5000.0, 2500.0, 1250.0, 625.0] >>> [round(x, 4) for x in ... pyramid_res_level(10000, factor=1/0.75, levels=5)] [10000.0, 7500.0, 5625.0, 4218.75, 3164.0625] """ return [initial_res/factor**n for n in range(levels)] class MetaGrid(object): """ This class contains methods to calculate bbox, etc. of metatiles. :param grid: the grid to use for the metatiles :param meta_size: the number of tiles a metatile consist :type meta_size: ``(x_size, y_size)`` :param meta_buffer: the buffer size in pixel that is added to each metatile. the number is added to all four borders. this buffer may improve the handling of lables overlapping (meta)tile borders. :type meta_buffer: pixel """ def __init__(self, grid, meta_size, meta_buffer=0): self.grid = grid self.meta_size = meta_size or 0 self.meta_buffer = meta_buffer def _meta_bbox(self, tile_coord=None, tiles=None, limit_to_bbox=True): """ Returns the bbox of the metatile that contains `tile_coord`. :type tile_coord: ``(x, y, z)`` >>> mgrid = MetaGrid(grid=TileGrid(), meta_size=(2, 2)) >>> [round(x, 2) for x in mgrid._meta_bbox((0, 0, 2))[0]] [-20037508.34, -20037508.34, 0.0, 0.0] >>> mgrid = MetaGrid(grid=TileGrid(), meta_size=(2, 2)) >>> [round(x, 2) for x in mgrid._meta_bbox((0, 0, 0))[0]] [-20037508.34, -20037508.34, 20037508.34, 20037508.34] """ if tiles: assert tile_coord is None level = tiles[0][2] bbox = self.grid._tiles_bbox(tiles) else: level = tile_coord[2] bbox = self.unbuffered_meta_bbox(tile_coord) return self._buffered_bbox(bbox, level, limit_to_bbox) def unbuffered_meta_bbox(self, tile_coord): x, y, z = tile_coord meta_size = self._meta_size(z) return self.grid._tiles_bbox([(tile_coord), (x+meta_size[0]-1, y+meta_size[1]-1, z)]) def _buffered_bbox(self, bbox, level, limit_to_grid_bbox=True): minx, miny, maxx, maxy = bbox buffers = (0, 0, 0, 0) if self.meta_buffer > 0: res = self.grid.resolution(level) minx -= self.meta_buffer * res miny -= self.meta_buffer * res maxx += self.meta_buffer * res maxy += self.meta_buffer * res buffers = [self.meta_buffer, self.meta_buffer, self.meta_buffer, self.meta_buffer] if limit_to_grid_bbox: if self.grid.bbox[0] > minx: delta = self.grid.bbox[0] - minx buffers[0] = buffers[0] - int(round(delta / res, 5)) minx = self.grid.bbox[0] if self.grid.bbox[1] > miny: delta = self.grid.bbox[1] - miny buffers[1] = buffers[1] - int(round(delta / res, 5)) miny = self.grid.bbox[1] if self.grid.bbox[2] < maxx: delta = maxx - self.grid.bbox[2] buffers[2] = buffers[2] - int(round(delta / res, 5)) maxx = self.grid.bbox[2] if self.grid.bbox[3] < maxy: delta = maxy - self.grid.bbox[3] buffers[3] = buffers[3] - int(round(delta / res, 5)) maxy = self.grid.bbox[3] return (minx, miny, maxx, maxy), tuple(buffers) def meta_tile(self, tile_coord): """ Returns the meta tile for `tile_coord`. """ tile_coord = self.main_tile(tile_coord) level = tile_coord[2] bbox, buffers = self._meta_bbox(tile_coord) grid_size = self._meta_size(level) size = self._size_from_buffered_bbox(bbox, level) tile_patterns = self._tiles_pattern(tile=tile_coord, grid_size=grid_size, buffers=buffers) return MetaTile(bbox=bbox, size=size, tile_patterns=tile_patterns, grid_size=grid_size ) def minimal_meta_tile(self, tiles): """ Returns a MetaTile that contains all `tiles` plus ``meta_buffer``, but nothing more. """ tiles, grid_size, bounds = self._full_tile_list(tiles) tiles = list(tiles) bbox, buffers = self._meta_bbox(tiles=bounds) level = tiles[0][2] size = self._size_from_buffered_bbox(bbox, level) tile_pattern = self._tiles_pattern(tiles=tiles, grid_size=grid_size, buffers=buffers) return MetaTile( bbox=bbox, size=size, tile_patterns=tile_pattern, grid_size=grid_size, ) def _size_from_buffered_bbox(self, bbox, level): # meta_size * tile_size + 2*buffer does not work, # since the buffer can get truncated at the grid border res = self.grid.resolution(level) width = int(round((bbox[2] - bbox[0]) / res)) height = int(round((bbox[3] - bbox[1]) / res)) return width, height def _full_tile_list(self, tiles): """ Return a complete list of all tiles that a minimal meta tile with `tiles` contains. >>> mgrid = MetaGrid(grid=TileGrid(), meta_size=(2, 2)) >>> mgrid._full_tile_list([(0, 0, 2), (1, 1, 2)]) ([(0, 1, 2), (1, 1, 2), (0, 0, 2), (1, 0, 2)], (2, 2), ((0, 0, 2), (1, 1, 2))) """ tile = tiles.pop() z = tile[2] minx = maxx = tile[0] miny = maxy = tile[1] for tile in tiles: x, y = tile[:2] minx = min(minx, x) maxx = max(maxx, x) miny = min(miny, y) maxy = max(maxy, y) grid_size = 1+maxx-minx, 1+maxy-miny if self.grid.flipped_y_axis: ys = range(miny, maxy+1) else: ys = range(maxy, miny-1, -1) xs = range(minx, maxx+1) bounds = (minx, miny, z), (maxx, maxy, z) return list(_create_tile_list(xs, ys, z, (maxx+1, maxy+1))), grid_size, bounds def main_tile(self, tile_coord): x, y, z = tile_coord meta_size = self._meta_size(z) x0 = x//meta_size[0] * meta_size[0] y0 = y//meta_size[1] * meta_size[1] return x0, y0, z def tile_list(self, main_tile): tile_grid = self._meta_size(main_tile[2]) return self._meta_tile_list(main_tile, tile_grid) def _meta_tile_list(self, main_tile, tile_grid): """ >>> mgrid = MetaGrid(grid=TileGrid(), meta_size=(2, 2)) >>> mgrid._meta_tile_list((0, 1, 3), (2, 2)) [(0, 1, 3), (1, 1, 3), (0, 0, 3), (1, 0, 3)] """ minx, miny, z = self.main_tile(main_tile) maxx = minx + tile_grid[0] - 1 maxy = miny + tile_grid[1] - 1 if self.grid.flipped_y_axis: ys = range(miny, maxy+1) else: ys = range(maxy, miny-1, -1) xs = range(minx, maxx+1) return list(_create_tile_list(xs, ys, z, self.grid.grid_sizes[z])) def _tiles_pattern(self, grid_size, buffers, tile=None, tiles=None): """ Returns the tile pattern for the given list of tiles. The result contains for each tile the ``tile_coord`` and the upper-left pixel coordinate of the tile in the meta tile image. >>> mgrid = MetaGrid(grid=TileGrid(), meta_size=(2, 2)) >>> tiles = list(mgrid._tiles_pattern(tiles=[(0, 1, 2), (1, 1, 2)], ... grid_size=(2, 1), ... buffers=(0, 0, 10, 10))) >>> tiles[0], tiles[-1] (((0, 1, 2), (0, 10)), ((1, 1, 2), (256, 10))) >>> tiles = list(mgrid._tiles_pattern(tile=(1, 1, 2), ... grid_size=(2, 2), ... buffers=(10, 20, 30, 40))) >>> tiles[0], tiles[-1] (((0, 1, 2), (10, 40)), ((1, 0, 2), (266, 296))) """ if tile: tiles = self._meta_tile_list(tile, grid_size) for i in range(grid_size[1]): for j in range(grid_size[0]): yield tiles[j+i*grid_size[0]], ( j*self.grid.tile_size[0] + buffers[0], i*self.grid.tile_size[1] + buffers[3]) def _meta_size(self, level): grid_size = self.grid.grid_sizes[level] return min(self.meta_size[0], grid_size[0]), min(self.meta_size[1], grid_size[1]) def get_affected_level_tiles(self, bbox, level): """ Get a list with all affected tiles for a `bbox` in the given `level`. :returns: the bbox, the size and a list with tile coordinates, sorted row-wise :rtype: ``bbox, (xs, yz), [(x, y, z), ...]`` >>> grid = MetaGrid(TileGrid(), (2, 2)) >>> bbox = (-20037508.34, -20037508.34, 20037508.34, 20037508.34) >>> grid.get_affected_level_tiles(bbox, 0) ... #doctest: +NORMALIZE_WHITESPACE +ELLIPSIS ((-20037508.342789244, -20037508.342789244,\ 20037508.342789244, 20037508.342789244), (1, 1),\ ) """ # remove 1/10 of a pixel so we don't get a tiles we only touch delta = self.grid.resolutions[level] / 10.0 x0, y0, _ = self.grid.tile(bbox[0]+delta, bbox[1]+delta, level) x1, y1, _ = self.grid.tile(bbox[2]-delta, bbox[3]-delta, level) meta_size = self._meta_size(level) x0 = x0//meta_size[0] * meta_size[0] x1 = x1//meta_size[0] * meta_size[0] y0 = y0//meta_size[1] * meta_size[1] y1 = y1//meta_size[1] * meta_size[1] try: return self._tile_iter(x0, y0, x1, y1, level) except IndexError: raise GridError('Invalid BBOX') def _tile_iter(self, x0, y0, x1, y1, level): meta_size = self._meta_size(level) xs = list(range(x0, x1+1, meta_size[0])) if self.grid.flipped_y_axis: y0, y1 = y1, y0 ys = list(range(y0, y1+1, meta_size[1])) else: ys = list(range(y1, y0-1, -meta_size[1])) ll = (xs[0], ys[-1], level) ur = (xs[-1], ys[0], level) # add meta_size to get full affected bbox ur = ur[0]+meta_size[0]-1, ur[1]+meta_size[1]-1, ur[2] abbox = self.grid._tiles_bbox([ll, ur]) return (abbox, (len(xs), len(ys)), _create_tile_list(xs, ys, level, self.grid.grid_sizes[level])) class MetaTile(object): def __init__(self, bbox, size, tile_patterns, grid_size): self.bbox = bbox self.size = size self.tile_patterns = list(tile_patterns) self.grid_size = grid_size @property def tiles(self): return [t[0] for t in self.tile_patterns] @property def main_tile_coord(self): """ Returns the "main" tile of the meta tile. This tile(coord) can be used for locking. >>> t = MetaTile(None, None, [((0, 0, 0), (0, 0)), ((1, 0, 0), (100, 0))], (2, 1)) >>> t.main_tile_coord (0, 0, 0) >>> t = MetaTile(None, None, [(None, None), ((1, 0, 0), (100, 0))], (2, 1)) >>> t.main_tile_coord (1, 0, 0) """ for t in self.tiles: if t is not None: return t def __repr__(self): return "MetaTile(%r, %r, %r, %r)" % (self.bbox, self.size, self.grid_size, self.tile_patterns) def bbox_intersects(one, two): a_x0, a_y0, a_x1, a_y1 = one b_x0, b_y0, b_x1, b_y1 = two if ( a_x0 < b_x1 and a_x1 > b_x0 and a_y0 < b_y1 and a_y1 > b_y0 ): return True return False def bbox_contains(one, two): """ Returns ``True`` if `one` contains `two`. >>> bbox_contains([0, 0, 10, 10], [2, 2, 4, 4]) True >>> bbox_contains([0, 0, 10, 10], [0, 0, 11, 10]) False Allow tiny rounding errors: >>> bbox_contains([0, 0, 10, 10], [0.000001, 0.0000001, 10.000001, 10.000001]) False >>> bbox_contains([0, 0, 10, 10], [0.0000000000001, 0.0000000000001, 10.0000000000001, 10.0000000000001]) True """ a_x0, a_y0, a_x1, a_y1 = one b_x0, b_y0, b_x1, b_y1 = two x_delta = abs(a_x1 - a_x0) / 10e12 y_delta = abs(a_y1 - a_y0) / 10e12 if ( a_x0 <= b_x0 + x_delta and a_x1 >= b_x1 - x_delta and a_y0 <= b_y0 + y_delta and a_y1 >= b_y1 - y_delta ): return True return False def deg_to_m(deg): return deg * (6378137 * 2 * math.pi) / 360 OGC_PIXEL_SIZE = 0.00028 #m/px def ogc_scale_to_res(scale): return scale * OGC_PIXEL_SIZE def res_to_ogc_scale(res): return res / OGC_PIXEL_SIZE def resolution_range(min_res=None, max_res=None, max_scale=None, min_scale=None): if min_scale == max_scale == min_res == max_res == None: return None if min_res or max_res: if not max_scale and not min_scale: return ResolutionRange(min_res, max_res) elif max_scale or min_scale: if not min_res and not max_res: min_res = ogc_scale_to_res(max_scale) max_res = ogc_scale_to_res(min_scale) return ResolutionRange(min_res, max_res) raise ValueError('requires either min_res/max_res or max_scale/min_scale') class ResolutionRange(object): def __init__(self, min_res, max_res): self.min_res = min_res self.max_res = max_res if min_res and max_res: assert min_res > max_res def scale_denominator(self): min_scale = res_to_ogc_scale(self.max_res) if self.max_res else None max_scale = res_to_ogc_scale(self.min_res) if self.min_res else None return min_scale, max_scale def scale_hint(self): """ Returns the min and max diagonal resolution. """ min_res = self.min_res max_res = self.max_res if min_res: min_res = math.sqrt(2*min_res**2) if max_res: max_res = math.sqrt(2*max_res**2) return min_res, max_res def contains(self, bbox, size, srs): width, height = bbox_size(bbox) if srs.is_latlong: width = deg_to_m(width) height = deg_to_m(height) x_res = width/size[0] y_res = height/size[1] if self.min_res: min_res = self.min_res + 1e-6 if min_res <= x_res or min_res <= y_res: return False if self.max_res: max_res = self.max_res if max_res > x_res or max_res > y_res: return False return True def __eq__(self, other): if not isinstance(other, ResolutionRange): return NotImplemented return (self.min_res == other.min_res and self.max_res == other.max_res) def __ne__(self, other): if not isinstance(other, ResolutionRange): return NotImplemented return not self == other def __repr__(self): return '' % ( self.min_res or 9e99, self.max_res or 0) def max_with_none(a, b): if a is None or b is None: return None else: return max(a, b) def min_with_none(a, b): if a is None or b is None: return None else: return min(a, b) def merge_resolution_range(a, b): if a and b: return resolution_range(min_res=max_with_none(a.min_res, b.min_res), max_res=min_with_none(a.max_res, b.max_res)) return None mapproxy-1.11.0/mapproxy/image/000077500000000000000000000000001320454472400164175ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/image/__init__.py000066400000000000000000000367451320454472400205470ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Image and tile manipulation (transforming, merging, etc). """ import io from io import BytesIO from mapproxy.compat.image import Image, ImageChops from mapproxy.image.opts import create_image, ImageFormat from mapproxy.config import base_config from mapproxy.srs import make_lin_transf from mapproxy.compat import string_type import logging from functools import reduce log = logging.getLogger('mapproxy.image') magic_bytes = [ ('png', (b"\211PNG\r\n\032\n",)), ('jpeg', (b"\xFF\xD8",)), ('tiff', (b"MM\x00\x2a", b"II\x2a\x00",)), ('gif', (b"GIF87a", b"GIF89a",)), ] def peek_image_format(buf): buf.seek(0) header = buf.read(10) buf.seek(0) for format, bytes in magic_bytes: if header.startswith(bytes): return format return None class ImageSource(object): """ This class wraps either a PIL image, a file-like object, or a file name. You can access the result as an image (`as_image` ) or a file-like buffer object (`as_buffer`). """ def __init__(self, source, size=None, image_opts=None, cacheable=True): """ :param source: the image :type source: PIL `Image`, image file object, or filename :param format: the format of the ``source`` :param size: the size of the ``source`` in pixel """ self._img = None self._buf = None self._fname = None self.source = source self.image_opts = image_opts self._size = size self.cacheable = cacheable def _set_source(self, source): self._img = None self._buf = None if isinstance(source, string_type): self._fname = source elif isinstance(source, Image.Image): self._img = source else: self._buf = source def _get_source(self): return self._img or self._buf or self._fname source = property(_get_source, _set_source) def close_buffers(self): if self._buf: try: self._buf.close() except IOError: pass if self._img: self._img = None @property def filename(self): return self._fname def as_image(self): """ Returns the image or the loaded image. :rtype: PIL `Image` """ if not self._img: self._make_seekable_buf() log.debug('file(%s) -> image', self._fname or self._buf) try: img = Image.open(self._buf) except Exception: self.close_buffers() raise self._img = img if self.image_opts and self.image_opts.transparent and self._img.mode == 'P': self._img = self._img.convert('RGBA') return self._img def _make_seekable_buf(self): if not self._buf and self._fname: self._buf = open(self._fname, 'rb') else: try: self._buf.seek(0) except (io.UnsupportedOperation, AttributeError): # PIL needs file objects with seek self._buf = BytesIO(self._buf.read()) def _make_readable_buf(self): if not self._buf and self._fname: self._buf = open(self._fname, 'rb') elif not hasattr(self._buf, 'seek'): if not isinstance(self._buf, ReadBufWrapper): self._buf = ReadBufWrapper(self._buf) else: try: self._buf.seek(0) except (io.UnsupportedOperation, AttributeError): # PIL needs file objects with seek self._buf = BytesIO(self._buf.read()) def as_buffer(self, image_opts=None, format=None, seekable=False): """ Returns the image as a file object. :param format: The format to encode an image. Existing files will not be re-encoded. :rtype: file-like object """ if format: image_opts = (image_opts or self.image_opts).copy() image_opts.format = ImageFormat(format) if not self._buf and not self._fname: if image_opts is None: image_opts = self.image_opts log.debug('image -> buf(%s)' % (image_opts.format,)) self._buf = img_to_buf(self._img, image_opts=image_opts) else: self._make_seekable_buf() if seekable else self._make_readable_buf() if self.image_opts and image_opts and not self.image_opts.format and image_opts.format: # need actual image_opts.format for next check self.image_opts = self.image_opts.copy() self.image_opts.format = peek_image_format(self._buf) if self.image_opts and image_opts and self.image_opts.format != image_opts.format: log.debug('converting image from %s -> %s' % (self.image_opts, image_opts)) self.source = self.as_image() self._buf = None self.image_opts = image_opts # hide fname to prevent as_buffer from reading the file fname = self._fname self._fname = None self.as_buffer(image_opts) self._fname = fname return self._buf @property def size(self): if self._size is None: self._size = self.as_image().size return self._size def SubImageSource(source, size, offset, image_opts, cacheable=True): """ Create a new ImageSource with `size` and `image_opts` and place `source` image at `offset`. """ # force new image to contain alpha channel new_image_opts = image_opts.copy() new_image_opts.transparent = True img = create_image(size, new_image_opts) if not hasattr(source, 'as_image'): source = ImageSource(source) subimg = source.as_image() img.paste(subimg, offset) return ImageSource(img, size=size, image_opts=new_image_opts, cacheable=cacheable) class BlankImageSource(object): """ ImageSource for transparent or solid-color images. Implements optimized as_buffer() method. """ def __init__(self, size, image_opts, cacheable=False): self.size = size self.image_opts = image_opts self._buf = None self._img = None self.cacheable = cacheable def as_image(self): if not self._img: self._img = create_image(self.size, self.image_opts) return self._img def as_buffer(self, image_opts=None, format=None, seekable=False): if not self._buf: image_opts = (image_opts or self.image_opts).copy() if format: image_opts.format = ImageFormat(format) image_opts.colors = 0 self._buf = img_to_buf(self.as_image(), image_opts=image_opts) return self._buf def close_buffers(self): pass class ReadBufWrapper(object): """ This class wraps everything with a ``read`` method and adds support for ``seek``, etc. A call to everything but ``read`` will create a StringIO object of the ``readbuf``. """ def __init__(self, readbuf): self.ok_to_seek = False self.readbuf = readbuf self.stringio = None def read(self, *args, **kw): if self.stringio: return self.stringio.read(*args, **kw) return self.readbuf.read(*args, **kw) def __iter__(self): if self.stringio: return iter(self.stringio) else: return iter(self.readbuf) def __getattr__(self, name): if self.stringio is None: if hasattr(self.readbuf, name): return getattr(self.readbuf, name) elif name == '__length_hint__': raise AttributeError self.ok_to_seek = True self.stringio = BytesIO(self.readbuf.read()) return getattr(self.stringio, name) def img_has_transparency(img): if img.mode == 'P': if img.info.get('transparency', False): return True # convert to RGBA and check alpha channel img = img.convert('RGBA') if img.mode == 'RGBA': # any alpha except fully opaque return any(img.histogram()[-256:-1]) return False def img_to_buf(img, image_opts): defaults = {} image_opts = image_opts.copy() # convert I or L images to target mode if image_opts.mode and img.mode[0] in ('I', 'L') and img.mode != image_opts.mode: img = img.convert(image_opts.mode) if (image_opts.colors is None and base_config().image.paletted and image_opts.format.endswith('png')): # force 255 colors for png with globals.image.paletted image_opts.colors = 255 format = filter_format(image_opts.format.ext) if format == 'mixed': if img_has_transparency(img): format = 'png' else: format = 'jpeg' image_opts.colors = None image_opts.transparent = False # quantize if colors is set, but not if we already have a paletted image if image_opts.colors and not (img.mode == 'P' and len(img.getpalette()) == image_opts.colors*3): quantizer = None if 'quantizer' in image_opts.encoding_options: quantizer = image_opts.encoding_options['quantizer'] if image_opts.transparent: img = quantize(img, colors=image_opts.colors, alpha=True, defaults=defaults, quantizer=quantizer) else: img = quantize(img, colors=image_opts.colors, quantizer=quantizer) if hasattr(Image, 'RLE'): defaults['compress_type'] = Image.RLE buf = BytesIO() if format == 'jpeg': img = img.convert('RGB') if 'jpeg_quality' in image_opts.encoding_options: defaults['quality'] = image_opts.encoding_options['jpeg_quality'] else: defaults['quality'] = base_config().image.jpeg_quality # unsupported transparency tuple can still be in non-RGB img.infos # see: https://github.com/python-pillow/Pillow/pull/2633 if format == 'png' and img.mode != 'RGB' and 'transparency' in img.info and isinstance(img.info['transparency'], tuple): del img.info['transparency'] img.save(buf, format, **defaults) buf.seek(0) return buf def quantize(img, colors=256, alpha=False, defaults=None, quantizer=None): if hasattr(Image, 'FASTOCTREE') and quantizer in (None, 'fastoctree'): if not alpha: img = img.convert('RGB') try: if img.mode == 'P': # quantize with alpha does not work with P images img = img.convert('RGBA') img = img.quantize(colors, Image.FASTOCTREE) except ValueError: pass else: if alpha and img.mode == 'RGBA': img.load() # split might fail if image is not loaded alpha = img.split()[3] img = img.convert('RGB').convert('P', palette=Image.ADAPTIVE, colors=colors-1) mask = Image.eval(alpha, lambda a: 255 if a <=128 else 0) img.paste(255, mask) if defaults is not None: defaults['transparency'] = 255 else: img = img.convert('RGB').convert('P', palette=Image.ADAPTIVE, colors=colors) return img def filter_format(format): if format.lower() == 'geotiff': format = 'tiff' if format.lower().startswith('png'): format = 'png' return format image_filter = { 'nearest': Image.NEAREST, 'bilinear': Image.BILINEAR, 'bicubic': Image.BICUBIC } def is_single_color_image(image): """ Checks if the `image` contains only one color. Returns ``False`` if it contains more than one color, else the color-tuple of the single color. """ result = image.getcolors(1) # returns a list of (count, color), limit to one if result is None: return False color = result[0][1] if image.mode == 'P': palette = image.getpalette() return palette[color*3], palette[color*3+1], palette[color*3+2] return result[0][1] def make_transparent(img, color, tolerance=10): """ Create alpha channel for the given image and make each pixel in `color` full transparent. Returns an RGBA ImageSoruce. Modifies the image in-place, unless it needs to be converted first (P->RGB). :param color: RGB color tuple :param tolerance: tolerance applied to each color value """ result = _make_transparent(img.as_image(), color, tolerance) image_opts = img.image_opts.copy() image_opts.transparent = True image_opts.mode = 'RGBA' return ImageSource(result, size=result.size, image_opts=image_opts) def _make_transparent(img, color, tolerance=10): img.load() if img.mode == 'P': img = img.convert('RGBA') channels = img.split() mask_channels = [] for ch, c in zip(channels, color): # create bit mask for each matched color low_c, high_c = c-tolerance, c+tolerance mask_channels.append(Image.eval(ch, lambda x: 255 if low_c <= x <= high_c else 0)) # multiply channel bit masks to get a single mask alpha = reduce(ImageChops.multiply, mask_channels) # invert to get alpha channel alpha = ImageChops.invert(alpha) if len(channels) == 4: # multiply with existing alpha alpha = ImageChops.multiply(alpha, channels[-1]) img.putalpha(alpha) return img def bbox_position_in_image(bbox, size, src_bbox): """ Calculate the position of ``bbox`` in an image of ``size`` and ``src_bbox``. Returns the sub-image size and the offset in pixel from top-left corner and the sub-bbox. >>> bbox_position_in_image((-180, -90, 180, 90), (600, 300), (-180, -90, 180, 90)) ((600, 300), (0, 0), (-180, -90, 180, 90)) >>> bbox_position_in_image((-200, -100, 200, 100), (600, 300), (-180, -90, 180, 90)) ((540, 270), (30, 15), (-180, -90, 180, 90)) >>> bbox_position_in_image((-200, -50, 200, 100), (600, 300), (-180, -90, 180, 90)) ((540, 280), (30, 20), (-180, -50, 180, 90)) >>> bbox_position_in_image((586400,196400,752800,362800), (256, 256), (586400,196400,752800,350000)) ((256, 237), (0, 19), (586400, 196400, 752800, 350000)) """ coord_to_px = make_lin_transf(bbox, (0, 0) + size) offsets = [0, size[1], size[0], 0] sub_bbox = list(bbox) if src_bbox[0] > bbox[0]: sub_bbox[0] = src_bbox[0] x, y = coord_to_px((src_bbox[0], 0)) offsets[0] = int(x) if src_bbox[1] > bbox[1]: sub_bbox[1] = src_bbox[1] x, y = coord_to_px((0, src_bbox[1])) offsets[1] = int(y) if src_bbox[2] < bbox[2]: sub_bbox[2] = src_bbox[2] x, y = coord_to_px((src_bbox[2], 0)) offsets[2] = int(x) if src_bbox[3] < bbox[3]: sub_bbox[3] = src_bbox[3] x, y = coord_to_px((0, src_bbox[3])) offsets[3] = int(y) size = abs(offsets[2] - offsets[0]), abs(offsets[1] - offsets[3]) return size, (offsets[0], offsets[3]), tuple(sub_bbox) mapproxy-1.11.0/mapproxy/image/fonts/000077500000000000000000000000001320454472400175505ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/image/fonts/DejaVuSans.ttf000066400000000000000000022767041320454472400223140ustar00rootroot000000000000000FFTMQ|<GDEFX]XGPOS/GSUBN֘LOS/2! Vcmap .cvt i9<fpgmq4vj<gasp glyf}o2|head.ep6hhea $hmtxvNUhkernk/6i4<~locaJTUlmaxpqi namepMi@=postL3Hprep; x\hɲ4rmrm "W           "##$HIIJLMQRpq|}     "#     ; < < = I J P Q Q R U V W X o p q r   ~!"qr|}2334Y \DFLTzarabarmnbraicanschercyrlgeorgrekhanihebrkana*lao 6latnFmathnko ogamrunrtfngthaiKUR SND URD MKD SRB 4ISM 4KSM 4LSM 4MOL 4NSM 4ROM 4SKS 4SSM 4 kern8kern>markFmarkTmark\markdmkmkjmkmkrmkmkx    "*2:BLT\dlt|x8  8 >/024<7j7:I`j0&:  sv{sv{ &,28>DJPV\bhntz::::r 4 4 `Iqrtuwxyz|Iqrtuwxyz|JPV\bhntz$ l N>X  &,lwlwlwfn " &,28l`l~l~l`l~l`Z& #HNTZ`flrx~tt;888  !"    ! "(.4:@FB :v| $*06<BHNTZ`flrx~hhh=DhhhDhh=DDnnnnhh   !# J P)rr0tt1v|2339  J P%r|,3378 $*06<BHNTZ`flrx~ &,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{ $6HZl~ cj cj cj cj c c cj cj#sv{>DJPV\bhntz*  &,28>DJPV\bhntz "(.4:@FLRX^djpv| $*06<BHNTZ`flrx~ &,28>DJPV\bhntz "(.4:@FLRX^djpv|     $ * 0 6 < B H N T Z ` f l r x ~      & , 2 8 > D J P V \ b h n t z     " ( . 4 : @ F L R X ^ d j p v |     $ * 0 6 < B H N T Z ` f l r x ~      & , 2 8 > D J P V \ b h n t z  "(.4:@FLRX^djpv| $*06<BHNTZ`flrx~{U:t!N8'Qn ppjjj,v,,vjj  XXXXD[j[j, 8 8>>j pjjj^jj,,,,,,,     8 8 8 j j>>, ppjI^`k/#eYYYcP`{U:tii!NQnU!Q{++++++jj++jj++jj++ 8 8jj 8 8jj,,X X ,,XX,,X X ,,X X      j j,j,j j j,j,j>  ++pp++,,,, ,,,,,,,,,,2  pp++pp++jjjj++jj++,,XX,,XX,,XXjjjj    XXjjXXjjXX&j&jXX&j&j[j[jSjSj[j[jSjSjXX 8 8jjjj 8 8,j,j>>SS&j&j>++jjj  pp++j++ 8jjjj++^++j++,XX,XX,XX,X X   >SSp++ jIII^^^```kkk///###eeeYYYYYYYYY$AMpBDcs~2#sv{BHNTZ`flrx~F 'PV\bhntz "(.4U0+0008q00800i00E0 0100000P=i0v00v00d000UU8000U $*,022 45 78:> "0 $6HZl~ cr cr cr cr cr cr cr crIqrtuwxyz|RX^djpv|``& b lrx~ &,28>DJPV\bhntz "(.4:@FLRX^djpv| $*06<BHNTZ`flrx~ &,28>DJPV\bhntz "(.4:@FLRX^djpv|     $ * 0 6 < B H N T Z ` f l r x ~      & , 2 8 > D J P V \ b h n t z     " ( . 4 : @ F L R X ^ d j p v |     $ * 0 6 < B H N T Z ` f l r x ~      & , 2 8 > D J P V \ b h n t z R``S`4rrLRLX X X X [r[r~x,LLRLLRLxLLLxx4RI^`n#YYY`R``S`++++++LL++LL++++LL@LL@XXXXXXXXxxxxxx++XV++,,,:,,,,:,:,,,,,:,:LrrX+F+Frr++L&LRR++LL++XXXXX~X~X X X X RRX X & & X X &&[r[rSrSr[r[rSrSr~~x~x~LLFLRFSrSrR&R&R++R&RL XVX++++LLRL++R++++XXXxXxXxXxX~X~4S4S4++&RIII^^^```nnn###YYYYYYYYY%%//88Mp')Hfghij4?QZ2[Iqrtuwxyz|rx~`{{{{{{{{` <BHNTZ`flrx~]xx@[")@>E"~~x2x::-."> @FLRX^djpv|]kxyyyxyz[f"w)h>yEy`P["~[~t`zxy2{`uxJJ::++-.   !" 28>DJPV\bhnttbbbbt`~~`~` Z R   !"# $*06<BHNTZ Y &,28>DJPV\bhntz "(.4:@FLRX^djpv| $=D]468QT9Zd=grHw{T D6L  $Js}- {{8> 7pv| $*06<BHNTZ`flrx~L/'s.}////////s}/////s{y5D;/}R7$&(,268DFHLRVX-* [\]^klz?   $*06<BHNTZ`flrx~ &,28>DJPV\bhntz "(.4:@FLRX^djpv| $*06<BHNTZ`flrx~ &,28>DJPV\bhntz "(.4:@FLRX^djpv|     $ * 0 6 < B H N T Z ` f l r x ~      & , 2 8 > D J P V \ b h n t z     " ( . 4 : @ F L R X ^ d j p v | L\/.Rs''}srJf;RRsRR%}^Gb`R////}}J////Rs}f7R/'z`RR///.RR'}r`RTTRTcRRJ@@RjRjRbRb}RRRRRRRR}R555RRaRt;Q'RRRRRRR}}^G^dRRR::R'aHRR_R:RGR R~RJ}'/'}'}^TTT@X}Tg^GX^//LBRRf,4$R'_zRf4L}`ReT'sR^G^5s/RRwRRJV1vvvR;nR RRL5s/<\R&Rx9\wR}RO$= D]$>BCHIJKRT  UV--WEEXNNYTTZYY[aa\ll]vv^{{_`bf iJqLmFFIILL""3366|}346;>FJNRW S S [ ^ ` b e e h m t t x x | }      ~ ~ "36QT7VW;Yd=grIw{UZ&&m((n]foy{|}??~   !"# $*06<BHNTZn 6ntz "(.4:@FNT\bjpv| $*06<BHNTZ`flrx~ &,28>DJPV\bhntz &,28>DJPV\bhntz     " ( . 4 : @ F L R X ^ d j p v |      & , 2 8 > D J P V \ b h n t z     " ( . 4 : @ F L R X ^ d j p v |     $ * 0 6 < B H N T Z ` f l r x ~      & , 2 8 > D J P V \ b h n t z  "*06<BHNTZ`fntz "(.4:@FLRX^djpv|  &,28>DJPV\bhntz &,28>DJPV\bhntzL\/.*s''}srJ{#{{;j{//{{s{ {o{{'{}{^{G{b{`{{'{{}}{Q{{{{}{\LX;\//''{ssr`{{{'{{{./'}{r`{{T{{{{c{R{R{J|@{@{{{jj{{b{b{}{/{{{{{{{{}{{{3{33{^{a{p{{;{Q{'{{{}{}{^{G{^d{{{{{::'a{H{{/{{j:{G{ J{~^{}J|E{}{{{{E}{p{{t{}{j{{{b{{^{~~{}{t{^{{{{/'{{{H/rtOs}s'LsoqGGYNsT{a{E{{{{@{{t{{{{}{{{{T{`{kb{{K{{{{{{{{t{{'{///{{4{^{c{s{"{O{,s{O{%'}{{{O{t{t{e{sK{{{{'}{E{b{{{{^{{{T{{TT{{@{{{{{}{{{{{{{{{T{g{b{^{G{{{{^{{LBRf,4${' _zf4DL}1{`{e**}T{{'s^{G{^{{{3s{{/ {0{{{{'l{n{T{T{wJ{{{1{{v{vqv{*\;{n{{{L5s/<\&Rx9{\{w{{{{{}{m$= D]$>?ABCDFG  HI55JBBKEELHIMNNOPPPRVQXYV[]X__[aa\ff]ij^lp`txe{{jkl pJxLmE]deiikkmmooww.DL ["#a*+c.6e@@nMMoXXp^^qbbr|}suvxyz3;{>GJNQW[[ S S [ ^ ` b e e h m t t x x | }      ~ ~$'*02588::QTVWYdgrw{ && ((!]f",/0124??5  "#( J P.r|533@A$*06>FLRX`flrx~ &,28>FNTZ`flrx~{{{{{{{{{{{{{{{{{{orr{r{{{{{{{{{{{{{{A{{{{{{{{{{{{{{ {{{{{{{{{{{{{{&!0#5PKr9KD &&K9a}au9aauaau/&DaDDkkDDDDkDD)ak}/DDa9}D}&&9}k}k}&D aDY}aaauNaaau}}k}ka aakkAk&k}}DHVaD)kkDN9a}au9aau/9a}au9aau/9a}au9aau/&kD&9a}au9aau/9a}a9aa/D?}DVD aDKr9KD &&Kk}k&/<&O$$%%&&''))**++-- .. // 22 33 445566778899::;;<<==HHIINNQQRRUUYYZZ[[ \\!mm"}}#$%&%'( )*+!!,,-((. /  0  ""&&100::?? 2 3 4$$%%&&''))** ++-- ./22 3344 5566 778899::;;<<==DDFFGGHHIIJKLLOOPPQQRRTTUUVV WW!XX"YY#ZZ$[[%\\&mm'}}()* ++,,-../"/&&010101234352678888393:;;  3<3<=<;    !! "" ## $$>%%5&&''!((?++@--@//@0011"33@55@66A77B88C99D::??4EFEF G43H4IJ H HA I IK J JL K KB L LA M MB C D M N O^$%&')*+-./23456789:;<=HINQRUYZ[\m}  "&0:? `$XAYAEGKMQ SW .DFLTzarabarmnbraicanschercyrlgeorgrek"hani2hebr>kanaPlao \latnhmathnko ogamrunrtfng thaiKUR SND (URD (  MKD SRB  4ISM FKSM FLSM FMOL ZNSM FROM ZSKS FSSM F    aaltaaltaaltccmpccmpccmpccmpdligdligdligfinafinahlighliginitinit ligaligalocl locl&locl,medi2medi8rlig>rligHsaltPsaltVsalt\     'PX`h "*2:BJRZbjrzJ\ nvX               p   *  H   R !$% B6##66>9LM *_ J K L M i$=EEGGIIKKLMNOWW      ""$$&&((**,,..0022446688:;==??AAHHRRTTVV  **__  J M &   &$$4F!!$$4F""$$4F##$$4F$$$$4F%%(0AD&.6EEGI&.6JKMN&.6OQSS&(0TW&v6Pblv",6PZd        $%&'()*,-./024578:;<=>A B  !$'*-0D$&(*,0268<@DHLNPRTX\`dhlptx|Megp#%B  "%(+.1l3.4:>BFJVZ^bfjnrvz~ QQSSUY^egmop)1B  #&),/2l3-39=AEIUY]aeimquy} QQSSUY^egmop)12  ww vssvw~&8Jlww zw zw utrq utqrtuwz 00> $*&$*&$J 8 "(IOILOLI OLIRl$*06< xwvutsrqP{NzMy &,!xwvutqOzQzRfnp0$B 8  WVWA(:FPZfr "   " $; <V p0 q(/ QF WX VR")567DF o ogfhdeji#9?FLTZgfhdeji#9?FLTZ,-DO *"&?,-DO\  X6 usvtyzr3{w|x  ! LM *_ YYYYY33f . `)PfEd@ m,$, x~OSXZbw~%V_  :UZot?5JR>PjGv#.[jx{EMWY[]} ' d q ! !I!K!N!###!#(#,#u#z#}######$#$i&&'' '''K'M'R'V'^''''''()) )A))))***/***++$+T,o,w,}-e-o..%..MGMQWn+?KO6<>ADO#t QWZ\pz 1Ya  !@Z`ty? 7LT@RtFn&0]w{ HPY[]_ * j t !! !K!N!S!###$#+#s#z#}######$"$`%&''' ')'M'O'V'X'a'''''')) )@))))** */*}**+++S,`,q,y-0-o.."..MDLPTb&0FN8>@CFR pv^\TO>=<4/,+&" mljiga`_^][ZYWVUSQ~}|zyqpofbY+)320/." zvsoQPOMI>8642\ ?><;:96530/) A,gb^0%$#qhkkkkkkkk5k1k+k'k!kjjj|"|snmlkjig_W ~bOQSWXZZ\bpwz~"#7 %1VY_a "$?D  F  HIJK!:L@UfZZ|`o}tty??#-/U 57JLRT:e>@PRjt  FG nv 3#H&.V0[_]jwx{{ d   E HM PW YY [[ ]] _}  2 g v    ' * d j q t  / 4 J L N P!! Q! !I [!K!K !N!N !S! !# ## P##! R#$#( X#+#, ]#s#u _#z#z b#}#} c## d## e## f## z## |## }$"$# ~$`$i %& &&'''@'' D' ''H')'Kd'M'M'O'R'V'V'X'^'a''''''''''''())) )  )@)A )) ))))))*** **/*/.*}*/**S**`++b++$}+S+T,`,o,q,w,y,}-0-e-o-o...".%....MMDGLM"PQ$TW&bn*7;=L&+Q0?WFKgNOmosw|~68<>>@ACDFOR #ptvV89;>@DFFJPRkՠ)]   !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`a rdei xpk rvj \s gw @ O MT il|=cn XT Dm} b T: @  y qz5fqu-J3T99NR7s`s3VV9s3D{o{RoHT3fs +b-{T#\q#H99`#fy```{w``b{{Rffw;{J/}oo5jo{-{T7fD)fs, %Id@QX Y!-,%Id@QX Y!-,  P y PXY%%# P y PXY%-,KPX EDY!-,%E`D-,KSX%%EDY!!-,ED-,%%I%%I` ch #:e:-ff@ /10!%!!fsr)5 5@ K TX8Y<2991/0 P ]%3#3#5qeB@KTKT[X8Y1<20@0 @ P ` p ]#!#o$++`@1      91/<<<<<<<2220@   ]!! !3!!!!#!#!5!!5!T%Dh$ig8R>hggh`TifaabbNm!(/@U" '&( /)/))/B" ) *!#*- ) " & 0K TX8YK TKT[KT[X@8Y<<<1/299990KSX99Y"#.'5.546753.'>54&dijfod]SS\dtzq{---@A$*.U# jXV`OnZXhq) #'3@6$%&%&'$'B .$ &($4'!%   ! + 1 4K TK T[K T[KT[KT[K T[X18Y9912<0KSXY""32654&'2#"&546"32654&%3#2#"&546WccWUccUVcbWWcd1Zܻۻa ۻۼ 0@      !         B  (('+'$ .  .'.'!!199999991/9990KSX99999999Y"2]@ " ) **&:4D ^YZ UZZY0g{ "-  ' (   2'') #**(/2; 49?2J LKFO2VZ Y UY\_2j i`2uy z 2229]]3267 >73#'#"5467.54632.#"[UԠ_I{;B h]hΆ02޸SUWDi;#QX?@Yr~YW׀c?}<$$/1oX3go7@ KTKT[X8Y10@ @P`p]#o+{ 7@  KTX 8YKTX @8Y29910#&547{>;o @ <99103#654<:=JN@,       <2<2991<22990 %#'-73%g:r:g:PrPbybcy #@   <<1/<<0!!#!5!-Ө-Ӫ--@ 1073#ӤR@d10!!d1/073#B-@B/9910KSXY"3#m #@  10"32'2#"  P3343ssyzZ @@B  KTX@8Y1/20KSXY"]7!5%3!!JeJsHHժJ@'B   KTKT[KT[X8Y91/20KSX9Y"@2UVVzzvtvust]]%!!567>54&#"5>32Ls3aM_xzXE[w:mIwBC12\ps(p@.    #)&  )KTKT[X 8Y99190@ daa d!]!"&'532654&+532654&#"5>32?^jTmǹSrsY %Đ%%12wps{$& Ѳ|d @   B    K TK T[X 8Y<291/<290KSXY"@* *HYiw+&+6NO O Vfuz ]] !33##!55^%3`d^@#    KTKT[X8YKTX@8Y190!!>32!"&'532654&#",X,$^hZkʭQTժ 10$& $X@$  "% " !%190@]]"32654&.#">32# !2 LL;kPL;y$&W]ybhc@B991/0KSXY"KTX@878Y@X9Hg]]!#!3V+ #/C@% '-'0 $*$ !0991990"32654&%.54$32#"$54632654&#"HŚV г "Əُattt$X@# %!"" %190@]]7532#"543 !"&2654&#"LK:lL>$& V\s[#@<21/073#3### %@  <2103#3#ӤR#٬@^M@*B$#29190KSXY" 5Ѧ`@ #<210!!!!^O@+B$#<9190KSXY"55//m$e@+$     &%K TX8Y99991/9990y z z ]%3##546?>54&#"5>32ſ8ZZ93lOa^gHZX/'eVY5^1YnFC98ŸLVV/5<4q L@2  L4307$7CM34( (+(I+*(I,=M<9912990K TK T[KT[KT[KT[XMMM@878Y@ NN/N?N]32654&#"#"&5463253>54&'&$#"3267#"$'&5476$32|{zy!orqp ˘s'6@   0210].# !267# !2'ffjzSb_^^_HHghG.@   2 99991/0`]3 !%! )5BhPa/w.,~ .@   21/0 ]!!!!!!9>ժF# )@ 21/0 ]!!!!#ZpPժH7s9@ 43 1990%!5!# !2.# !26uu^opkSUmnHF_`%; ,@ 8  221/<20P ]3!3#!#"d+9.KTX@8Y1/0@ 0@P`]3#+f B@  9 KTX@8Y991990@ 0 @ P ` ]3+53265M?nj @(B  291/<290KSXY"]@ ((764GFCUgvw    (+*66650 A@E@@@ b`hgwp  ,]q]q3! !#3wH1j%@ :1/0@ 0P]3!!_ժ @4  B    >  91/<290KSXY"p]@V   && & 45 i|{y   #,'( 4<VY ej vy ]]! !###-}-+3 y@B6 991/<2990KSXY" ]@068HGif FIWXeiy ]]!3!#j+s #@  310"32' ! ':xyLHH[[bb:@   ? 291/0@ ?_]32654&#%!2+#8/ϒs R@*  B     39991990KSX9Y""32#'# ! '? !#y;:xLHHab[T@5  B    ?  299991/<9990KSX9Y"@]@Bz%%%&'&&& 66FFhuuw]]#.+#! 32654&#A{>ٿJx~hb؍O'~@<    B %( "-"(9999190KSX99Y")])/)O)].#"!"&'532654&/.54$32Hs_wzj{r{i76vce+ٶ0/EF~n|-&J@@@1/20K TX@878Y@  @ p ]!!#!ժ+)@@   8AKTX8Y1299990]332653! ˮ®u\*$h@'B91/290KSXY"P]@b*GGZ} *&&))% 833<<7HEEIIGYVfiizvvyyu)]]!3 3J+D {@I      B     91/<2290KSXY"]@  ($ >>4 0 LMB @ Yjkg ` {|      !   # $ %  <:5306 9 ? 0FFJ@E@BBB@@ D M @@XVY Pfgab```d d d wv{xwtyywpx   []]3 3 3# #D:9:9+=; f@  1 ]@ /<20KBPX@   @    Y3 3 # #su \Y+3{@(B@@ 91/290KSXY" ]@<5000F@@@QQQe &)78@ ghxp ]]3 3#f9\ @BB K TK T[X8Y991/0KSXY"@@ )&8HGH    / 59? GJO UYfio wx ]]!!!5!sP=g՚oX;@CK TX@8YKTKT[X8Y210!#3!XB-@B/9910KSXY"#mo0@CKTKT[X@8Y<10!53#5oXޏ@ 91290 # #HHu-10!5f1@ D10K TKT[X@878Y #ofv{-{ %@'   #   E&22991/9990@n0000 0!0"?'@@@@ @!@"PPPP P!P"P'p' !"'''000 0!@@@ @!PPP P!``` `!ppp p! !]]"326=7#5#"&5463!54&#"5>32߬o?`TeZ3f{bsٴ)Lfa..'' 8@  G F221/0`]4&#"326>32#"&'#3姒:{{:/Rdaadq{?@  HE210@ ].#"3267#"!2NPƳPNM]-U5++++$$>:#qZ8@G E221/0`]3#5#"3232654&#":||ǧ^daDDaq{p@$   KE9190@)?p?????,// , ooooo ]q]!3267# 32.#" ͷjbck)^Z44*,8 Cė/Y@     LK TX @8YKTX 8Y<<991/22990@P]#"!!##535463cM/ѹPhc/яNqVZ{ (J@#  &#' & G E)221/990`***]4&#"326!"&'5326=#"3253aQQR9||9=,*[cb::bcd4@  N  F21/<90`]#4&#"#3>32d||Bu\edy+@F<21/0@  @ P ` p ]3#3#`Vy D@   O  F<2991990@ @P`p]3+532653#F1iL`a( @)B F 291/<90KSXY" ]@_ ')+Vfgsw    ('(++@ h` ]q]33 ##%kǹi#y"F1/0@ @P`p]3#{"Z@&   PPF#291/<<<290@0$P$p$$$$$$$ ]>32#4&#"#4&#"#3>32)Erurw?yz|v\`gb|d{6@  N  F21/<90`]#4&#"#3>32d||Bu\`edqu{ J@  QE10@#?{{   {  {]"32654&'2#"s98V{>@ GF2210@ `]%#3>32#"&4&#"326s:{{8 daaqVZ{ >@   GE2210@ `]32654&#"#"3253#/s:||:/daDDadJ{0@    F21/90P].#"#3>32JI,:.˾`fco{'@<  S  SB %( R"E(9999190KSX99Y"']@m   . , , , ; ; ; ; $( ( *//*(() )!$'      '/)?)_))))))]]q.#"#"&'532654&/.54632NZb?ĥZlfae@f?((TT@I!*##55YQKP%$78@  F<<2991/<2990]!!;#"&5#53w{KsբN`>X`;@    NF921/290o]332653#5#"&||Cua{fc=`@'BK TX@8YKTKT[X8Y91/290KSXY"@Hj{  &&)) 55::0FFIIFH@VVYYPffiigh`ut{{uz>]]3 3#=^^\`TV5` @IU U U U   B     K TKT[KT[KT[K T[X@8YK TK T[KT[X8Y91/<2290KSXY"@" 5 IIF @ [[U P nnf yy          %%#'!%""%' $ ! # 9669 0FHF@B@@@D D D @@VVVPQRRPS T U cdejejjjn a g ouuy}x}zzxy  { v } @/   y]]333# #V`jjj;y` C@F      B   K TKT[KT[KT[X@8YKTX8Y91/<290KSXY"@   & =1 UWX f vzvt        )&% * :9746 9 0 IFE J @ YVYYWVYVV Y P o x  /]] # # 3 dkr))`HJq=V`@C        B     K TKT[X @8YKTX 8Y9129990KSX2Y"@     # 5 I O N Z Z j        '$$  )( % $ $ ' ** 755008 6 6 8 990A@@@@@@@@B E G II@TQQUPPVUVW W U U YYPffh ii`{xx   e]]+5326?3 3N|lLT3!;^^hzHTNlX` @B K TK T[X8YKTX@8Y2991/0KSXY"@B&GI  + 690 @@E@@CWY_ ``f``b ]]!!!5!qjL}e`ۓ%$w@4 %   !  % $  C %K TX@8Y<<29999999199999990&]#"&=4&+5326=46;#"3>l==k>DV[noZVtsݓXX10#$@6%   #%#C %K TX8YKTX@8Y<2<9999999199999990&]326=467.=4&+532;#"+FUZooZUF?l>>l?VWstݔ1#@  1990#"'&'&'&#"5>32326ian ^Xbian ^V1OD;>MSOE<>L5 b@ <2991/0K TX @ 878YKTKT[KT[X  @878Y P ]#53#3+e#!Q@+     "  "<<<221<9990%.'>7#&73JDFHAMf fIX⸹)**'# 32!b`@!    <<1/2<2990K TX@878Y66].#"!!!!53#535632NL=ty-=))׏/я^R#/@I -'! - -'!0 *$0* $ $(st*(s099999999919999999907'#"&''7.5467'7>324&#"326{r%$&(r;t=:x=q%%&&s7t@?s9q(&%%s>v:@t8s'%$|pprR@F  B     fe f e<2299991/2<2<290KSXY"K TX@878Y@(' ' ')((79  ]]!#!5!5'!5!3 3!!!c`Tþ{yT9{3{JD{3@ <210##  \= >@54&.#"#"&'532654/.5467.54632{?>?>S8alӃ\]>9̭IXW:fqր][;;ȦI.Z.L-[.K''PGZsweZ54m@''TLf{xf[1,pEF)@dd1<20K TK T[X@878YK TK T[KT[KT[X@878YKTKT[X@878Y@````pppp]3#%3#^y/IC@&=>:A$104G$ 7aD=0^* D^ J21/02#"$'&5476$"3267>54&'..#"3267#"&54632mmllmmmmllmm^^``^^⃄^]]^\^BB@zBCFInmmmmnnmmmmng^^^傁^^__^]⃅]^^! "s;)_@3(%%  * "(kl"k *22999199990!!#5#"&546;54&#"5>32"326=P,]uu>DIE~bRhP{@p?Dq[[""CO@Mr%# @I    B   o o n<2991<2990KSXY" 5 5%-+#-+#RR^@ 10!#!^d10!!d/8L`@6EBC?2H09JC 9 $HE301B54&'.'2#"$'&5476$#32654&'2#'.+#^^``^^⃄^]]^\^ㄘmmllmmmmllmm}{{nWXfi`C.;I6Bf^^^傁^^__^]⃅]^^gnmmmmnnmmmmnb>KL?gwyVpMI`3Db+/10K TKT[X@878Y!!Vu=  @  Z[Z10"32654&'2#"&546PnnPPnoO@v+..ooPOmmOOp1.-rB .@     <2<21/<<0!!#!5!!!-Ө-}}^J@$}}B ~9190KSX2Y"!!56754&#"5>32 "?XhU4zHM98rn81^BQ##{l0b(H@'    #)~&~ )999190#"&'532654&+532654&#"5>32 \e9}F4wCmxolV^^ad_(fQI7Z`mR|yOFJLl?<:=svcE`sRf1@ D10K TKT[X@878Y3#fV` M@%  !   NF!2912<990"`""]3326533267#"&'#"&'#% )I#ER2bf*V H<9 NPOONN;9 %@]] 91290!###.54$yfNݸHF103#F#u@  ' 1/90!#"&'532654&'T76xv.W+"J/;<+->i0Y[ 0.W= ,@   |]|| 12035733! c)t'+n`d.@  klk 9910!!2#"&546"32654&PXγгi~hi}|P{ݿܾsH# @I  B   o op<<991<2990KSXY"5 %5 +-+-#^R^  ^R^  &{' d 5?&{'td 5b&u' d 5 $@/  !# #%" " "!& %999919990KTKT[KT[X%%%@878Y@ ttttv]33267#"&546?>7>5#537ZZ:3mN`^gIYX0&DeWX5^1YnFC98ŸLVV/5<6hk&$uuhk&$suhm&$vu  +@ ]1h^&$tu #+@ @O# /#]1hN&$ru  +@ 0?  ]1hm !@T   !!  ! !!!B     !  VV!"2299999991/<9990KSXY" #]@  s P#f iu {yyv v!# ]]4&#"326!.54632#!#TY?@WX??Y!X=>sr?<҈_Z?YWA?XXN)sIsrFv)H@9  B     <291/<0KSXY"]@gww  ]!!!!!!#!59=qժF՞su'&&z-k&(uuk&(sum&(vu@@ ]1N&(ru @@ @]1;k&,u/uk&,s/u`m&,v/u +1XN&,r/u +1  g@    2  y<291/220@(   ]]! )#53!!3 !iP`P5~.,3^&1tu"+@ 0?""]1sk&2u'usk&2s'usm&2v'u+@]1s^&2t'u!0 +@ 0!?0 !/0!0]1sN&2r'u +@ @O]1? @M    B   <291<290KSXY"  ' 7 7w55v8vL57y5yy5f +@< +,  )&  *&& &,+,* # )#3,99999999199999990@*WZWU!je!{vu! FYVjddj(|svz( ]] 324&'.#"&5!27!"&''3>_'y=_''NOy;WfNPƀ[gX@CHp@CpDfbMKYg[KKX)k&8uu)k&8su)m&8vu +@ / ]1)N&8ru +@P_@O /]1k&<ssu =@   ? 2291/0@ ?_]332+#32654&#'ђ/@0-'!  **.  !' $'$-F099991/990@@'(     ! "&  : :!MM I!I"jj  ]]4632#"&'532654&/.5467.#"#:A9`@IPAtx;e\`Wqqs`/Q*%jd_[?T>7;[gp{-f&DCR @?&/&&]1{-f&DvR @?&/&&]1{-f&DR (,+1{-7&DR.< +@ ./<.<]1{-&DjR -( +@(o(P-_(@-O(0-?(-( ]1{-&DR%@&,,& 2882 ++1@ ?5?/5/]0{o{3>@C'-%= 4%:.-*1 %?47&%7& =&-7"E?<9999912<<29990@0+0,0-0.0/00@+@,@-@.@/@0P+P,P-P.P/P0+0@@@@@@@@@??? ??0,0-0.0/@,@-@.@/P,P-P.P/ooo oo`,`-`.`/p,p-p.p/,-./]q].#">32!3267#"&'#"&5463!54&#"5>32"326=DJԄ ̷hddjMI؏`TeZ߬o0Z^Z55*,ywxx..''`f{bsٴ)qu{&Fzqf&HCqf&Hvqf&H"+1q&Hj@@ ]1f'Cof'v\f& +1F&j +1qu('@^%{&%#${##{#({'(#&'('%$%(('"#" ! B('&%"! ## #)&' ! (%#" QE)999999919990KSXY"?*]@v%+("/#/$)%-&-'*(6%F%X X!` `!f"u u!u"%#%$&&&''(6$6%F$E%Z Z!b b!z{     {zzv v!x"**']].#"32654&#"432''%'3%F2X)6 ~r4*!M!ü޼z&77kc\̑oabd7&Qquf&RCsquf&Rvsquf&Rs+1qu7&Rs .+@ /. .]1qu&Rjs +@ @O0?]1o )@ r <<103#3#!!oAH +@<+,&  )&  *&& &,+,* # #Q)E,22999999199999990@p(?-YVUV jf!{    { z{ {!"#$%{&%--&YVUZ(ifej(ztvz($$]] 32654&'.#".5327#"&'')gA\*g>}66]C_56`?`!*(Ou))Hn.Mw834OMx43NXf&XC{Xf&Xv{Xf&X{ +1X&Xj{ +@ @O0?]1=Vf&\v^V>@ GF2210@ `]%#3>32#"&4&#"326s:{{8daa=V&\j^+@ 0? /]1h1'q;$ +@@O]1{-&qJD+@o]1h'J$+1@oo]0{-&OD"+1u&${u{&Ds'k&&s-uqf&Fvs'm'vLu& <=/1qf&Fs'P&&zLuq&Fs'm&&w-u@]1qf&F&'wq&Gq @_?]1 q$J@$ "    GE%<<1/<20`&&&]!5!533##5#"3232654&#"F:||ǧN}}daDDa3&(q=q'qH@p]1m'yu(@@]1qH'H@p]1P&(zuq&Hu&(qu{&Hxg&(wo@@ ]1qa&H!+@!]1sm'v\u* <=/1qVZf&hJ  <=/1sm&*yuqVZH&JsP'z\u*@?]0qVZ&hJs'^*qVZ4' J;m'vu+ +@ / ]1dm'vuK*+1KQX88Y@ @@]:@    8 22221/<2222203!533##!##53!5qʨ"ʨ9Qx>@!   N  2221/<2290#4&#"##5353!!>32||}}`Bu\zzedx^'t.u, +1g7'+1Y1'q.;,+1H'q+1gm'y.u,+1VH'+1u%'d,u 'JLP&,z/u<<1??]0y`,@ F91/0@4D@P`p]3#\`{f'-\,@1V'M8L@F1f_m'v.u-+1V\f'+1j' .' N` @(B F 291/<290KSXY" ]@_ ')+Vfgsw    ('(++@ h` ]q]33 ##%kǹ`!jl'snv/Jl'sZvO<1KQX@8Y@O]0j' /' O@@]1j'q/'q9O @]1j'y1w/'ysOK QKSKQZ[X@8Y1u ?@   : y<<991/900P]3%!!'79Pw^Mo;jnH ^@  z z <<991/90KTX @ 878Y@ @ P ` sz p ]37#'7Ǹ}Lɸ{JZjXj3l'sv1@O]1dm&vBQ @?O]13' 1d{' Q3_&1wg +@ /  ]1df&Q +@]1'QU~V;@  AKTX8Y21@ /0!"#367632+53265PͳNijQRW1fOCCoa`ZVd{;@  NF 21/90`!!]+5327654&#"#367632dRQi&&||BYZuccH``01`e22wxs1'q';2 +@]1qu&qsR+1sm'y'u2+@]1quH&sR#+1sk'{'u2quf'Rs ;@   299991/220!!!!! !# !39OAg@AժF|pm|q{'3@1 . ("%4"1 K1 Q+E499912<2290@%?5_5p55555????? ooooo ]q].#"!3267#"&'#"32>32%"32654& H ̷jbdjQGьBN5Z44*,nmnm98olkp݇Tl'sv5m&vBUT' 5J{' UT_&5w}g@_]0Zf&U +@]1l'sv6om&vBVm'vu6  ))Ic:1of&%V  ))Ic:1u&6zou{&Vzm&6wu + ""Ic:1of&V + ""Ic:1u&zP77u&zW_&7wsg +1@_]07&Wq7p@]1F@   @ @ <<1/2<20@@p ]!!!!#!5!!  ժA@7C@  F<<2<<2991/<<<20]!!3#;#"'&=#535#53w{%&sQQ''PO>)^'tu8 '+@ ]1X7'X&+1)1'q;8 +@ / ]1X'qX+1)m'yu8+@]1XH'X+1)o&8iX&X| @@@!]1)k'{u8^f'Xe)&8u`&X'Dt'v|:+1V5m'EZ+1t'vr|< +1=Vm&^\+1N&<rsu +1\l'sv=Xm&vB]\N'zs=X&]\m&=wuXf&] +@ ]1/#@  L<1/0!##53546;#"c'&яN()g ,D@% ")%,$'".EG* ,(%#'F-<2221/<204'&#"327667632#"'&'##5353!!STTSSTTS:YX{{XY:E/tssttsstRd0110d}}P)C@#   . *29991/90"]!2654&#!2654&#%!2#!"#546D+ |v݇f>orqp ˘0_i1F&8@# (EGF'221/067632#"'&'#!%4'&#"3276s:YX{{XY:NkrSTTSSTTSd0110dtssttsst 3@  . /21@  / 9/04'&#!!276!2#!#ONDNO|N8DCDCD>@  G /221@  /ij9/0>32#"&'##34&#"326s:{{:"QrdaadDs'0@  0 <10>3 !"&'53 !"shSzjffbGGaaHH_^9'(9^_sZd$D@"! %  %  0%210&&].# !267# !2676;#"'ffjzS` SfM?nb_^^_HHgh$bzq"N@$ ## HE#210@ $$$$$].#"3267#"!2546;#"NPƳPNM]-GFE0iL~++++$$>: a .@   2 99991/0`]3 !%! )"#5465BhPav/w.,~0_i1F.@  .21@   /0)!"!!"$54$3!!@DNN|#+qZ?@G E221/0` ]5!#5#"3232654&#" M:||:ndaDDadqVuc'T@ )E Q E(]99@   (99@%S 910%!"'53254%&'&326&#">kGxfu'~@3cnBOFFu\0%p9 *E +@    21@ /0!5!!5!!5E>9+uD@& 39190!!"56$3 ! 7327upo^   2`_FHg[{(@@$ )) #)* &)190.54$32.#";#"3267# $546؃ YsrSǾmTj^У%!| &${spw21%%ݐf#A@  2991990 ]!!!!+53265ZpPM?nժHVe@#   LK TX@8YKTX8Y<<9912299990@P]#"!!+53265#535463cM/ѮcMPhc뻫Ph*Nsd&I@43! F'1@'$$'990%!5!# !246;#".# !26uu^[DM?npkSUmnꪖ_`%Rv%@ 'P $&]ĵ 91@ %$&222990@ #%$$<<$#$%#@$"! #9927654'&'3#"'&5476736,3,,3,6hC.KddK.Ch B9Iy\\yI9B z^ȮwBAWWABw1G*O@, *&NF+291@ '&&  #/<<9990%27654'&'5+"&54&#"#3>323LTWJ>ymoF||BuLibep_!edg .@  KTX@8Y991/9903;#"&n?M-– R E@   >f3@)B 6  999991/299990KSXY" ]@068HGif FIWXeiy]]!3!+53265jG?n+Vd{Ks 1@ 3221@   0! ! "!&32sy:;x Vb[[z=g&24v'X Rs3@ !  <1/0!4&#! !2!2"327&nzy;pa'Xܯ–bb-LgFqVY{!:@ """# E"9104'&##"3232"327&&&idRصRQ@TVt1098``:6:@   ? 291/0@ ?_]32654&#%!2+#"#5468ʄv/ϒ0_i1FV$O@$#% %G  F%22991990@ `&&&&]%#46;#">32#"&4&#"326siL:{{8(adaaTV@  ?  2299991@  /9990@ @u|]#.+#33 326&#A{>ٿJx~hb؍Oђ r!d@ -" "99991@B!  "90KSX@ Y6 327# '&546?6764'& {璑z<;YZL-|숋_ppٶ+23@@md{'@  !! RE(99991@ '$$(90@S !S BKSX99Y"]@/)?)_))))))]@% '$&((*//*( ( ))$]@.,,,;;;; q>323267#"&546?>54&#"Lf@eaflZ?bZN?$%PKQY55##*!I@TT((7V6@   O 221@   <20;#"&5# 54!23%&'&#"3wMc/R5!n|wj=hP`@o,0A37V?@ F<<291@/<2990!!;+53276="&5#53w{KsF0j&&էN01`>X@ @  991/2990K TX@878Y@@p ]!!##"#546;^vժ+Zi1F7I@  F<<2291@  /<299990]!!;#"&5#53546;#"w{KsբcMcN`NQfT@ @@ 120K TX@878Y@@p ]!!;#"&!n?Nժ=–_&84i' XN:@!3   1@   <2220!! 47!5!3254'5!X ƱXw>*a"Lav-@   /<91@ 0%254'&'5!'&'&33cAnMagn"ʦmWDtz–d@  @ @99/1@  /9990@        BKSXY""#3 632#54&9%NZUUIG9\[ny6P=V{j@  K TKT[X @8YKTX 8Y9991@:        B    9990KSX2Y"@      '$$  )( % $ $ ' 755008 6 6 8 A@@@@@@@@B E G TQQUPPVUVW W U U ffh { F]@%     # 5 I O N Z Z j ]+5326?3 67632#54&#"N|lLT3!;^0XQ99) hzHTN43`rr:T*\@5    B  B K TK T[X 8Y9991/<20KSX<<<323#L:s_%'ST_ijxzX"Jh0@umHLIwKK!!C12\RI`1]5@ F1@  0 4&#!!!%$ $5& )sQ;-%,%hV)$yhL?`3@  F1@ 203 4&#!!!32!"'hi;-ԧc%,&cV)$yJX$!"'&'5327674'&+#5333!plnUQQLITNPc9:V>}ws}#(rAbLrV{@@  F221@ B 0KSXY#36763254'&#"s4QҸMNr98xܭz BR1pqWBAV&@ F10@ @P`p]3#V''V:@    <<2<<219/<2<203!!!!#!5!5!5!s____,Ԫ m'?' f'@'qf'@Gf$'-/V'Me/V'MvOf'-_1V'M>1V'MeQhm&$wu<1{-f&DZ +'+1`m&,w/u  Ic:1^f&  Ic:1sm&2w'uquf&Rv <1)m&8wu<1Xf&Xv  Ic:1)3&08X1'q{;)Z&86X"&X)Z&80X"&X)`&80X"&Xq{h3&${-1&qR;h3&${-&DH4'q>{o'qs%T@!$"43 &<1@"#%&99ܰ KTX"@8Y<203## !2.# !2675#535!5yyuu^opkC XSUmnHF_`%'XqV{ 4X@"2% G,E5221@ #% ) 2/3 &)/99<20`666]4&#"3263#!"&'532767!5!6=#"3253:aQQRZ9||9=nXF]@,*_EG^[cb::bcsm&*wJu!<@!T!$!]1qVZc&JJjm'wu.m&Nwu* +1KQX88Y@ @@]se'42qeu{'Rse1'q';qeu&qsm'wuyXL/f&TVdf'%  Ic:1 '=' ']'q']Gsl'sv*qVZc&Jv-5@8221@ /203!327653! '&5!#>=B>d`gd"dPNOKZ߀xxv 9V@@  221@ B 0KSXY%#3676324'&#"8WST=<HW5xz7 GF3k'uu1dd&QChs&s\}{s&s}Hl's\v{oc&vefl'svHc&vhp&$|z{-d'Dh6&$x>{-H'eDp&(|zqc'H6&(x>qH'Hsp&,|Yzc'fw6&,x>>UH'$sp&2|Azqud'Rs6&2x>quH'RTp&5|yzJc'%UT6&5x>^H'-U)p&8|zXd'X)6&8x>XH'X'v6o{',V'S77'WRs. 56$>54&#"57>54.#"5632?4o1\}p_s54&#"57>54.#"5$32Fp>!BlJc(v];?"AW?-1CA#E ptgDZX%KlaF='.`[b[3XpVU 2#PQ̝qpD(4%3254'"632!"'#67&5#"'&76323 76'& %44nI5"C0:XY|ˀ|YX:ST$TTTTT- H:E<$d0110d^jtssttssq% ;W@$3=E (B!8;7B/E<̲ ;]91@$3< ;<,<990" 7654&327654'&'52 '&54767&'&5476!˸jkkjpkk_;̨_`Lm䖋_``aCUtMMMMMN'|OEH-AA+Mdha "ccttttُcc"FYXSJqq 4C@6E B42()+&BE5221@4)".559920" 7654'& '&5467&'&5473327654'qSRRS SSSR:4HRQ;4?+IHIJ,MMMMMNMMJ@b@Y "ccttttُ"#VKYIAAAAAtw>\V@ B  K TK T[X 8Y991@ B  /0KSX@ Y@@ )&8HGH  /59?GJOUYfiowx]]+53276=!5!5!!Hri&&gPP%01oXV`@   K TK T[X 8YKTX @8YĴ@`]99Դ@`]1@ B  /0KSX@ Y@2&GI + 690EIWY_fh]]+53276=!5!5!!۞Hri&&5ejLP%01%hP&$@{-&D_u&(zqu{&Hz{s3&2bqu1&qs;s3&2iqu&RsO'z't2qu&sRs3&2jqu1&qs;1'qr;<=V&q^\p\%3254'"632!"'#67&73%44nI5"C1- H:EVy` 8@   OF 991990@  @ P ` p ]3+53265F1iL`aq #/A@1E%G +G!E0<<<<1@( . /22220 6& 23632#"'#5#"'&76'&  7/ST$Trrrrˀ]STTSST$Tjtss ^ŨŢtsstjtssqV{ %/D@1E$G+G'E0<<<<1@ *.! 02<220'&  7"'##"'&763253632 6& STTSST$TrrˀrrST$TdtsstjtssRŢŪjtss|3 #!#'#7'7 3!Jafp|҈2F;R/o]jY'FF8O ",'&76!27&'!2767# '#&# rfuSv=:efc.1 tsfjwv9tFXh$xYv+!f //_H$$\/ح ]"+'7&576!27&'32767#"'&#"i`UUQ.-Y_vcPNONMRS]7GGcc^N lOU ^q+$Vqrg j ;@   : <<1/<20@ 0P]33#!!#53ʿ_w1##'!5!7 !4" gZ8f,i> XRBY bo{=4'&/&'&54632.#"3#"'&/&'&'&'53276 23@LLfLNZDE11?PS{W*L'TrGY$alfccaFF'K((%$JK((**T@%$!,KL[@~$=&[#5-,X3`!;#"'&/&+=!qjN\1*LlTrGY=Z^e`1~$=&[? %P6@ 9991@  /0##32654&+"56;2'񍚚EOZ*,FP{7@   991@  /032654'&#"5632##/dLUIVVN}AH+Fnt  (\@ #  . &%)<229991@(% #/99/<20*]!!!2654&#!2654&#%!2#!#53[D+ |迿ɐʇf>orqp ˘p _@ 8AKTX8Y<2<21@   29/<<2299990]3!33#! 5#53!3265˥ߦ®j*$}h0B33#!!!!#7#!#!AX .AA<VF㪾FqB&-1&'&'!3267#"'#&'&3273&#"#So+Jajbck{cPm!)81G\9/Zo Z 6Z44*,!  C "2JcfRY@    9 KTX@8Y<2991<2990@ 0@P`]#+53265#5333RM?nʿwHVS@$   OF<<22991<2990@ @P`p]33#+53265#533#F1iL`(aؤsf$C@$  %" %  %2299199053;#"&5# !232#"nEMMT–\\xEEqV@{$H@"%"%G E%229910`&&&]#"&=#"3253;32654&#"@F:||:Li1戮VּdaDDada= T @  ?  !<299991@!  B  /<229990KSX9Y"@"]@Bz%%%&'&&& "66FFhuuw]]#.+##53! 32654&#A{>ٿJxʿ~hbw؍OJ{=@ F<<<1@  /<20P]###533>32.#":.I,h<ĤfcΡ3!733!#!53!ٗ ٗwјv9 V`+5326?!533!33!+N|lLT3!øLùmhzHT33`{ ,@ .% F-22991@-&%"*-%  9990@1?$?%?&?'O$O%O&O'_$_%_&_'o$o%o&o'$%&'$%&']@+?#?$?%?&?'?(?)O#O$O%O&O'O(O)_#_$_%_&_'_(_)]2654'&#"367632#!3267#"&߬A@o\]?^^fe~ST`Te__Z+f{b:9ml)Lf01a```FE..'qZ{8@G E221/0`]53#5#"3232654&#":||ǧdaDDa{ 8@  G F221/0`]4&#"326>32#"&'#3姒:||:/Rdaad` $C@  !G! F%22991/0`&&&]4&#"326>32#"&'#46;#"姒:{{:Z[/Rdaad~Ӝ}}{ 0@ ! !"EH!<106763 #"'&'5327654'&#"LQQU]SRMNONPccccPNON5#$+qrrq+qs{'/O@( ,,H"E02991@.*%00@ 11111].#"67632#"'#47&'&!23254#"NPc'>IjJ?_SPI 9/-U:Me5++rQ,3H=Y}/)9DhQ#3 :#:9KqV@$K@$%"%OG E%221990`]#"&=#"323;32654&#"@F:||:Li1戮VּdaDDad^ؙa=q$=@" %%  GE%2210`]546;#"#5#"3232654&#"iL:||ǧadaDDaq{"r@ KE#91@  #90@)?$p$$$$?????,//,ooooo ]q]47632!"&'532767!7&'&#"qkcbdcjfg ]\RS^,*4cdWWZZq{A@$  KE91905!.#"5>3 #"73267qN ͷjbck 9Z44*,#ė|{ 4w@6.('4 KE5<Ķ&  91@/.'""5 5@  &"90@ 4 &'<<<<<%6'6'32#"'&'&'&5>3 73;#"'&5Nf  R`\Lladbck $˸&&i+@WR֊>8E#Z`vg'#d4*,#)u10`Z|I|*|>i@@603273;#"'&5|PUTZGUU]UTNHtCDFEwGQPabLq_&&i+@WR@\l%88ZX83,-F@.. NBj10`ZȦFq|/;@ 1 &,E01@00)0#90"327654'&+5327654'&'2# 76`cchҗUTNHtCDFEhqr<V`K@   OF<<22991<2990@ @P`p]33#+53265#53F1iL`(aؤqV 0U@)  &#-* *-+& G E122991/990`222]4&#"326!"&'5326=#"32546;#"aQQR9||9iL=,*[cb::bcaqVZ` #C@ # GE$21/990`%%%]!"326!"&'5326=#"43!aQQR9|=ͻ,*[cb:*qO{8@4 E1990%#5!#"!2.#"326Ae{-h]_cƳO|$$>:77>>`Rd`#y@ %  $ĵ 91@  $222  990<<<<< 3#"&54767327654'&'bB_j&;;&j_BC(::(xܱSccS$-EIdccdIE-`d`#y@ %  $ĵ 91@  $222  990<<<<< 3#"&54767327654'&'b)rG,EE,Gr)C'88'bLx>>xLb-!@2FF2@!-VX`9@     NF21290`]332653##"&||Cua{VfcdC@!   N  F2991/<9990`]#4&#"#46;#">32d||iMBu\~aedVd!J@%  " NF"2991/9990`#]+53265#"#46;#"632diLiMHa=~a >@    F<<<2221/<20@ @P`p]33###533#¸`<Ĥn`Mt` '@   221@   /2205!#3!53t褤K#<@ % V V$<<1@#! !//2<903327673#"'#&'&#"#67632= &}33[ &}33[ %$RIJ %$RIJLT5@  <2<1@ /9/<2033##4'# 7632&#"3=5*7M\TK9V_ (@  F 1@   990;#"&5y=x1F|t(L6$@#&#" F%<̲#91@B""  " /9/ 990@$#@  **8;ILT[q ]@$$%$$5$7E$FT$\ ]@    ]2!"'&'5327654'&+5!#3!CicUQ^cdjTmcd\[je8+lh%12KKKJ3Lb&^@PP F'<91@  #''<<<290@0(P(p((((((( ]%#"&5332765332653#5#"'&Cb`ruSSrw=ZXyzVUy=<b`^zbze32>>Vb&a@PP F'<91@  #''<<<290@0(P(p((((((( ]%#"&5332765332653##"'&Cb`ruSSrw=ZXyzVUy=<b`^zbzZe32>>V{0c@PP)%'F1291@ %*!*-(&/<<290@02P2p2222222 ]>32+5327654&#"#4'&#"#3>32)E__RQi&&ru99wSS?yzUV|v{zH``01NM_``gb>>Vk{Q@N O F2991@ /9@   990`]#4&#"+532653>32k||F1iLBu\satedVJ{;@ N  F21@   /  90&54&#"#3>32;#"R||Bu&&i1F``edH10d` y@BNF 991/<2990KSXY" ]@068HGif FIWXeiy ]]!3!##`ylqu{ ,@  Q E2210"!.265!2#"qt蔔98q$`I@  E2ij 991@   /<<@ 9/0!!!!! '&76!#";:E*%xxxx%`ݛlklm>|$2@ &E E%1@ #%<202765 26= "&'"&H`k&InI&k`B"F:.aע ģ0[1[0T\l6puypVi`/@   /2991@  /90%!"/32653#r%832JI,:.˾ fcVJ{:@  F2190P].#";#"&53>32JI,Li:.˾atfc~{%@ 21@  /29903!5346;#"iLAat~{%@ 1@  /29903!534&+532ʴLiAa`@4  B      F299991/<9990KSX9Y"@]@Bz%%%&'&&& 66FFhuuw]]#.+#!232654&#0s2âJ{Qpwu t]:'`iVNM``E@  F299991@  /29990332673#!32654&#Q{Jî2s0jp|Ɓuw`':]t i`MNVoV{0@C  S('  S'('B1 '(!.1' ($R$+E19999190KSX99Y"0].#"#"/;#"&=32654&/.54632NZb?ĥdXLie@f?((TT@I!* ajYQKP%$V4@ O F<22991@  99046;#"+5326cMF1iK»Ph)aV O@ !O F!<<229921@! ! !99<20546;#"3#+53265#53#5cMF1iK`NPh(aؤi7V5e"O 1@ 04&+532;#"&McKi1F(hPaV2@   O 221@  /<20!3## 54!346;#"#"3276w5RcMów|n!o@`Ph3A07^3@   /<<2991@  /<2990]!5!4&+5323#{Ksբ>`N7V=@   F<<2991<2990]!!;#"&5#53w{Liൣa>`C@     NF2221/222220` ]3!33##5#"&=#5!326:CuȮ||h=$#^lfk`8@   91/20@ 3 3#f%.]`8XV`@"B  OK TK T[X8YKTX@8Y2991/0KSXY"@B&GI + 690@@E@@CWY_``f``b]]!!;#"&=!5!qjLLi/F7e`ۧa%X`!@  "KTK T[X8YKTX@8Y299<21@  /<0@ BKSXY"@:&GI #+ #690#@@ECWY_#``fb###]]!367632+#47!5!3254qjL"TA`:&>R~ie8FX`ۢG7W9W`/=3<;4%6]XL/` @ "!̲91@B!  !9/ 990@ @  **8;ILT[q ]@  %$ 5 7E FT \ ]@    ]2!"'&'5327654'&+5!5!`q|/=@1 %,%E01@0 0"0( 90";#"327654'&% !"$5467&'&5476EwEFDCtHNTUhcc`a|p<:!a>>`V.9@ F<<991@   /<203#33## 54!3#"32767Ku_+xG`͋BA0 L` ## 33R9L T#`@ F1/03!!`3qV $C@  #%% "GE%2210@ `&&&&]32654&#"#"32546;#"#/s:||:iM/daDDadaX$L@ & %<<ij#1@  $! /<2KPXY032765&'&#"56763 3###53T?V:9cPONNLQQUmlprLbAr+#}swԤX$M@ &"#E%<<ij "#1@ $!# ##/<2KPXY0535&'&5476!2&'&#";3##plnUQQLNONPc9:V>ws}#+rAbLrq &) 76'& %3!!!+5#"'&7632/ST$TTTTT iL:XY|ˀ|YXjtssttssH^Lۓd0110MqL4@#5#"'&76323!2!"'&'5327654'&+5 76'& Z:XY|ˀ|YX:jejbVQ^cdjTmcd\]:ST$TTTTT3d0110d^L$8*mh%12KKKJjtssttssq 3: 76'& %%!332!##47!#5#"'&763233254#/ST$TTTTTghL<):XY|ˀ|YX:FXjtssttss_ 3<;4d0110d^6[7@F.#"#"'&'#"'&5#533!!;5327654'&/&'&54632NZED11?QR|{Za]gQQ{%&sfccaFF3,@LLf?((**T@%$!,KL[[!&PO`>''M5-,QK($)$JK7V&/!05476;#"+53276=#"'&5#53!3wxWQîc&'QRF1i&&QQ3%&sN[V((h)``01PO`>''7p-9D!6!2&'&#"63 #"'47!"'&5#533276'&#"&57!3w{UQQLNONPcccO+eKTIQQ;BS_r(ր%&sz#+qrfr v)2LOAPO`> 'KV ''/Vo5+5327654&#"#!##535476;#"!;67632oRQi&&||ӹWWc'&-BYZuccH``01/яNUV((hce22wx#5.#"#"'&'#34632327654'&/&'&NZDE11?PS{|Zb]hf8b_caFF2-@LL?((**T@%$!,KL[[!&2-,QK($)$JK @   F<2991@ B /0KSX@  Y@B &GI   + 09 @@@@@C EWY `````b f]]3!!!+iLLۓ6 333# #333# #6ttttU=63@    <2<21@  220!#!#!#!#6kkUXrXJ3@ NF 21@ 0%#"&54&+53232653#׃Li1FęaBþyVv!:@ #NF "21@" ""0%#"&54&+53232653;#"&'׃Li1FPh2FęaBþyfu0@ 32tNN^luu)qJy}wYYk\g88u:KSX@ 32tNN^lugrB0)qJy}wYYk\xkW6Vr88 #@<<1@03+5327653#zt43r,Bttx66XVru@ 1@ /0.#"#3>32.biuu$uT  qksa97H <1 /032653#5#"&'H.bitt$uT  qkJa97Hu' <1@  /<032653;#"&=#"&'H.bit0B,rg$uT  qkJ V6Xlx a97 !+33276?3327654'&+CFCDtk=%%(f{n!!"}K'))'K}N;[--s?5/.6 333# #6tt&+53276?331/.N]D0 {{bp"#WK/itftf&t  @ 10#5Rڬ@u1 ܴ? O ]ܶ ]<1ܲ]90526544u@XX@sPOOP{X@?X{POPPu1 @    ]<1 Բ]90"'&4763"3sPOOPs@XX@PPOP{X?@Xu+@ 91@   032765&'&#"567632#'y7$#?q22110335WDDFk[@*7K$@ ` XFh_@Cu-@ 91@   0#&'&547632&'&#"3kGDEW53301212q>$%6y[AmC@_hFX ` @$K7*@ 2% % g 25-5g'|?f=u912]90K TKT[X@878Y3# #fg|?fLu91<Բ]90K TKT[X@878Y@ 5:5:3]]33|g?f7@ u91290K TKT[X@878Y3#'#f?f7@ u91<90K TKT[X@878Y373x^@1@/0#^+b+qsRf3#ff #ofv^@1@/0%#^++Tq^#onvsR3#lo#E@ j,5!##–,dU 533##5#5Dud&u!5!&>ߖ)9H W@ VV1<0K TX@878YKTKT[KT[X@878Y332673#"&v aWV` v HKKJLDfN@ d10K TK T[X@878Y KTKT[X@878Y3#  @ V xV104&#"3267#"&54632X@AWWA@Xzssss?XW@AWX@sssLu @   '1/90!33267#"&546w-+76 >&Dzs5=X.. W]0iJ7c@$   VwVv99991<<99990K TK T[X@878Y'.#"#>3232673#"&9! &$}f[&@%9! &$}f[&@Z7IR!7IRfB@991<20K TKT[X@878Y3#3#߉fx%3;#"'&5&&i+@WRd10`ZȢf '#7'373\\]]\aa``u # 5473733254/MMz /1/03#zttu/2&'&#"#"'&'532654'&/&'&547632j1549W++](}24NM9>=D@?>=RX o(l00GF@99 a /$*+MW33 k2-*)*IX01 u! #'#37 ͉H+uX@ 1/0!!5!AGЈX'@??//21/]0!!5!3A4X@ 21/0!!5!3AhhX'@pp0021/]0!!5!3A4X@ 1/0%3!5?p+v'qqm 93vJ!_@ Vw V v"99991@   "<<99990K TX@878Y'&'&#"#67632327673#"&9 &}33[&@%9 &}33[&@7 %$RIJ!7 %$RIJf6@ D910K TKT[X@878Y # mXfvqPf6@ D910K TKT[X@878Y3#fs?f<@u991290K TKT[X@878Y3#'#?fsH7b/q|  )1H+d%@ 910@4D]3#hF)I@ dd 91<20@#4D`````````ppppp]3#%3#^y)7{"@ V@ V /1@@ /0632#546?654&#"7pihX,#w3-.>GZdH3UC=A   (6%""($4fCf<@u991<90K TKT[X@878Y373NxsD/1/0#DD'4]fB@991<20K TKT[X@878Y#!#͇fxx)1')1H VV/1 /<0#.#"#> v aWV` v ")KKJLD( @0#3Ӥ?#55#53pp{53#7"op{y3#@uUCqPUv$<#5353#ĠxxxF33##xx2xU?p!5!#Ik{1@V/K TK T[KT[X@8Y21@ /0532654&'3#"&=X.. W]0iw-+76 >&Dzs5V @  V21@ /0"&5463"3VZ||Z(55(}ZY|x5'(5 M3!5353D M#5!##걈ň$ #53533##Ġxxxx 5! zV '+53276=0RQi&&``01wV %3;#"'&5w&&iQR10``fSC'SjC( @V xV1@ /04&#"3267#"&54632[6'(55('6y|ZZ||ZZ|&65'(56&Z}}ZY||jT @03#Ӥ#uzLuD/1/0#D`tP#5!#fJc9X#"4533273273" v aWV` v "6KKJL9HS/TB  #"'&'.#"5>32326SKOZq Mg3OINS5dJ t]F ;73 !;?<6 7=xh!5xhh5!Ĥh'`_^NO'ygfFXY @  V21@ /02#52654&#Z||Z(55(B}ZY|x5'(5[3!53[J!!5#>J*>c9X632#&#"#&'"#72;tv gfv ifvtR+ '7'77}`}}`}}`}}`p}`}}`}}`}}` .54675>54'&'C!RI 7!RI 0PQn +0PQn : '  fCqPfvH7FbV+I#5!#!Ֆ֖V,2!5!5!5!>>2xx3#3#@`tt!#!–*>,Jf'73327673#"'&'#7&'&#"#67632Bmk  &}33[& !Bnk  &}33[& g  $%RJI g $%RJI J!%'.#"#4632326=3#"&3#3#9 $(}gV$=09" (}gT";薖Җh! 2-ev 3)dw.CJ"ttc( 7!#'73!'3p~(͛3#557'2d͛~~x&'&4767@*,,*@rNPPNr*,@A++{OPPN1'+!x050567654'&xrNPPNr@*,,*{NPPO{++A@,*.Do2>&"762"'"&46264&" 5O57O5>||=>||66O5555M75m?|}A@}|6M65O5p pk Ppk!!p kpT!!p ଔ* '#'&'&#"#67632327673#"'&O,$e5Fqp[?9ZO,$a9Gqp[?9J7  $0GJI "7  $,KJI pn w(5!'3#7ws~~d͛q` !#!#!#Sb+e !#####b+tf@103AntVH@10%#AnH3y`V #"'&=3; #V!. {q{'yOF{'y#sRf1@ D10K TKT[X@878Y3#fFR&jl@_]@_q0hf'&HFyuf't*f',}f'z.f'4(f'n9f'h=6'.Mh$%j@ 1/03!!)ժh=@ B1/0KSX@Y !3f5:9+(\=;+s!2@"" "#3"10!!"3276'&' ! '&76>b܁܁:xżp[bb,j.h<@ B1/<0KSX@Y3#3#:9&+031b *@    <<1/0!!!!!!29iggqs2;3 F@B   <<1/220KSX@   Y%!!5 5!!>!8ߪp7<s'<@) !%(<<<<1@' %'/<<<<0367654'&'&'&76753#–bbʖbbWssWWssW=;;s.@ <<1/22<20!6'"'&336763#ּՂnʊnhg椌gHN&3@ &("3'1/<2220%!567654'&#"!5!&'&576! cccd?IH1/GGaʦa>”XN'r/u. +1N'rqu9 +1qf&Enf&PIVdf'Kf&MF*&Yqy *@ ,%E+99@ ?/]q@ ) !/99@<<10@  ]@IIIJN LNIK ]@:9:88? <>]@ + +*))]@  ]@++]'&#"3273;#"'&'#"'&763 N,-=MKLyHc( #) Xn^T).^,ru7 nik%1)0T*XoW)&V!7@E F21@  90%#! !"3 5 4# yYo 0kEdZ&J:@ V`@@ 1@ /<20@ 993#&+532i^;,_1FLdVD~qu-T@(/E( Q!E. ]99@%%.99@S910&#"#"'&4767&5!232654'&'&fu5KxD7VUV[a~@Fu\0%p̥@$OF(Iqrs`g |2=@" 33'(#,34 '0E310&'&547632&'&#";#"32767#"'&546p<@ KQX@8Y1@ 20%#457654'&# !5!ʄOTJPE* :;f,KOxsPWKL,#%5,*3Y'iVd{1@  FN  F21/0@]#4&#"#367632d||BYZuccH`e22wxqu$!O@ """#E QE"2]21@?]0@ w##]!3276'&#"2#"'&76EVSI 6VQ@=񈉉d~uvn` @ F1@ /0;#"'&5c"$lYoRR`+.0`b` I@   F 21@ /<20@    <<33 ##Gb`/ZFB?= F@ 1@ /<0@  # #'&+5z~J/k`ue<2~V`wJ`B@1@ /20@ 99!367676'&'31!xdLjE.*{`T|p5dwY|rNįtkR&@@ (" %'1@ '#"'<90%#457654'&# %$47#5! $ڄOTJPE* :MKOxsPWKL,#%5,*,X$Rݿ qu{RJ`/@  1@ /220!#3267#"&5!##J117,#J%x\c`PH? XV{1@ EQ F]1067632#"&'#44&#"326=;{:+fZ#adqR{$6@ !& HE%1@% %0 !2.#"32#457654'&-ULNPƯPTJPE* >:##++LOxsPWKL,#%5,*q` 1@  QE]1@ 0"32654'&'!##"'&76sRVVOcm񈉉qnsȷzn휝dm`#@  1@ /20%;#"'&5!5!!$lYoRR\ W0`b*`+@ E F@?? ?]1@ /<0327676'&'31'"'&5R27ki;jF-*eb`+@EvfwZ{sxvpVh )=@+E(#E*<<1@ *'*<2<20"27654'&'2##"'&7673=A__UVF6˷džfB:VVMpˑRh]p[nmNssg.;Uda@    <<91@  <<90%KSX@   99  9 9Y#&+53;'$ܕ11FA3N11F~0)~pV`6@   <<1@  <2<<0&'&53367653#EkUJ|CUvܷ%aw~LB,BTxnc#n'`8@E  E1@  /<2<0 433233243! &aƏ˪ޏƛa!)R@O@+}&Mj.*&jYquf&}S*f&"Y'f&]YVj 3! # # wHV1M% 'G@)E& F(2Բ?]1@ ("((Զ?]990267656#" '&76#327>&iPDyz]6;~oxҤ]Y:PWp=l޺lǧ_ը,嶖ꀰ-ўqu$ 7@ !EE <1@  04'&#" '&4632  1BSxyJ̃Я#/p~ZZ7Ai6deBWQ I@ "!9Ĵ?@]1@ /<99@ o]0#4''&"562%62#"FR**RMw(oUCHk&_*SKHv H# 0r{C @[)/Bf'nfPWQN'rufpV'A@)   $E(<<<<1@ (  (<<<<02##"'&76327676'&#"DžǷdžǷqMTVMqqLWULc휙owgsugHgusgAm`E@ EE91@ <22205!#%$! 47)323764A,Ma")aM:GϤ*RѧOp[g9&'&47#"54654'&#"563277632327"'532! `7"7$>9[@[`7"7>9[&F]_I I5l|"O z:6hl0'[Ml |"Oz:6hlf$11sXD@!  ܶ0]9ܶ0]1@   <0#&'&76!   76';:{HpҳI椤qVu{ <@!E E ܲ0]9991@   <0"32654'&#&'&7632sVVUVVV9kjstntstu n}{R$.@ & #%1@ %"%0 32#457654'&# '&76)F`{[mzYTJPE* :xe+wTOxsPWKL,#%5,*eNqRQ` 4@ " E!IJ]1@ ! !0")!"32#457654'&g-[oPTJPE* >LOxsPWKL,#%5,*#)@VF'6  (<1@ ( $(0347632&'&#"!!#"'&'53276`1213$)),x:KAb933.1220W@Rd >Qoɏ?s K_7"'&76'&526n 'BQ_'BQ_[~,`*l#FR`*l#FRB@ 91B/0KSX@Y #!3&pM]rV`!#56! #'#64?!"QhRR_@0:IKiXL}/M4!wx#&'#&' #'nd2Fb.-t`4#M!P^sK=W@< 9:?5 +,">99KSX +9> &1>29<90'6767&'&'#"'&46733276=332764''3=D۴vayͤgDd''dey{d;]TCHI}rHGFFtAGCT_8d榈d*0QA^^^Fkmihhimw'AFU(`%S@!'E  E&99KSX"Pe^Ґ8*7D ! ! 12԰.#AL.#^Yq4+& "H4B;;=/?"+VhPOV !! 7654'&#"#676! 3 7llc^#,V)ۄe]6?fضdVj{ # 7654'&#"#67632327\B\\TP%I/yYk}oSKu,2R¤ຐs5%! &'&#"567632 67632'&#" ;!53276n"?E! rK,/ 4'Kr !D<&tEGGH h=" C(FK#C "&E !!6{5%! &'&#"56763267632'&#";!53276[96:@%((%@:6-:IkI:8=3553gs%+$67632! '&76!2767&#"327*W8QU{2Τ|sK^lȺhiieb-sJV"1Pһ '$Astxssq[/&67632#"'&76!27674'&#"3276I,)e[xtgO_\SG]EZSTVXXTRS7xJF61𢢜Pһ ''rsstxsst,V4@  <<1@   <220#5!#!#!3`d`du7U3@  <<1@   <220#5####!3_pzpppg3#"54654'&#"563277632327#"'$47(`7"7$>9[@[`7"7>9[@[|"O z:6hl0%[Ml |"Oz:6hl0%?[MV{$:@&E QF% ]1@%" %04767632#"'&')! $'&  7Z6;x[Y: +STTSST$T%Уb^#10dX4tsstjtssq{FVyMsaq{!&'&#"!!32?# '&76!2%%cjf_[_fMJOhk en(' c\\c( +{!56763 !"/532767!5!&'&#"'(ne khOJMf_[_fjc% ؜c\\c Vs'& @  >  91@ B  /<290KSX@  Yp]@ 6II YY @  &)5:EJ ]]! !###-}-!+V` O@ F  1@ B   /290KSX@   Y!!###`{`UV{'4767632#"'&'!!#5#5'&  7Z=;{XY:eSTTSST$TfZ#10dȪpptsstjtsss'Hs'&y3s''yk&uuN&ruBBBB|#I#IabhFaF`C`#BC`CUXC`C85YBB#Ih;5#I@PX@855Yf4@  <1@/20%+532654&#!#!5!!!2L>o||Rh"9+Fjk&sus'N@  2<1@  IIPX@8Y0! ! &! !!! 'zOFӐhgս6,XNf-T/3@   <1@  /<20!565!32#!% 4&+pٕxL@+8/Xڦ5@ 2<21@   /<2<20!!#3!332#4&+326 z6࡟9d݇,@   <1@    /<202#4&#!#!5!!||Rqf9+Fk&su3k&uu#m'yru; )@   1  /<20)3!3!#++h$.@  . 21@  /04&#!!26!!2)DlN݇@%j@ 1/03!!)ժe4@ <1@  /2220%!!67!3#!#p&axު D+?x4&A((v@   <2991@B   /<<2290KSX@    <<Y@ I:I:I:I:I:I:@  <<<<33 # # # 3DDxM(?@ * %)21@  %&" )02#"$'532654&+532654&#"5>I8z,|йԳƆ\qѲ|!ĐBY+wps{M("3 y@ B  6 991/<2990KSXY" ]@068HGif  FI WX ei y   ]]#!33j+3m&yu# + KT KT[KT[X@ 88Y1 Y@   2991@ B  /<290KSX@    <<Y3! # #_yT:%@   1@  /<035675!#!T>Wxfb/X++0;+s2;@ 1/<0#!#;"++3s'&7#> 1B /20KSX@   Y%+53276?3 3 OM?w.-!suٵ2&]*jklyj =@!   <<<<1@ /<2<203>54&'$%53# W==U+  -=;; )@  <1@ /2<0)3!33#;ʪ+$@  21 /20!!"&533!3_||xdv+ *@    1@ /2<<0%!3!3!3OOʪ+++o2@  <1@   /22<<0)3!3!33#OOʪ++< *@  21/0!!5!!2#4'&#!!276GN6ONDPO+DCDCF&, $@   21/04'&#!!2763!2#!ONDNONDCDCo#N@ <21@   IIPX@8Y0! 7!5!&! 56! ! 'oOzFՎaa0&8@''!&$#(  !%$'2<1/0"3276'&76! ! '&!#3~܂܀s;:ŴL椤kj@@  21@ B  /<0KSX  Y3!!" &$54$)#!:ƒdv'V+w{-{Dp7):@+E'Q! E*21@*$ *9902#"'&5476$%676"32654&}:[;z631-~LӔ{0w)v ,u8w>` /@ " F!21@  /0!2654&#32654&#%!2#!r~~hhVlj9_ZZ^SJJOgyr`F1/03!!`3k`4@  <1@  /2220%!!6765!3#!#}v[(bt:d6(U3Rq{HF`@   <2991@B   /<<2290KSX@    <<Y@ I:I:I:I:I:I:@  <<<<33 ##'# 3?nn`QO6m|(N@ &* )1@ #)) ) KQXY KQXY0#"&'532654&+532654&#"5>32|PZG]twGabLx\l%%pZXkYF@\]y` ?@B  F F 991/<2990KSX@  Y##3y`}`y&# +KTKT[KT[X@ 88Y1` Y@  F 2991@ B  /<290KSX@    <<Y33 ##Tsŷ`OQ5Ls`$@ F  1  /<0356765!#!L8D{X^~ŷoPO` M@B   F F 1/<290KSX@   Y! !### >? ˸ʹ`'P` '@  F F 221/<203!3#!#U`7qu{R`@ FF1/<0#!#`3`V{Sq{F<m` 1/20!!#!<1BB`3=V`\pVg (3B@5E)! '.E4<<<<1@,41$ 4<2<20327&#"#"323>32#"&'4&#"326/{brrb{9SS99SS9{brrb{/Ǩ<9^N5=L^^LN^Ǩ;y`[` (@ F <1 /2<0)3!33#9U`33R`;@ F21/2#I #IRX 8Y0!!"'&533!3Hf\45h)_Vu;;` )@ F  F 1 /2<<0%!3!3!3ڹ"ٹ`3+`2@  F<1@   /22<<0)3!3!33#"ٹڹ`333R>.` ,@ E  21@   /02#!!5!!!2654&q8$~͓7_ZZ^`'">`%@ E  F21 /04&#!!263!2#!z~~@9LZ^_n7q{M@ H<21@   IIPX@8Y073267!5!.#"563 !"'q2 ǚ-VړiVFHL{ :@ E  F2<1@/0"32654&632#"'##3Jq и¾.`At"`<@  21@ B  /<0KSX  Y;#" .5463!##zwwVtS^a\'qk&CZq&jBBBB|#I##Iabh#FaF`C`#BC`CUXC`C85YBB##Ih;#5##I@PX#@8#55Y/V?@N F <221@ /<20#533!!>325654&#"#߰Bvz||яLmedY).ПĞm&vq{N@ HE221@  I IPX @8Y02&#"!!327# ǟ 2ғ-{FViګVH>=o{VyLFVyML`6@!E  <1@ /<0356765!32#!!%2654&+L8DثX^x~~~ŷ7oPv_ZZ^`8@E   F2<21@    /<2<2032#!!#3!2654&+N޹"\~~`7`73_ZZ^/:@N F<221@ /<<20#533!!>32#4&#"#߰Buʸ||яLmed*m&voyk&C]=V&^` )@ F F 1  /<20)3!3!#TfUf`3s48@$%6 )  51@ $-/<2<0"'&46733276=332764''3#"'&':y{d;]TCHI}rHGFFtAGCT_8d{{ђed''deFkmihhimw'AFf^^^^'`]:@  <<<1@    /<20!2#!!5!53!4'&#!!276XNpqONDNOQQfDCDC:@E  <<<1@    /<20$4&#!!2!5!3!!!2##~~EW^͓Lʣ+#3376!2&'&# !!!2767# '&SvwhfstgFtsfjwvú 9$#G_//wƪ//_H$$O{#2&#"!!327# '&'##33676>\" , Ux{ z{FVAW^3VH`3ʀ !#!#!#3 73` !#####3 Ñkk`_ !#!#!#!#3!3  o_<9d7`!#####!#3!3 kÑkk`_s@   9ܴO]9ܶ@@]9991@B  /<<9<20KSX@  Y@]##767!#'&'!ʓdսxQPtՀ`>YY~b҆12z(k{`~@   9ܲ]9ܲ0]9991@B  /<<9<20KSX@Yp]! #4'&'##767E]kKV:VS8V‰Jl&VtO\KtU'4! !#'&'##767!#3!PtՀ`ʓdսUn>qd2z Y~b_49n(.`! !#4'&'##767!#3!7kKV:VS8V‰]w&VtO\Kt`?sVszS#"&#"3276&#"#"'&54763!27654'4327654!"567376767632'&#"ssD#`At bTDt;<}J5?u_hFAXVRuťޠsj#B#' "2ZbrRUgr %',azQ^XRj7&6J- @' WoWdE\`[tO#"&#"32632&#"#"'&53!2654'&'"#5223 54'&#"5673767632&#"vmDPb!',-cX;b12i?,ZnN .rr. >._- > ^ >‘  tӪ ҫ q{&P%327654'&+"&'&'#";67>2# '&5476!36767623 !#"'&'&r-HVV?- ,4, -GVUH- ,4 .xt. 4 .wt. 4 `ta  _tp_   颈   袉   vt&"'0'&#'s3'<cS'&<sV'9@  0Դ/?]1@   /0]!# '&76!2&'&# 3!#SvwhfstkSh$#G_//ӂqV{9@  HE1@ /0@ ]! '&576!2&'&#";#UQQLNONPccccɖ#+qr͹rq;'''7'77'77did}}didii}}}d}}}}dBz/!"'&'&'&547676763!476767623 8  8 g    ) M #&#"56763 v][Jw}$)/K'*Ca"53#7 a#55#53g M 365%$# ʭf'rQ q\t{F` &3@MZg#.#"#> #.#"#> #.#"#> #.#"#> #.#"#> #.#"#> #.#"#> #.#"#> v aWV` v "8v aWV` v "v aWV` v "fv aWV` v "v aWV` v "v aWV` v " v aWV` v "v aWV` v "AKKJLQKKJLKKJLKKJLKKJL)KKJLKKJLKKJLX- #)/'7'7'7%'%53-#%5#53 3#kyo\wyo\zV\Ly[`@¬@_ӤRӤRZy\yW\zn[wyo\ԤRԤR߬@¬@Vm&=yuV8&>!:@  <<<1@    /<20!2#!#535334'&#!!276N訨ʨONDNOQQfDCDC&E 9@ E <<<1@  /<204'&#!!276!2#!#5333>CB>ytts9L^*..+URRRя>'+#!2'674&+327'7Uj~ rGj#u~{Sqrے-,9/~V{)%'7654'& 32'#"'&'#367632*nOSTTSSTFoWl{XY::YX{ ]ststsjts].01d d01j@ 1/03!3!)2$ F1/03!3!`:33G )@  <<1/<20!!5!!!!!N)#l8U` +@  <<1@  /<20!!5!!!!!?`۪ f3@  <1@/0#!!!2+5327654&#)qmL>87||9ժFwrKK"V `3@  F<1@/0#!!3 +5327654'&#rFRRQn!&&1`GQ``07 )(33 3## # # 3׈)D"AMF`33 3###'# 3?nfz!n`QL6mu&z9u|&z3! 3## #E#A`33 3###Tw8sŷ`OL5373! ###ʭd_dTy%u`37533 ##5#`eBTse``avFOQ5a!33#! # ##53ʨ_ʨye=3!!3 ###53dTsŷ}}z}5OQ5}2 _@   2991@B   /<290KSX@    <<Y!! # #!2_=y+*` _@   2991@B   /<290KSX@    <<Y!3 ##!*8Tsŷ`OQ56@    8 22<1/<20P]3!33##!#"dA9@`1@  F   F2<21/<203!33##!#W`39L -@   8 221/<203!!!#!#)"d9` +@    F221/<203!!!#!#W`3ͪJft8@<1@ /<0#!#!!2+5327654&#;"rqmL>87||9+wrKK"V!`3@!F <1@  /<0#!#!3 +5327654'&FRRQn!&&1:`GQ``07&.sAY%.54>323267#".'#"$&54>73267>54.#"+9lR2*DaSN}aF-?jQ&h;>e3.x=&QUW+Byc[sp8<{R?S0 $0>&1H3!(BT1kBtW22Tp{:SJ#&4t}f|}ާbm:E/fcYC(+G[`_&bnqxz?P4>73267.54>3232>7#"&'#".>54.#"qKц][-2`X'V$?/(PtMBpP-\_#D-)*%-8%7CFIGԑLV"- !(,!(؜XFrXbr> %gx@]sA9hY^    , Tָ&^dc+KiB&HiCsu''z-qu{'z ,@ @ @ <1@  /20%3##!5!!A+<m` (@   <1@ /20%3##!5!!B1BL<=V`o@  K TKT[X @8YKTX 8YI:9120@BKSXY"%#3 3;^^DNl!#!5!53 3!ssf=V` !!#5!5!53 F;^^`XXNl=;%3## # 3 3p\Y/su A{+3;y`%3## # 3 3q!r))kLHJqG5@ @ @ <1@    /2<20%!33#!!5!!+A+B`3@  <1@    /2<20%!33#!!5!!xZ9B1B9L|.@   <221@  /20%3##!"'&533!3_qm||x˪Awr7ٟd`F@ F  <221@  /2#I#IRX8Y0%3##!"'&=33!3f\45h)L _Vu;;#"'&53;333###;qm||֐wr7ٟ9d+`5333###5#"'&=3f\4+ _Vu;0$@  21 /<0!2#4&#!#z||f9dK"*I@#$ $3 +291@ $ (+<2076! !!267# '&'&=3%!&'& ":Cppoż vzKB@bHam`_F$$UgkL>D9||f{%.i@.&&K /2@ p000]91@& &"*"/o]2</]90"'&=33676!2!32767'$'&&'&#"XY`09Jt⃄ fgjdcbchneNRS]\RZF1!&łZdc4*ZZWWu'Puf{'Q,(vm'y[uFH'f532+5327654&#!#3!qmL>87||qwrKK"9wV`3 +5327654'&#!#33^HRRQn!&&,%wGQ``07$)`6V!#!567!3#:bCux+8.%5ժV.V+`%3##!56765!s{{v^̳;bVdžf;1@ 82<1@  /20%!#3!3+53276q"L>87h_9dKKV`/@ F F2<1@  /<0!#3!3+53276WRQn!&`3``07V!#!#3!33#;"9dժVV@`!#!#3!33#W{`39V/@ 221@  /20%!"'&533!3##_qm||xɪwr7ٟd+`G@ F221@  /2#I #IRX 8Y0%!"'&=33!3##Hf\45h)p_Vu;;V%3####! !+-}-VV`%3####! !H{˸ʲ>?V'P`yOh'J+1@oo]0{-&O"+1hN&ru  +@ 0?  ]1{-&jR -( +@(o(P-_(@-O(0-?(-( ]1H{o{m'yu@@]1qH'@p]1uQq{uN'r ulq&jTm(vN'rQuF'jN'ru&j:yXL/`T31'q;y'q3N'ruy'jsN&r'u +@ @O]1qu&js +@ @O0?]1saqu{7sN&|r'uqu&}jso#N'rguq&j#1'qr;=V&q^#N'rru=V&j^#k'{ru=Vf&^N'ru&j^j #@   <1/03!!3#)ժA` #@  F <1/03!!3#`LFN&ru&jGV9@  <<<1@ /<20!!5!!!!!!+53265N)#iGRiL`na8VU`;@  <<1@ /<<0!!5!!!!!!+53265?`nFRjK۪`na=f+%+532767 # 3 3*SfL>7( ^Y/su bzK5sx+3;Vd` +527>5 # 3 dkkCQO5r))`&9as mHJq=;3 3!!# #!5!suNt\Y+wD{;y` 3 3!!# #!5)) ~q4H &@  21@   /03!!"!"$54$3!fONDNONNCD#CD+fq` %@ F E21  /03!!"!"'&763!5>BC>9sttyLZ+.i.*RRPRUC 09@2&)  1291@"-(1220!"32765#"'&54$3!3327653#"'&NOO_KV! 3j^nN?4pi;?nhf1CDP_m}`61f[JJOZxx9qs` 08@2F&) E1291@" 1-(1220!"32765#"'&54763!3327653#"'&=C>A@j\-1C]^fety>dhd.*^\:9m4l01a`RUaPOORAsxx%7@@9., ,#81@'2-28904'&+5327654'&#"567632327653#"'&'&\]OOQRSrsdeY憆GGRQ?4pi;?nhf0!JK;$& hi|UV!bb[JJOZxx8PaF|5G@7., ,#61@66'2,6 KQXY04'&+5327654'&#"5>32327653#"'&NHtCDFEwGQPabLqr<=ih<>dhpb8f83,-F@.. NO]@AHOHXDEORAsxueV<):@  '+%*1@!'(/90!#4'&+5327654'&#"5676323#s\]OOQRSrsdeY憆GGRQJK;$& hi|UV!baV|)?@ !+) *1@ / KQXY0%3##4'&+5327654'&#"5>32ȻNHtCDFEwGQPabLqr<dhpb{v^̳;b`WORAsxue{-`6@F  F221@  /20327653#"'&=!#3!zgh<>dhpbW`WORAsxue{`3s0@  1@ 0# '&76! &! !2653d-|e'%{9!Ҏ׿qF{0@ E E1@ 076!2&#"3253# '&q кĽbZZb/n||r|r|>禞f/@  @@1@  20327653#"'&5!5!?4oi;?nhin+[JJOZxx}q`2@  1@  2 ]0327653#"'&5!5!x>=ih<>dhpbB1VFEORAsxue{~{R|ITf:/@ 1@ 20356765!+532765!T:WxM?77fb0dKLøLVs`/@ F1@ 20356765!+532765!L3DF1a.&{X^}з0)oPT 35675! 3 # # !T>Wysu \Yfb/X+3{L` # # !56765! k0X^̶8D')`HJoP~ŷt32654&#!##!23 #h /ϒ0*3V{ ##"&'#3>32&  k\{::{T%+ܧ$`tad dakj3&$54$)!!!!!!3!!"d;>v78ȒFwtw{&/!3267# '&'##.5463!632.#"%;#"w ͷjbckVteVgKww^Z44*,'ėS^a\s4qVZ{TD:V5`ZTfs%9@' !&<1@!/<035675!!2+5327654&#!#!T>WxqmL>87||fb/XwrKK"9+LV `'9@)"#(<1@#!/<0356765!3 +5327654'&#!#!L8DFRRQn!&&,{X^~ŷGQa`07$)oPft!?@ #8"2<21@ /<2<203!3!2+5327654&#!#!#qmL>87||"dwrKK"99V`#@@ % !F$2<21@!#/<203!33 +5327654'&#!#!#UFRRQn!&&,`7GQa`07$) !!#!3#q"r+A9` 3##!#`9L3` F@   8A!p] 991@  /2  9033265332#54&+! '&ˮ® ,gQ]*-呐u\GCF1l[R.$)K@  8Ap]2<991@  /Ĵ`]0 ]376! #54&#"!2#54&#!$ˮîXgQ$9 𝶫F1l[%D@   8!&p]<2991@    /<<0O']32#54&+#!"'&54! 4&#"3)GgQG*ɟn(!ˮî5ZrF1l[=ó|#ӢI|H@   8Ap] 91@   /90O ]32#54&+#4&#"#576! YgQGˮîːZ`F1l[O 9$\)$30!2#54'&#!3276=3! '&X_`07QWWWWˑ呐1[[F1l*1jiij 9㒕$2%!67#"'&543 2#54'&#!3 7654'& f<0I|q4_`07Q5˧OPPOOPP'.ƪV][[F1l*1LL]]]^^]])D@8 :  2]99991@  /0%!2#54&#!3!2#54&#!}gQXgQF1l[F1l[)@@  8Ap]<991@  /0]376! #54&#"!2#54&#(ˮìXgQ$9 $F1l[-:#'&'&763!&'&#"#76! 32#54'&!#"327654:gimINK(*WWWː\!%_`05л9:E5:. rs TfLQR2jjiu$[[[F1j,1i--Q@+#! '&4763!332#54'&)"32765pG혐nG_`07TZ5WWWWܕ.|n[[F1l*1}Hijji):@ 8Ap]21@ /09]363 #54&#"#ˠ(ˮ;dK2V 3@ : ]991@ /0@0P]!2#54&#!}gQڶF1l[327653#"'&!#3|%3x*%qXdq`>WWK7}bbpiOA$3! '&7#'&=33!2#54'&#%" 76'&ɼżg``07Q_`07Q|y&bc\[F1l*1[[F1l*1 椤)!## '&33276=3)ˠ혐WWYWd+&jiih) !2#54'&#!5 uw _`07Q1k,[[F1l*1f'1?%#"'&543267#"'&543 327%&#"32 7654'& oUIeβr0I|q9I9~dX/? 9.YOPPOOPP@$2iw'.ƪdkWM( ]]]^^]]?@  8Ap]1@    /90O]%32#54&#!4&#"#576! )GgQìː!F1l[ 9$\,3276=4'&#!#5354763!!"!2#5# '&WWYW07Q `_# Q70X_`ˠ璐ijjgl*1[[1*k[[Fd%!! '&332765!2#54'&#)呐WWWW_`07Q& ܕ$ujiij[[F1l*1S" $53 6&#!5!2654& #4$ 5JRS覥A ++.WHNMItYa[J\n@@  81@   9/0326=3! #"&=33®ìGœgQm 9-!F2lZ) 3276=3! '&576%7%5zZ[WWˑz=s9W/hiik 9ψ&dAU)7@  8Ap]1@   /<90]376! #4&#"!ˮî$\uB)4'&#"#576! %5%$76aZ[îː 1y=\gW/ίgj 92dAU##576! #4'&ˈKuˮ9)uBGlP| 9\̍P0%&'&43 2#54'&#!3!767654'&'& Eq4_`07Q5e, 7OOPܪƪV][[F1l*1L,@B@^^]t~H@   8Ap] 91@   /<90O ]32#54&+#4&#"#76! YgQGˮîːZ`F1l[Ou$\)8!!# '&5332765332#54'&#^혐WWYWG_`07Qd)jiih [[F1l*16).@  8Ap]1@ /0]376! #54&#"(ˮî$9 uS0@ '&53 7654'&#""#6767&'&5476! "327654'&RQJRSSSSRefg#RHJIIPacIJIJcaW"ccttstNMMNMMM *c" Y[`XX^[Y01YtAAAAtY10 =@ 8  Ap]21@  /0 9]54&#"#363 3^ˠu2;dss:\,<47632#"'!2#54'&#!##"'&=337654'&В􄑑I_`07Q _`07Q*]WW]_WW_rsppzzpS[[F1l*1=[[F1l*1A>T]=BD=[V>Cs2167654'&4'"!"'&'5&'&547632qG^CC95+<&0kljxw{vEB[eK[ 4D~n>=>@%c3A +mlpp/E# ,,W`aru~^#33vx%"#476327653[RBhj[RBhjTDDjlTDDjl}fC^7#47! !"33254'&'#" q3U7a\ "9S A5z\&NZ%03!Z}4b`&^@PP F'<91@  #/<<<290@0(P(p((((((( ]%#"&5332765332653#5#"'&E``ruSSrw?XXyzVU|:<b`^zbzh02>>Vd{?@    N  F22<1/90`]54&#"!!#3>32||Buܟ6V edqV{ <@" GE!<221@  !032654&#"##"3253!!/+:||:Z/\RdaDDadOV{=@ N  F2<1@   /<  90!#4&#"#3>32!d||BuZVH`ed X?@ NF2<21/90`]3!!3276=3#5#"'&>>|TVCuddZL PO_bvfcxxqV/{<@ G E221@ 03!#"325332654&#"Zs:||:էRdaDDad,@ F<1@  /0)3!!32#54'&S[zM`01LI[F1i&&Vd{>@   N  F2<1/90`]!4&#"!!3>32||VBu ed\V6{ )u@ +G  F*2ij$!!$ISX $<323#'&5476#"3276#§:{5`4xBdBJ4/' daZ+h|{Nvqq<q/ 4@ ! GE <21@  <<0!"32765#"4763!33ƈbOMSK}<zaksC+D߫LVd5@  N  F21/90`]#4&#"#3>32d||Bu\edV` @F1@0!!3y^ VI@  NF221@  /< 90@]32653#5#"&5!#3||Cu`a{fcLq0\@ 2 $G,E1Ĵ,1@ 011(1<<0!""<<!<<#"327676''&5476;#&!!'&'&4763[AS].SD81N/Vɮ!qZsIR\++(VL-%)$?뮘VX:@     NF2190`]332653##"&||CuZ{VfcdKqZ 4e@ GE5<@ (''*%%*39/ 91@. '/ 90@ `6666]32654&#"#5#"325&+"'&5473;2/nD:|WCv>!%7)/kPըdaDE<6pG5P0,!K7V9{;@ N  F21@   /  90!4&#"#3>329s||BuH`edTX-b@ (N  F.<<2 -9   /1@%/<<! (90#5#"'&=47#5367$732%326=4'&#XCubdzzp>BiO>AycW fcx{Iʪ`&$%8vJMO;) +?@-% $NF,21@ &$&)$/90332654'&/&7676;#"#5#"&|| M.=<(`Cua p0.- */(fcVy`2Z#G@%  N!$21@  !   /<90;32653#5#"&5#"'&5476;#"||Cu;^PZl}YYa{fc^PzKWV{!<@ #E F"<2<1@ ""0!  3!!"'&547654'&#"#4632/Q@'$C#@l;qsDE E+G56dZY0Y^cԫeed{QFV;`%X@##'  &9/1@#&&990%   ! 3!!"'&547676/&5476;#&(3W:'$F[L2se`6g+! E/>A/(32||BuƯ`ed X`XV-=@    NF21@   903326533!#"&||sCua/Vfc{%i@  PPF&<<1@ "  /<<9  90@0'P'p''''''' ]3>32#4&#"#5#"&533276BYƸ||zUVCdȸ||XW{ed\_`fca_\Vd{7@  N  F21/90`]#4&#"#3>32d||Bu\ edqVZ{J`@F1@/0%!!3y&"`V%k@  PPF&<<1@ "  /9  90@0'P'p''''''' ]3>32#4&#"##"&533276BYƸ||vYVCdȸ||XW/ed\_\Vfca_\V{$U@&E  G/<2221@"%%<<IPX32#"&'!!#54&#"326չ:{{:+Īdaad)qu{RzV*"-6u@83 .# *E7<<<<<1@&7/  "7<2#99#93.  90#,<<. #"'&'53&'&547632##4#"27654'&,Dd%Kcfep_{5S#al~EU@<%I]7_E8BQ-a`ta2N-bliZn!vFDs:#+IJ>8@    NF21@   /90332653!!5#"&||^CuZ{OfcR@<21@//073#3#R` 27#"'&'3U oo,rrONcAUUWDC <21I:03#3#D-dC'KRX@8<1YC %  <<1@  <5G.i=dB]Gg`":T)yX`!  1  /204&#!5!23!5!&nZͦy–1CZ`G 1B /<0KSX@      Y4&+532##n̒[^ޕ<S"Xh`$1/20@]1#!5!t/яd`4@ FN F<1 /<0@ @P`p]!#3#4&#!5!2snvy–t`FF1/0]!#3t`X` #  1/20@ ]5!"#7XNrXGяy Kd` (@ F N F1 /<0@]!#4&#!#!2dny–/``*@ E F1@  <0332654&+532! w`ҏ/t`FF10]#3tXV` , FN 1 0@  ]#4&#!5!2nV#–X` @ EN <1 /035!26&#!5! #Xt뒦X&@ F N1 /04=!3!#T[CLzld` )@ F N F1 /0@]3!2%!4&#!6n`–X`^@ F E991@  /<990BKSX@     99Y"#673632!5!4&WWHFdaxѧȠ˨Vt`FF10]#3tV X` %F  1 /0@]4&+532!5!ny–X(` *@ E 1@  20#5! !"264&+" я0D_ЍNO`U@ F 991B/2990KSX@  9999Y%67676535673VGu",:pΈLƒ4U}*p>1=!"$Vd`1@ FN F1@ 0@]#4&#!;#"&5!2dn\pTQV#–U;zdd`,@ E N F<1@  /0! )5!2676&+;#"&5*4{\Lwuq`U;zCVp`D@ F  91@ B 290KSX@  Y#3>=3#q_V`}՛C!`J@ F  991B /<0KSX@      <=3!5!CcMgXC"`ԛ:V`,@F F<1 /0!#76654&#!5!2#3l)WzB'*˺u,/HVv.4X` ) FN 1/0@  ]!#4&#!5!2ny–`/@F F21@   /<<033$763 76763) :0nLaT`Sl+7`+@ F 1@  /<20!#4&#!+53265#5!2ndDrL~y–a; `')) `')- `'--`@ D103#`n`@DD1<203#3#`|"%0#4'&'37676537653#"'% '##5 rb{ .q & q-aT !}Bs12j{@E#$]} q!<"ibP-F`)*5"2767#"'&54767&'&5&76 '##5M@V:118UF%/>7P6.N@?^G?D)7-#F}Bs)^ &# \*$@.") n F>]KH*!#TH#bP-F`z %3#%3#3#%3#ƴ>^< %3#%3#%3#3#%3#>>^!#53ӤR@ 327654'&+5336767N5G4pQf$h?FA@6b ! eI(R[2* #53 3#ӤR%@-$%#5754&'./.54632.#"'/XZH߸g^aOl39ZZ8{4<5/VVL89CFnY1^5YVeU"756767&'&54767632&767/SD435gcbnZdF31`9:H:ZU!LOTAKv?=0ps2#nl '{R'z>oy3#&}9&m~ &~& (f&X} (f$3  !27# '&5767"$JKԖ^`e~h'?6`vc–e4- (&X}?}R%67654'&'3#"'532# b&\}q  ?%#&'$473327676'&/3327653323#"'&'TPxmil_Qb_y^@@$;sR,%@n\Kf% I01_2F,k>GHܳ&%0l}=J"5^.327654'&'&#"&#4763&547632#bzL,5;(.;Dn2KxAZM\MObxX'*9:X DD(NOf7*(?$S-8APH&}? "327654'&'2#"'&5476B!799[]KB{ƶ`Q%T*WE{R,,9.UMAx|KU#JN @ &"34'&!5 767"'&'&547632?,3/V%._]g>v-(tYhYH9!$3/,;̠X*VL_ !"bWg3ZfJ6%#"'$47376767654'&'&'&'4762#&'&'&VfxH?Ba=~T;~BrC:@_` B(EN><}9M I&huqc- !P85J.39sJ%*==!'&"7*S@UYD J&o~ $5%5%HHnnnn$&567&'&54763233"/#"'&5332767654&#" %!lE?I(7 /4KU^r8Z #08 " -d$* 9^W4'6O'&n=NV)qaK" %$5%%5%HHnnnnn$5%Hnn$-&'&5476323"'&'#5276767654&#") lE?I(7$# +EȓV " - 8_W4'6O -n=*{nmp" %$5%Hnn8(#"'&54737676533254'3'&!9EO)"a 2=`YG g -SGL(E?4mmb}8T"RY$6îs9It6Y ! 4&#"32>"&462X@AWWA@Xz柟?XW@AWX栠h732767#"'&'gC*6:)kXZZC5"LMD6{S )L}@FOwO  4373ËF3# !#'3%1yI !nR#'337673#" %1BR{6)coajr!nUPymL%#'37676537653#"' %1/(/H/; 'G 44.5WY9!nr|> @2%,*;l>3  *"2767#"'&54767&'&'&76#zf\MOYp0;JcX~VI|eepdkAXH,7p 4C@#90L@rRiUZhsBBsǮuu5aU#'#"'532N%bU`DK*22<!&'3673b~ĚZ00ZĥxU:Ũ ;6I<3#&'#6̴UxĚZ00Z~bI6; :d#"'&'&547632#54'&#"=:i_{\ %Z[,,G\O98<SGU37e{a}UwnWl42@B^!x$%-`+-!d! M fM&I&9 &9 &'~& && & (&Xz8 (&X? (f&X~ (f&X (&X (f&X (f&X /'I>\ r'|>\ &'|\ &\ :654'&32! '$&73! 76767#"'&54767632)B,4((7(*Hnق@AZAd#?zKbNLZB`.+M;3*)3P&ڴF=)d \^tL"9;l&NKCW4,E$2Hf6&x~&xx)-%2767654'&54767#"'$473$62 #dGf>5?AhXPA7.EB|=Q#!w*6(  %{{qeVUI&b \^~B")+&&j|H#"'$47332767654'3HdnaPm/1]]LGL"fh8D%jdQ45b`ޜ ('&X}? @r'|>nJor&o|>m}~RLR%'&547632&767#"'#'3X\lTX\D8/0E= %1Bx:=$!"4'Qjr!n8j$(327654'&#"327#"'&5732#"-2!WZWXZV%2-Z(.5__52ZJkV0B7,g`p5oU%mao3/AbM3))I<<d (@  1@  0"32$  h P3343ssyzZ (@  1@  /20%!5!3%=Je+HH=  21 /203!#3ulh=   221 /0)5!!5!3=lȪ=   21/0%!!!3!l =21 /0!#3!=l*=1/0!#!3!=lcr8A'91/0#3ASuNA (  < /<10%!3!#N{ 2@ EEܲ@]91@   /<02>4."#&'.4>329[ZZ_PGr䆇䄄rEMp`77`p_88 1ŧbbŧ1 y@ 1/03#+q!/@ E  EԶ 0 ]1@  0 6&    z>z='+@  2291@ /2903#36Q*=q33# =qCq @ 1/<0)3!39Uq"q @ <1/0!5!!59qKqO!>@#E E"ܲ@]ܲ@]1@  /2<0%!!5!&'.4> 2>4.":RJr 惃sKRQ[ZZ{ 1ũbbŨ1 p`88`p`88 %@    21 /03"3#!5!p9 fq2@ E<21@  /<20!#!##"&6 54'&"3qvCf^]8mr^:<UfɃ]8ƃD '@   <<1@  /0#!!!y5!Փ/= '@   <<1@  /03!!!}5!Փ/ %@ <1 /0!!27654'&'2#!3,R4,,=iXXXlι]Oz}I__ҭ$;@   ܲ_]9@   /999@ 10#4'&'5!4B 5McAq_9V= 491@ /̲]촍]0 53#T9+!-@ #"1@  !/203432>324&#"!4&#"!}x5%^ZHZlK--Xh&|ŕnc= &@   <<1  /<<0!5!3!!#KK?=9@  <<<<1@    /<<<<<<0!!5!3!3!!#!KøL=??q!@ 1/0!!9UqqK==1B/0KSX@Y! #tFC00B~+n 4@ <<1@    /<20327654'&+!!2/!!m]%i ;@ED\TqQE=4."XErrJSRJrCEoJ[ZZO{ 2Ʀ1 { 1SV/p_88_p`88} @ 1/0#!#}+B} #@   <1/0#!#3}Om +@   <<1@   /0!%!!5!!z;  TKѓ+qO $=@&E "E%ܲ@]<<ܲ@]1@  "#/<<02>4."%#&'.4767673 [ZZTXErrJSRJrCEoJR"p_88_p`88 2Ʀ1 { 1SV/ qO(#&'.4767675!5!!2>4."XErrJSRJrCEoJRNQ[ZZP 2Ʀ1 { 1SV/ p_88_p`88b/1/0!!VBf#"&/#332?E=9Qct2 %xf" %/x $Dp/1/03#=f7u91290K TKT[X@878Y3#'#f[fE9190@ Ueu@ )9IUe]]!5'3{Bf3326?3#'#"&'Bx% 2tcQ9=Ef$ /% "[fC9190@Ueu&6FZj]]5%3%[{fS/1/03#̭F'/1/<<03#%3#\yu  <1/0#527#53gu  <1/03"3#  gd 1/03#!!Mdd '@  <<1@ /03#3#!!Mޒ1/0'!! '(033!!3'#67654'&67654&udruxtNMddx>DD>xIIv! RTxXY`aw,0dc1-!:;z{t{*L@$% E+<<<<@!#91@$+<@ (+0%"3254"3254#"54!#"543263 #4#"h??AA??A'+,LW@@@@@@@@pطQQ9/@@1(. #E0<<1@!0%* 00"3254"54$3  !2632&#"# 54-654!"`@@@CvBըiUv˫:knL?o@@@@N;Ejfae:.88U8327&'"254"%47&5476! #4'&# 63 #"'632# i60IKhh*)7!o^RX;*:9u`/'"6OfqAtqLI $\9.ȶlQ!6@   E"1@"  "0463 #"&'7325#'&&7'6met "xCBCquЍ h! ACBB )2@  #&E*<1@  *%/0"32654& 4''&5432#5476$ % U%|{e6Lj` %"%:yx~)RhKK>  65@$- 3 (E7<<<1@ 5/7&7"32654&4763  !27632! 54-654!"#"`$ % 琺By#xJi:OknLIo %"%0yKpjNdfDQcwiC|85sr *;@&%   E+<1@")+&+02654&'&47&7'73%$$!% l݁6ZA| $! $Vm-G4 p?{1@ F1@ <@0%"32544!  #"54$32@@@)@@@@Pvv .<@- " 'E/<1@ $/-)/<20%"32654&672#4#"#"'&#" #"53232l$ % L 7*>(z*M#6&8"$ %"%3|0ۯqiPWu|+?@-$'+ ,<1@ )!,&,<0%"3254"3254 #"5#&767663 #4!" @@@@@@!Ӣ7y-^@@@@@@@@edm%W ,9@. $  )E-<1@ '-+"<0"32654&4323254#4#"%$7"@$ % 쐋'(uj %"%@կ̰Xsgh\_"9@ $ E#1@# #<0254#"53265$54767653!"'#W@@>z]U]iTrs@@@@pegu/ssHs|2@  E<1@  0"325447&763! 3%$5@@@ԶMg@@@@R&Ѩ'LBHs2@  E<1@  0"325447&76! 3%$5@@@ԶMg@@@@<%Ҩ'hBY E"32654&!"32654&&''"&5623253765$7465&'7$ % $ % Kfg饤IJ %"% %"%IKbv4ˋ42@7-]fn9h%A@'$ F&1@&<<@" &0!"'# 432!32533253"3254hfg襤>@@@ JJ=|\@@@@@h} -?@, (,$ E.<1@"&. .<<0"32654&2533253!"'# 47&5432d$ % AfgB %"%4˩/JJ=%܉Mh -?@, (,$ E.<1@"&. .<<0"32654&2533253!"'# 47&5432d$ % AfgB %"%4˩JJ=%܋L@`$@1@  <<03!23! '#"543225O)3Ɯ)`,88{r *;@&%   E+<1@")+&+02654&'&47&7'73%$$!% l݁6ZA| $! $Vm-G4 p& ,7@  '#E-<1@+.%.0"32654&4! ! &# ! ! '&54323 c$ $ 6buUKX $ $8${nE{N%O 0@@2, %&E1<1@%/1!*<0%"32654&&'&'&5! 765! '676%&4% $  ,D )@ ' 1#-E5<<1@ )6/%!60"32654& 4%$54!232#"'&#"! '&5432h$ % ${ajjh@MqKy)LJm_ %"%1EYl0xP^b8Rsu_|]F'"2''&'$!32'&547"32?6AS2;9’hhNU~ +;9jq!Ban' u _ +@   /991@ /0! &7623$'4'74"Y#!A[VB8?<kP$U.FM?>={{+@  E1@   <0 ##"2#"53254#"n=;C>@{jVR777r&" @ji  /1  /<20! ! !5 74! %&?%~?>~@i$@  /1 /<220! ! 3!5 76! %&>%~?>wJ~~@ji*@   /1@   /<20! ! !5 74! #5%&?%~?>~N@i.@  /1@  /<220! ! 3!5 76! #5%&>%~?>wJ~~T3"36654'#"5432AA\(DeN[̼o[$N[u%@ /1@ /0"3254"547&54323253r>Juum@s> [yu?{EBXF_ '656%"'&76! 4"3YVA!. {x9322674&#"CCjFPH OQ$!%!p'(FnJv-O!3] $ $z{&01, ("32654&&3 #"4/&5432N$ % s $ˌeqɘzm %"%82y,v\#"6@ E#@! 1@ ##04$54%&&5! $#"57"3254ix@@@X4|`Pٳ ?@@@@ ""32654&5&'7!$#"47#$ % dt.; %"%Ȉ_p 8>u%t/;4#"#"'&#"$#&532327632! '&57"32654&"3C2z7J,"/IN\=0BWTO3H$ % Xt\DD\t] 5<\UCfwpv  gH %"%V@/1/03#V~!'@ /1@  /<0'6"%)56574 65+*+UGm++),}݅.p\(>.4"!27676327673!#5654#"'&'&#";&543.%2~*&IHHܝBOg(LBC]i%>e>.`h>3A?~= h\$kb8:;-F_Zkf2)N !@ /<<1@ /<<053533##5N؎؎؎ P>r@ /1@ /0432#"73254#"ЄLTPPHHH` " 7654&' ! '&476^L:NbX1coqoh`WĒcg&24764'&#"676'&'&5476  pHgc/5pIu upHECle\gUܚsuϨcy\$24"27#&5432# '&5?$5+r%3]f́|pHFPfouTapH/%24'$5432327#"'&#"%$'#"54322533]L/|tkZ1AQf(3Ɯ)DjR:jTh8KOpt$68{cW%24"$'&5?$532&'&32!r|T9lc ~x?LvTamY<KcW-224"7&5&326532&'&32$'&324!B}b$|T9lc ~xr=Ch(筭 ?fXmY<KLvttY4@'&''"&54323253765'$543227#"$#""32654&fg饤u ^|uISL\>$ % ,IKbv4ˋjEaTW8ҋ %"%{ &%"324"324#"54!#"543263 #4#"h??AA??A'+,LWpطQQ%Rpt MU"32654&254"#&76767%4#"#"'&#"$#&3232763276'767$ % nnvp+-"2D2z7J,"0IN\=0J%.3?5xv'Q %"%933hk//3wt\DD\t 5<\UCrTF-2bG;"b,i $5354#" #"524"m~ŶejsX\|9~ LX"327$"3273253!"''&76324%$7&76%$5#0#&76262654&'&A?A?fxԅ$8$+Rb,7Hu Ӣ5r$!% @@@@@@@mӔJce$3- /ԋu cd $! $~ I"327$"3273653%"'%5254%$7&76%$5#0#&7626A?A? Tcb*@RX6&$Hu Ӣ5r@@@@@@@mo6J,/7'- /ԋu cdPi.".54>7!5!!"32>54&'7i7eȬd7&KlGqǔVXxyӚYlūc66clJ7^sz֟\[{6yEr2b\TZ@#!#".54>7332>53!w!KNM#hN&?Q*nq-Nj=8kT3$ KfWxc*s@nQ/+Lk?Z 5!4.#".54>2!/A%'B/+(=B=if:y'D33D( R0oCEOc88cO'MP.4.#"32>7#".54>7!5!!"@YmEgLLfjJkoX؁q؝XGxdI(YjiMKkii۫uuZ8!4.#".54>32i+Kg<>lP-7:p0M6NifL>@kK*0Rp?>?1ill3eMF}gZ,#!#4.#".54>32!|/@%&@0&%BE;hRPg;(C03F(#P/MCOe96`PFZ(4.#"32>7".5!5!>32&/Oj=kOOδMEHHjMoP.)NpF@pR00RfLLfan0/IP- %#"3!!"$&546$3!!J׉@@ט`a( ]wxԟ\Fww2P:G!!3!n!.x1Z+(4.#"32>#".53>32`+Li=?jM*,Ni=>hL+iJEMfLK{W06\xCKxT.4XwA_bKr62NpZ+4.#"32>#".5##!>32*+Lk?AlM+/Pj<@jM*KihNIHk?pR0,PpEBnO--OnβKKgcBvj12KZ+"32>5!#".54>3!!5!!Q@lO--Ol@>iL+MghONiL.QoA@nQ//Qn@/eMMehJ{PS$!4.#"#4>3!!"632,Mh<>e-PKhr>iM,fL?nR0*'R} gM1Tr@aKfPc K4.#"32>2>73#".5#".54>2*LjOwϙYVz|՚X0/':Yr?DsU0 E nǬc67dȭe7><qU'!RkL)[z{֝[W.>#K]59_|D 6clkǬd77dk{Z'kE"1%P#".5!5!2>53KeiL*-Nj|fH'eMOfXAqT12Vq>P&!#"&'.5467!5!32>534JEp=AB7D4+! )#$+e;:iP/05IGHeLJ )RpEn),/,Nj=Z""!#".54672>53!`NgiMNL6--Nk|jN,+eMKgV[@n6@nQ//Qn@]S#".5332>54&'7SLfiJ,Mi>=iL+>5{-H3eMKg@nQ//Qn@6|>/dfdS 4.#!!2>7##!!!2/Oj;;jO/JGHܓ.gM@qR0,Nl?dEHCMPbF4.#"32>5>54.#"#".4>32YywКYXxԗR6aO"?/$/ .@KZj>mȬd78ekoɭc5[])D0z֞[[z{֞[WwAoV5'//!6cǫc66clv?GOPb 14.+32>#";+##".4>3!2>mTEETmFUl>>lUFk\܀EFޥ^^ހWܢ\YqA@n@oWVn?~ٟ[][ڟ[]F!#!3!3!3FS!4.#"#4>32/Oj;53`EIgJ,Mg<:iO/02Kf>mQ0,Mi=nB-#".'332>=#".533267653BLi`R0Lc8;jO/FIiJ,Ni=:c'YgKSkFzZ40Sm>1/Jg@mQ.+(P|S!4.#"#3632+Kh<7g4QɑeL|?nR0++N|uaKfPk*!5#".54>32.#"32673YUlǬd77dlps[.\YS$wЛYXy^Pr2@6clkƫc6JH,Z՞Z;;xXZ)"32>5!#".54>3!3!Q@mO--Om@>iL+MgiNNi.QoA@nQ//Qn@/eMMehJ35S !4.#!!2>7#!!#!#!2 )4)2VsA4(AsV2*=&$;,S~U+ 9/XZ.?#".'332>54.'.'.54>32#6.#".KfbT1Ng:jN-VU^]4R8eMRlHzY3/Qn@72*63UeMTkH|Z4/Rn@)D BFRZS#"'!!332>53SLf,Ni=53 KihN/Pj732>54.'.54>32#.#"]~|ۤ_-H3K7>kTSj==jS8mV4/Rn@8gQ6';#w٥aL~מY[{:omn:vQNVl=53 ,T,Pf:l #D;%?.a=4.#"32>7+!!#".4>;5!5!54>2-  -)//?%&@.%?/:fPW2WvCDwX22XwDp=  @xY9lY|PW!%! %674#"&5! % %1,lշ._z+,S.+RLo ۤTn8d`'675$!2363 ! ##&!"#"32CxuM6sc*rE) PlaؕyZdX!&732#"&5 ][*8F e]N/I3^@[7rr2dX'!&732=6+537#&5! nN ggGVzkB3L.ķ@JKW~Xq\,d!$75&7! &324'"6Z^,CH!IJ:QU,X\$d56#"! !2363#"32UTcD>0R^<]td'6#"$! +.!TueudY! 473254+5365!5 Wb 퇇2mNEIJ(bC+d`3675$%2363363 565&#'#"#'#&#4%"fDjPQUOR Tg@! 5y<O-6d! !234#"#!#"2mLC{%  }>e~! )!363#"7Y`PlB   ry_d56#"#'#"$!2363 H LDVza!t#rd!! 4732+53274'$53X`4"gzҶ/c7Qib6ȕ!6G))=HdY2! 3325 '%5%Uc| CGko 4Y_nd9$5$#"#'#"$%7367 > B)oQT7-ngDP5kn1w5! 3324&547cTɜW\wؠ?c-'9dY: %3! ! %#d6*Q&q)QGFޕd$! %35#$ 3#3%#" 5;54 X`dHrrr44OfkQؔcdX2&!"'#!525#"3$%2363 #"321ZG\KVOvBppdY_!! %$54#"'! ! 4'7_GD `U6I@bYsrg8A:ԃM){6\lY(3324'7%#"'#723! ߫fB߻cV̿0?7YpdW $!6=3! 47$$5! eڞòkHuLL8TWJ&)*d54&#"'675&%'%"t_CCt?h]|KytJfqI8=ۣ&*2 5#5#5#d 2"4;%"4#"32;ѹF|pux$LRQ´h=@ B1/0KSX@Y %##.d+hK'Eh)hO'zt@1B/990KSX@Y sNO'z)tN'r)u'ew^?1B/990KSX@Y 5](&xyw^O'z1t'56'&56'O'56O&E'E'EO'EO&O'z0'wE&O'wEO&w^O'z?0 3#!38Ygg`nC^^n7]^7nn7]]0d"&533265453zWA@XzCss!AWX@+!U#454&#"#462zX@AWzB+@XWA!s0U!5!2654&#!5!2@XX@s0{X@?X{0U 4&#"32>"&4623X@AWWA@Xz柟C?XW@AWX栠H> %'111 ]]1<203!3CC~K3#K!5!${1V #5#53533zz{{1##5!z$ %{{:'U'"'=wq'h9hK'Eh0hO'ztw^:<1B/0KSX@Y7 5wM40w^O'z)tw^N'r)uw^'w^:21B/0KSX@Y%5^xyw^O'z1t'56&9'56&O'56O&'wE&O'wEO&O'wE&O'wEO&w^N'r1u<291B0KSX@}}}}Y5`sbbs]103C)8)K'E)*@ 8AKTX8Y1  /<03! #4&#"!!ˮî$*\u)O'ztw^ 2 <1 /07! )5! )w5BhPa.,~w^O'ztw^N'ruw^'y` 2<1 /0%! )! !`aPhB5jiy`O'z"t&''&O'O&'w'(O'wO&('y'(O'yO&(' ~21@  0# $54$!3#"3nn͙ nn{'|'|w}'dy'F> %@ 21@  /90"32654&"$54$32#Bz_̀#R3IK'E %@  21@  /90"32654&#4$32#&f̲_ȭT#R3{O'ztF> (@  21@  90%2654&#"3#"$54$3Bf̲_ȭ벃F>O'ztFN'ru (@  21@  90%2654&#"672#"$53z_̀ʃIO'z5t'F'?'~'|?O&~O&|'F&O'FO&?'~&|?O'~O&|?&~  $~ ]21@ 02654&#"632#"&53XP^J\TaaQ_VFTHUGQK})~J8 2654&#"03#"&54632xOaT\J^P_KQGUHTFV}i~F'x'F'x'F> 1 /0#4$32#4&#"#fK'E 1 /04&#"#4$32f#O'ztF> 1 032653#"$5fF>O'zt FN'ru  1 03#"$53326f餗O'z5t 'F&?'~&|?O'~O&|' F& O' FO& ?' ~& |?O' ~O& |?& ~ ] ]1 03#"&53326yaO\T~JPML 32653#"&5T\OaQLMPJ~w:1/0!#!5!)+jK'E!j@ :1/03!!)ժjO'zt!w:1/0!5!_++wO'zt#wN'ru#j/jO'z5t&5&w'&!'!O'"O&"5'#w&#6O'$wO&$'&&&O''O&'&&]10!!3 nC ~21@  0! $54$)!"3͙ nn{3!5 nw} (@  91@  20"32654&'2#"$547!5__ȘLӦnjFY 'i<FY} )@  91@  20"32654&'!!#"$54$C`^ȋMӑnj 'zi<<w "@  91 /20%2654&#"!5!&54$32__ȋfLnjw'z<>w'r<>FY #@  91 /20%2654&#""$54$32!C^`șMgnjFY'zT<AH}':w}';:3'AFY'yA3'BFY&ByFY'rT<A\ 2654&#""&546 !j>_IEcI_(0MJBSKFXCIn~|Q;n."&5332653ܨabaaJPMMPJ\ 2654&#"0!5!&546 _IcEI_>jm0(MICXFKSBJnn;Q|~w 1 /0%2654&#!5!2#bŘ쥒FY 'OFY 1 /0%"$54$3!!"Cꏙƥ᪑FY'z<Ow  1 /052#!5!2654&᪑w'z<Qw'r<QFY 1 /0"3!!"$54$3CbƙFY'z<TH'Mw&M;3'OF&O13'PF&P1H'Qw&Q;H'Rw&R;3'TF&T13'UF&U1\"3!!"&5463RiPYnvDZHCn~}w^ %5-5 ^j22F  ? 1 /0!3#$53TCc Xon2K' Eh @ ? 1 /053#3  cCT-ncCO'z thF   ? 1 /0%#5%3# c--noXF O'ztjFN'ruj @  ? 1 /0%!#3#c-gCcnO'z3tm'fF'f'h'hO'iO&i'jF&jO'kFO&k'm&mO'nO&n&m  ] ] 1  04&+3#XHǜV+.#"#"&'532654'&/&'&54632Cw7Bh#-8GC>=JGBAm'./G?;=~ÇH)@@V\`RʺªV\`RʺªhZ·%XhZ·Fl632#4&#"#"&3326tҪºR`\VҪºR`\VX%Zh۷ZhFlO'ztF'32654 !"/.#"3"54!2!rz|K٬42 swUҤ'4X˧|`í~pX˧|`J3~F'z<F'763 #52654&#"# '4!"326(24׬'Uvr!24֭٣K|zsp~ȕ`|Xp~8=`|F'z<&F&'F&O'FO&'FU''FU&'FU&'FU&'>72#52654&#"#"&'463"326[*'sobI=J>",BR\*$jt_UV) '2654&"#"'&54632! 33265,B:d:B0<~JIjˮîB,">>",BVU_tjN*$u) '"2654&'632#"&5! #4&#",B:d:B0<~JIj!!ˮîUB,">>",BVU_tj$*\) '"2654&74&#"#! #"&547632(B:d:BB®!!jIJ~<UB,">>",Bu$*Njt_UV)O'zt)O'ztS^$264&"&546; )5! '&Vhf# fw_:@ 91@ B /90KSXY%4$32#4&#"!7g#ʲfhXdfF.=@ 1@ B 90KSXY#"$533265!>ʲf"fw_?@  91@ B 90KSXY '!32653#"$5g"ffd餗 K'  '  O' ;' ;O'  '   O'  ( (2654&""&546323326=3#"&=bFntnPX/Q,CEmaZT:KMMKFHn|ppX;oBGj9$ 3>2654&"!&546323326=3#"&=!"&54632!2654&"bFntnP?+/Q,CEmaʔ/bFntnPZT:KMMKFH;XppX;oBGj9|ppX;T:KMMKFHFY<@   91B /0KSX@ Y!"3"$54$3!7YꏙbXhUFY'z<w8  91B /0KSX@ Y!26544#!wb gXw'z\<FY:@  91B /0KSX@ Y'!"$54$3"3!YhbƙXiU𥒥FY'zi<\'%!"&5463"3!\=.̞RiPYB~}nDZHCw%#535!53!3##q=ԭ-!%#5#53!3!3=~0Ԥ!O'ztw533#!#5!5#5q=-ЭԤwO'zt!3#!#!#5353=ԭ0~!O'zVt 33#!#!5#53m unfy~n ,@  221@  /990%2654&#"672#"'"#3z_̀ٷ{O{ʃIH+'sZ@  21  /0# !3! !5aPh//+jiN !!!5!;VnVN#5!5!5!53!!75$i2$i*mւVxnVnՆu!s #'#37 ͉sH+'Y &s & O& 7&  7O&  &  O& !!!!#!YX  !!###!YX  !!#####!YX    H!!#######! \YX     !!#########! YX     !3!!  !333!!&  !33333!!e    G!3333333!!     !333333333!!      !3!!#!?r !333!!###!?r   !33333!!#####!?r      Y#!3333333!!#######!?r        +!333333333!!#########!?r         SC !3!!#!YX\\SC!333!!###!XX\\\\SC!33333!!#####!\X\\\\\\S FC#!3333333!!#######!ZX\\\\\\\\S C+!333333333!!#########!YX\\\\\\\\\\!33!!# #!՚rՙr %!3!!#!!2^DD^ Wc !!!5!5!!!wsX #5!! !!'!%'! !7%!77'7!  ww u||||||||||||u  G7+/37;?CGKO!5#535#535#53533533533533#3#3#!!#3%#3%#3#3%#3%#3#3%#3%#3??????𨨨!!!!aOq:#[!' 7#}CrarCrrD:[! !rarC}rbar=` !#!#3!ff`G [`3!!!!!!!! j /t`Ӕ&{o{4=J%#"'&=!.#"5>32>32#!3267#"'&32767%2654'&#"JԄ℄N ̷hddddj||MI؏ii~ST`Te__ZjkSR\]i߬A@o\]Z^Z5*,=>` #% 54)3#4+327#!5#53!2x9||ԙf_ڪrĐq{Fg`32654&#%! )s7F0Ǔ$g` ! )#53!#32654&+7F0ɖzٍ`` !!!!!! /`Ӕ|1#"&'5327654'&+5327654'&#"567632p<54& #.54! ì++f++$$>:#tNPƳPNM]*U3MY + 3267>54&#"'>3 '# 5467'7*(Ou))Hn.Mw834OMx43N)gA\*g>}66]C_56`?`q{&/=5!&'&#"5>3267632#"'&'#"'&732767276'&#"qN ffjbdjQGhi񈉉ijBN℄RR\]VVUVVVZdc44*,nmn67윜78lkpĘZYWWsttstuq/u{ 4&#"#32/8qu/ 32653#"4/8`!264&#%!2#!#N[cc[H^^>2`!.54763!##"#676#";jpkla;;?î545w?@@?w iQP%$q2^66**TS++2`!&'&'3;3!"'&546#"37545â?;;a|lkp w?@@?wS66^2q$%PQicQ++ST**<m``$ 653 &53sXٹ};ML+%!5!2654&#!5!#TZ`fcL||BtN5353!5!2654&#!5!#Z`fcxzʤ||Dv/{&#!5!2654&#!5!27654'&#!5!#|vz{\MN`_`gb>> E__ru99wSS?yzVU=`YV5`ZX`]x`73264&+5%5!2 'Ӏ{n Fo}ɽBdd>Jm7{3!!I{/=`N`#!#`I``JZ^`367653#5&'&3U9VˆmmV9S`1Ms,}},uMLs` h !3#'!#ZgVXVq`!!!!!5!#!.AeW"___DXI &327654'&#327654'&#%!2#!g1221g̼^-..-^EOO)(N^h+&&MO%%X@? ]65dL.- rUpz 327654'&#%! )[ZZ[vNONN]eefe !!!!!!R-@___S !5!!5!5!5@-_/__H~$5#5!#"'&547632&'&#"326NJYXe|}}|\SRFFPOWWVVWCj]/rssr'y5UVVUL 3!3#!#΀2Wr3# 3+53265A@1(TFDE`Tli 33 ##-<azBm3!!_ 33###|{9="G 33##|_{EEG ##3G|_{EDEH"327654'&$  '&RQQRQQQQwvvwtww[\\[[\\[\vvvvuvG>@"327654'&327654'&'52#"&54767&'&54763sCDDCstDCCBR65<%j<=0ER^X65`l<=ca==ll*6RI)++LK,++,KL++5##,&)$%LY+8:6iG2278PyAAyP87'21I.* 32764'&#%!2+#Y0110YQQQQ))))]?@@?[ #'&'&+#!232654&#=)&''y.,,LPO)*s\^^\$ )(GTD<32#"&'#3t4554455$pMPPPPMp$uuc@AA@@AA86Z[[Z68^gG3#5#"'&76322764'&"Jtt%78NPQQPN874555555S^8Z[([Z@AA@@AAG#!32767#"'&547632&'&#"@AsC?>>>BADbc^]SSt44Va:: 2j88a WW[ZQRmT3210YGMK SX@ 2KSKQZKT[X888Y1@   /0Y5!.#"5>32#"&73267GsC}>?CŻthVau2koamTebXTb2&'&547632.#";#"32767#"&5476G&%HG{065>=f,K,,+*Ib]W-155_;65-9553+,$$4O,, ^$'U13 `fa<))R`1#"'&'532654'&+5327654'&#"5>32FLHG{065>=23-KX+*Ib]V.156_:65-9j2RQ,+ H4O-+]4$'U 12  `33a<))G 14'&#"327#"'&'53276=#"'&763253J44^]4444]^4PP=7633223r99$88NOPPON88$tm=>>==>>FNO e 45k37XX"XX7_z3#53ztttu 33 ##uuZu2u{"4@ $ #32>32#4&#"tHKYhuu'oMLl+yRowtHJZiw[Wk\sa97EBEB~wZXku4@ zx66X6VYYk\sa8BDG 6@ KSKQZKT[X 88Y1@ /0"32654&'2#"&546]ml^]ll]ǁqqpoWGu 67632#"'&'532764'&#"G0336^_]^:5311213p?>>?p3121 XXYY _ ?@@? G4'&"#46320T6667zWVoBAA@qWWG27653#"'&506667zVWoBAA@qWWu#3>32#"&$4'&"27uu$pMPPPPMpf4554455b_86Z[[Z6@AA@@AA#3#;#"'&5#5350Hww33UUPM,V-,vTPn3327653#5#"&nt''N^67tt+78Jy~{Y,-65\c`9nA!5!27654'&#!5!#Ue22<KLg#"FS10gg%dAl88u{(#"&53327653327653#5#"&Q+<=Rnxu$$IZ54t$$KY45tt(78LMlE!"z[+,64\c[+,66Zcb;F&33#&{{y #! !&'3254554#"t nυ9F}攥^ؙ83a _{3#5&+532{t<,||GXG+&#" '&54767&54!232654'&'&yAJZVWVWW!/bL+"766^]l9=P(r(B4?KWXXWr]$,O'(@?Ajp69G  )"27654'&'2##5"'&5476734 )=;67-!XQVVQs~SVV@h)%661FQ:5}t?3XJOZUUXR=\ ,Ajq@:%'#&+53;'&^sa,(^ra,GX]:DFYzg duudnsd&sdyodsdy67632#"&'#44&#"326&_%sNo%ti\[jj[\i92ض78"{qqrG xd%tdV{(!2.#">32#"&'#32654&#"aQQR9||9F,*[cbbc#Lct`5!#3#3!53#53t𰰰त TV/%+53276'7#3/F0j&*06G#367632#"'&$4'&"27tt%87NPQQPN78f5455554_s^8Z[[ZA@@AA@@Gu&'&#"32767#"&54632u1122q>??>q22110h;533` @??@ _ GKv+325&#"47&'&54632&'&#"632#"Z%0\R@5`$^4412/412q>??5{3 * &;/Z ` ?@@biG.&'&#"32654'&7#"&54632''7'37 i:;n\[nO$$ZY drP =67Tb1#"'&'5327654'&+532654'&#"5>32N+,QR2658-56:_651.V]aIV-+K-32==l/|GHL ))unn77wU:8P#P,i/0\+53276=#533343r,Brrtn x66XU P#PG ,5#"3276#"'&'53276=#"'&54763J]4444]^44tPP=7633223r99$88NOPPO>==>>=۠NO e 45k37XXXXn3327653##"&nt''N^67tt+87Jy~{Y,-65\cO9I 5333##53#Irtggttt\\jz~ ;#"&5C,rfpUWlwI 5!#3!53IMjjo\\E\\I5!#3#3!535#535IMjjjjooo\\\\\\V`3#"54;33#'#"3276ztteztry "3rKNB ,|ssW?#5$ z~3;#"&5ztC,rfSVXlx[`+53276'7#3`34r,Bttax66XS gq3!!q_u{467632+53265&7454&#"#4'&#"#367632+=32#4'&#"43r,B0t*pJz>?t'(N^66x66X6V~a88BDwY,-56\uU 4'&#"#367632;#"'&5P''N^66uu)89Jy?>0B,r34Y,-56\sa8BDzV6X66xq 33##q-{{~G 2#"'&5476"!&'!3276WVVWUWWU6//1w &6^]6&WWWXXWWWW@9\[8E-AA.G&.#5!#3!535&'&5476767654'&OpFVVFp^nCWWCnt6%66%4#76$\\FWWG\\FWWE[*,ApoA-9*A@+Fa:.#"#"/;#"'&=32654'&/.547632;1j8W*,]({44MN9> 0Br34@?>=RX l)k`GF@rb/$+*MW33 V6X66x"j2-*TIX00476;#"+5326z73zno>43r,B0]Me30U:Jx66X6#3#;+5326=#"'&5#5350Hw43r,B033UUPM,ax66X6V -,vTP^!533!33##5#"&=)3276^ntgtuut+87Jy~''N^61\\`9Y,-6/G&5!327654'&'5!# '&54767GE()78Z[78*,?G$"ZYYZ!"J\{':?KY7667YR8>#{\8?>LRRQRR<=:u2653#"'&53QHuDEEDuHPZs{>??>{}ZPz3+"&53?27654'&'&gH#"YZ,rftA Z87)2:08?>LRRlwpU67YQ8C&# #3{{ s7n !!!5!G'L\^=R^7!!#;#"&=!5!G'LC,rf>\^=R VXlx ^7^n#47#5!5!3632#'3254#|`\'Ln& m,7!!^R^=jR37!2#"'&'5327654'&+5!5!hCQ>63``;??C5~Ex>?::hn\& =;M|CD m**PJ*)]R^G !32767&'&"2#"&76So/6^]6/ +66,ǗWVVWVV*MWXMmGYXFovw^wwwv[f!5!73[f3!Px[f#'!5f[f!!#PU騋fBf 3#'#35fxBf 73#'#˴fxh'${-{'TDN'zs%N'>E&%&E&%&Esu'l'sLvquf&vCO'zt'qbN'>G''qZ'zG&'qZ&GOw&'z[quZ&Gz'&'qZ'^&GZ&(q^'HZ&(q^&HK&(7qK{&H7v&(qv{&Hum'yu&(zquH&H'zK#O'zvt)/P&I @s&*2"qVZ&JI;N'zs+dN'>K;'+d'K;P&+j@dN'>Kt;&+ztd&Kz9;&+ 9d&Kv&,Jvg'LYZ&,tF&ajl'sv.l'sZvNj&.&Nj&. &Nvj'/''O jk'*u'/S1'q(;j&/J'Oj'&/\'&Ol'ssv0f&PvO'zwt0'FP't0{'P3N'zs1d'Q3'1d{'Q3&1d{&Q3'&1d{'&QsZ&2fqu &RsV&2lqu&R'jotrsZ&2jqu^&RsZ&2hqu^'Rl'sv3Vf&Sv2O'zt3V'STN'zs5J&UT'}5J{' UT1'q}; "J&q #T&5TJ{&UO'zt6o&%V'6o{'Vm'sv'z6of&V&VvW&6o&#"O'zt *o& +*O'zrt77N&W#>'q77'W&7b7&W'r&77''&W)'8X`'{Xv)&8vX`&XK)&87KX`&Xu7)Z&.8X&+v)4&28X'Xh}&9F=7&Ymh&9=`&Y^Dr'u|:V5k'C ZDr's|:V5m'vZDN'j>:V5'jEZDN'zs:V5&ZGD&:V5`&ZJ=;O'zs;;y&[g=;N&;j>;y&[jfO'zps<=V&\f\m'vu=Xf&]\&=X`&]1\&=X`&]d&KfN&Wj->V5&ZB=V&\{a&D/P&A@7&#"#4>32"#"'532654.546m@f_@&9dc07CjjCӴmob)F[dd[F)Z@hoϋ\(Ž}_-C-->T\_EFvX5P3) $2BgCquHh'${-{'!Dh&$u{-{&DTh:&${'Dh:&${-&Dh[&${'Dhu&${-'Dhm&{-f&"hZ&${-'DhZ&${-'Dh&${-5'DhY&${-&Dh&{-&3&(q{&H&(uq{&H^'tu(q7'H:&(q'H:&(q'H[&(q&Hu&(q'Hm&qf'& Z&,#uD|& &,.&Ls&2'qu{&Rss&2'uqu{&R}s:&2lq'Rs:&2jqu'Rs[&2jq'Rsu&2equ'Rsm&'quf's& sgk's'ubvf&vscgk'u'ubvf&Cscg&b'uv{&c}g^'t'ubv7&scg&b'v&cs)&8X`&X{)&8uX{&X}_k'suqif&v{r_k'uuqif&C{r_&qui{&r}_^'tuqi7'r_&qi&r{r&<ur|=Vk&\C!'v<=V`'t\&<r|=V&\`^'tru<=V7&w\qa&E ppqa&E Hqf&E }qf&E qf&E ~qf&E qm&E vqm&E Dha&& p#ha&& f'& }|f'& f'& ~SXf'& om&&1 Qm&&x Na&I pDa&I 9f&I } f&I %f&I ~Of&I R-a'* p-a'* 7f'* }|If'* f'*" ~Sf'*^ oVda&K pVda&K Vdf&K }Vdf&K pVdf&K ~Vdf&K Vdm&K Vdm&K a', pa', f', }|f', nf',3 ~Sf',d om',t Qm', Nna&M pna&M f&M }'f&M <f&M ~Qf&M =nm&M nm&M Aa'. p5a'. Kf'. }|Kf'. f'.4 ~Sf'.p o"m'. Q)m'. Nqua&S pxqua&S nquf&S }equf&S Tquf&S ~quf&S a&4# pVa&4} Of'4v }|Yf'4 f'46 ~SPf'4w o*a&Y p=*a&Y *f&Y }'*f&Y !*f&Y ~`*f&Y W*m&Y 8*m&Y Ia'9b f'9 f'96 o3m'9L N'a&] p^'a&] T'f&] }Y'f&] ^'f&] ~'f&] 'm&] c'm&] ^a&=N pqa'= if'= }|uf'= Cf'=t ~Syf'= om'=B QPm'= Nqf&E tqf@f&I TfAVdf&K VdfBnf&M fCquf&S {quf`*f&Y 0*fa'f&] M'fbqVa& HqVa& HqVf& HqVf& HqVf& HqVf& HqVm& HqVm& HVha&  oVha&  oVf&  oFVf&  oFVf&  ohVXf&  oVm&  oVm&  o2Vda& 8Vda& 8Vdf& 8Vdf& 8Vdf& 8Vdf& 8Vdm& 8Vdm& 8Va&  oVa&  oVf&  oVf&  oVnf&  o#Vf&  oTVm&  odVm&  oV'a& YV'a& YV'f& YV'f& YV'f& YV'f& YV'm& YV'm& YVa&  o\Vqa&  oVif&  oVuf&  oVCf&  oVyf& ! oVm& " oPVPm& # oqH&Ezq&EqyqVf& $HqVy&EHqVf&@Hq7&E qnqV7& gHhm&&yuh1&&q;f&&B RhfVh&& oxa pVxaH <ܲ?]1 Դ?_]KPXY̲?]90IIPX@@88Y#55#53xgJ7FJm'tjVdf& (8Vd{&K8Vdf&B8Vd7&K qVd7& v8f'*b Ruff',n Rf V;&, of' p  f' p. BJm't pnH&M$n&Mqn&M .%x7&M q.zm&M r0gm&.y.uY1&.q.;f'.q R}f!~f'  f'  _Jm't *H&Y'*&Yq$*&Y *DVa&U pVa&U *7&Y q'*m&Y rm&9yvu1&9q;f'9 Rf#5a'6 F)&j lFRfCV'f& 0YV'`&]YV'f&bY'7&] qOV'7& Yf'4; Rf"f'=D Rf$NV&= osRfvxaH ܲ?]<1 Դ?_]KPXY̲?]90IIPX@@88Y53#7"͔gd10!!dd dy/10!!dOydy/10!!d8ydy/10!!d8yy/10!!y&__J&BBB@ 10#53ӤR?@ 103#ӤR՘?@ 10%3#ӤR@#5R՘?m '@   1<20#53#53ӤRӤR??m '@   1<203#%3#ӤRӤRլ@@m '@    1<20%3#%3#ӤRfӤR@@m #5!#5RmRխ??9; '@  YW Y <<1<203!!#!5!oo\]9;>@   Y W Y <<2<<2122220%!#!5!!5!3!!!oooo\\3!   \ 104632#"&3~|}}||}3q31/073#k1/<20%3#%3#V #@   1/<<220%3#%3#%3#ki3#iq L #'3?K@D$%&%&'$'B@ .(F4 :&$L%IC'1+C =  1 =I 7+ ! L9912<<2220KSXY"KTK T[K T[K T[K T[KT[XL@LL878Y"32654&'2#"&5462#"&546!3#"32654&2#"&546"32654&WddWUccUt%ZVcbWWcdWccWUccܻۻۻۼܻۻ q r "-7;EP\"32654&'2#"&546"32654&'2#"&546  &54%3#"26542#"&546"32654& WddWUccUyWddWUccU<¹ߠZucbcNWccWUccۻۻۻۼ5ۻ(`3(`u(`&  ,(`' ,&  X(`#3W`u(`&  ,(`& ' X , #'#Rs#G@%Bon29190KSXY" 5s-+#R#I@&Bop<9190KSXY"5 +-#^R^  &K'N''=NO'^O$#5>323#7>54'&L Za^gHZX/'-93A% #C98ŸLVV/5<4BR-5^1Y7| B_ % ij991@  <202$7#"$'56:<hh~vvuw~ign % ij991@  <202&$#"56$6;>nvv~hhgi~wuI3 # #bbc$$v=' {' { 3_!!V_+@B10KSXY"3#-\X 3!!#3hX^#"#JX 53#5!!53X^JݏޏJ&""gJ&"JJ'^"d] 7 91@ B  <20KSXY327# 'du](; 2###׎辸( 3+"&5463yv}~}|( ';2+v~}O|}=k {B# #5#5R#۬@n&  =o'  BC''Hd1#"'&'&'&#"5>32326撔 錄ܔ撰 錂1OD;>MSOE<>L~ 8| #'7!5!'737!!qaqqaq)`rrbqr2 535353,(`$' ,& '  XfN 53!535353fXp fN 5353535353,p  3#3#'d 3#%3#3#3#dipD %53535353#!5!3!,|f  feP> 3#3#3#>w 3#3#3#3#W "27654/2#"&5462332233VVVVVVVz@ <<1@03#3#zttttg? @   ] <291<290KTKT[KT[KT[K T[K T[X@878YKTKT[X@878Y@T /9IFYi       "5GK S[ e]] !33##5!55bf]myf !!67632#"&'53264&#"y^^a`<~B9>>Eoo4h6_ MLKJq ff\/"327654'&&'&#"67632#"&547632X3333XW33331221DD &9:DTTXWll122m45[Z4554Z[54bg KL1LMONuv l!#!liH30Y *:"32764'%&'&546 #"'&54767327654'&#"55j]\655T./RQ./SZ85UVUV56-/.UQ100/SS0/*,+KLV,++]12Hdt::dJ01:7PyAAAAyN98?&%%$A?&%%$S.532767#"&547632#"'&2654'&#"1220DC #<9EWXWXkl122Xf33XU5443g KK/MNoouv rh\Z4554Z\44k !!#!5!Q_i_k_8_83!!'3_a!!!!''^_o #&'&4767TRRTe^///._~g3#676'&ge_/../_eT)**)~~~u0@ 32tNN^luu)qJy}wYYk\sa88WT dC{d^TtdbTud?C dfC d\T dlC dYT dST d d8 d  doif dgif dMrdGxdGdu!sdGydV##"32.#"3267!!!!!!Oc%eNLbbL:/667756GFDFG ks9'.473&'3267#"'#7&'#7&'&76%73&'hA>/(%:@w]ayA9&AX}R4>C5Ai<)^_HH?WghйKp(`,%6767# !2.#"3>32.#".aXj]aye6{_]w|^0n&<$'/_HGghGG_^ٜu]\Y!!!!3###5qZpP~WHE9Eb#!!53#535#535632.#"!!!5-쿿=OL=tyB_))HB+#&'&#"#3676323632#4&#"#̪m49wSS>YXyzU6%X\xruxGM_a`f21>&>E3\u"&)''#!333#3#!###535#53355KO8~8~OO4&{{&&{{{ P32654&#+#!233!!;532654&/.54632.#"#"&'5#"&5qzzWQeGl`[z_<`HJU];Ufɘ/ϒjqqR>N#55YQKP%$((TT@I!*##`3E326&##.+#! 32654&/.54632.#"#"'&ٿJx}A{>[b`cae@fLNZb?ĥZa,/b؍$~3YQKP%$((TT@I!*;"&)-1'#53'3!73!733#3#####5!73'!!7]:1000019]zu }Luuguuguuuu_ % #4&#!#)"33!3_SV*$oN&1@: "+ /) 2+"!)#&  , & &*!/<29999999999122<20K TK T[K T[KT[KT[KT[X222@878Y@z  1Ti lnooooiko o!o"o#n$l%i'i-  !"#$%&'()*+,-2   USjg ]].#"!!!!3267#"#734&5465#7332[f A78 ʝf[Y`(77(6bbiZȻ{.# .{ZiHH"{/ #/{"G(33!!###5uX_Tws1s!5!!77#'%5'&PPM4Mo؈onوn9 -bw'67>32#"'&'"326767654'&'&67'>7632#"'.'&/#"'&54632326767654'&'&&#"32">1aJ{%A01Q[W7>/W1   >$<  . #dCw-^URB$`>DL_K>.3b @N\uLMiI(S395l9,8G(/&  -9)ЗiRm:3Xwdg7? 2j7#=5(6$ 629T/ (2M !:5S}$@{mbq~Es/4 -& "TAB`]|@8nRkcd]aC".)5'632327&547632#527654'#"'&#"%654'&#"o|@X"07PYtaTk~j[IwmqJ2530D#24!`NkBX``S㫣†qJ323!!!3267# $547#5\J5 ;_srigCS1r{jJ,{ +kv67&&UB{\* {;^~FE/0K?{w!,&'&#2767#&'&576753w[TUeeUT[Y\Y[dsye]Y\[CvlCi----iH$"u9Bt"#BuflC3!~d=!5!'3 G~d=z!#'73!5~~͛=z5!'3#7=~~d͛F 3#%3#%3#yfPF 3#%3#%3#%3#ky)=z #'73!'3#7~~<~~͛͛C $(B"326=7#5#"&54634&#"5>32%3#.#"3267#"&54632pSHfmƩogDc\GD^o8yy8o^IICBRCI M >OW\ 7$44"C +EI.46'&#"#&'53254&'"326=7#5#"&54634&#"5>32%3#VNz$p;i0ʪ%={pSHfmƩogDc\GD}|49d$, !5Lf,1BRCI M >OW\ 7$s'!.#"3267# !2'Y藣yyYjzS #bvAZ4-4ZBuHHghG[!!m&r&F+,/-/ܸܸ,(и(/A&6FVfv ]A] и ии# /!"+!0153&'&'6767!!5&'&76wI3cc3I86QLNN7887NNMR48_ki:rq;zn #++$ * rn<(2.#"3267#"&54632%3#"326&$  &54^o8yy8o^IICDkavva`ww~44"K <M-1332653#5#"&.#"3267#"&54632%3#\QPcu`^o8yy8o^IICDLriuD P44"K{Ro#&&r)Io!6767632#"'&#"32767#"'&'&547!#"'&54632327676"#"'&'&54767632l(9BKc{=&%%03!((!,739%7`lG;7 25]hB4,'5  'B[QF$%]c'G  %! }Kr~,1ьIg)*!&!(D;w},75;!_']7:y}[Ϟ\@4>#,!, 'QFj(JG4$$,*)/9yK#%P73276767654'&'&#"&'&"'632654'&'&54767767#"'&'672#"*i(X%# 1FSE/ O.55FuPU[QF[00rl~"KI}!;IFs;n;_T^͌Q79}w^l.Gyr\[4O9%#i#^MX;yv@c}e.ID\7I;>2V秉uӰ3!3%!!!!!!nnq  dx+%H#>54&#"#3>32u j_ y/wFx \/HT^Ȧ^m$RZ3%632##"#'7-P4-> {|a\=BcL;t9#"'&5476323276765"#"'&54767632thn<7# ;KQ>!|Za,4(XM!},‚<7D9#7.M=.1?@ '(MXI(' jF!2?632327654'&54?#"'&#"632327#"&#"jou9!ydG>PPPP5ʺ68^nm{z}}ȋo֏zZ'PVaK~pmdykb^OP681/::b:DnJ327654'7#"'&'$#5"'47676766767632#"'&'&'&#"32nZS_n0VBRny#HB?X!$9BMw>7l. ;7%,;(ӧuy,D0&3273#"'#67&5477632654#0)W:K32#"&'####53&  O:{{:ܧ$}daad}j %# !3!# dX0dd q+6+/BB/,/<-ݰ.<-ް#? < # 9 FhH)##Ii;BB=#IbiF`FaC`#BC`CUXC`C8Y& <BB00<İ< 6< <9 FhH #Ih; < ְ ݰ,9, FhH &ְ& #Ii;/,#Ih:1#IC`#BC`CPX& ,/C`C8K RX #IC`#BC`C@PXC`C@aC`#B C`C8YYYBB=#IbiF`FaC`#BC`CUXC`C8Y#)<BB1#IRX   <  < Y3525!463"!4632#"&732654&#"5!6jgggg92299229k̀k@4nNggNNggD{{ "-! ! ! ! '32654&#%!2+# JR12)uyӲckkc?L00ey wXQPXdn;C0<67632#"'67327654'&#"#"'&57&547276545[ۄFIyeL )qz]E& JEYq:?.蔁0.A ƂMkeLPק<+(h|H=y|n=B {u.F/4_NT 33!27&#%!2+!67654'&,d.@nX<-]\,q jdZ)VV)s!)%#'# ! % 7& 676'&B 3y;:x+lllli$ #ab[ 2222jT%%5$c$B2 _327654'&'&'#"'&5476323276765""'&5476!6?232767#"'&B=]iS\ZV30Fn7;#FfS9!!< #5,h";<2XngZR{,##9>;K!QIag£S D5@7*'S:y}*7H0 5#!,Il @3Xnh0{(2r:=OSlIX&54'&#"#"'&527654'&#"3"'&547632763227767654'&#"R(O*\xggfg-.@@?@@?\QA@@@S6fggfeӻp/$~AB}:1$ -*MJJ@f[+8vuuv zVWWWXWWVVW\uvuuu# bW1W{|^1$h{vC[SK\GChfy /2 &.2&'&+3!.+!! !27&#676'&%3LDEx-Me5q>HJxnu1EA+ZY*01/O~hbb)j)V>U)-  /!/ и/ ܸи!ܸA]A)9IYiy ] и /9 ///+ +0132654&#+#!273 # #s sNCI/ϒ_6۬kk%T$+.3&##&'&''7#!27%7 67654#?\A>:AٿKE6ToF^~_ ,8~|T3Jۏ/HDh0& ,ok؍]-Dbg('4.#"#"&'532654&/.54632733###UW'AG/E8pi4sG[d/EK7?8pc|3iиY"*/( VAO[`*,2,* M=H\T(l0`!!#!!!!!!!3!!rso+` `ffff'F >@!    b b cbc91<<2<<903#######5Jq7rqr/B^^"h %73# ' 3,o-MoF+,\ %#!!!5!8kO8d qddd XL/ 654&#!5!5!5!!2!"'X $''ߦԧc̆eeaԊfJN=NsDU767654'&#"#"'&5733272632632!"'4'&'&#"'6763232767654'&'&#"_}yj#1Q\$####,TGG\n#?QY>kDM4giMqE#"'&'&5476?&'&547632#"'&547654'&#"3"32767'_ilE_ml=Oc{T3-2") %+fa@aP/Z_|{w:maZu> IhA"%@_l$=PczS2VN-2!$+%$+@e}N069na[u>_T M#"'&'!#!"'&547632327676=!7!&#"#"'&5476!27327#X':'7?<=**M_4. B^l{>!'Ba>nG#&#w4$B00!K=DcK_4B( 03B{>ceDInFT=I,Fw7K. 0# )5!!5!3#Pʪ9Bk32767"'&'&47'&'&'#"'&547632326765&#"6767632377632#"'&'&'&#",5(.'*'E`97y{7a;f7;>F3.^PeMD*#7@,j!HhH<=.%_yipp3 T}B',$ *5܀/,,@!;Da97TVM;nwF^O?/,%!;>jytX<;}f?E'_n H''#  .hJ) 4&#"322#"&54WOmVPm˜ݢt}t{أأg4 4'+5654/&4?'&547 '&5474/c2>Bd=VE/b5c2ltc2c2uc1LS2?Bd,>8?]/c6c1LS2tc1LS2c1LS2903#!".54?>3!4'.#!".54>323!2O,""$%@;5H *Y[#$"x2 1[G(  WA,!2#"&/#!"54?>3!!"&5462TPl 0%= -d,mF"$mG- .7#*(/ $"Sae(!q~B;V&!"&54>323!2#"&'&5 mG * 5G 0%9 . q~( 0 (/ &Js!S'DQIF 4632#"&3!53#5!pQOooOQpoTQooQOonuyy5yZR; ! ! ! ! HH#[[breH !#y;:x L`  !!!!#!3#'!#33 # #DjwZDZ֏R``C5MR.}$z`-1%5"'&'&5#2327#"'&5#!#"#463!#3#, 9Yl(Ht*=Z2dr!Z4@'!8 ֦zEB bLs{dYsZ{3#"#4763 3׮UEEl4FũdGQnCF\xB*WbOZ=0 3%!!,:*nq dd3!3!!!! nn8q  qwS ! ! !!5 5Y*dccS!!6$3 !"$'53 !"kJu^uopkoSUggHF_`2/.2%!#!5!)+!5!_++!# #3bef9WJ " )327&#!3676654'&|tK"P"coAfյ|cv~dAA xPfUmZ #2!7#"547632!3 32767654'&#"* 6B8wx!Nbb|˞"#>|OO'vN 2wx87tKsO=  =d01 PD10d^dTd6Jthi[{ (232767# '&5477632!7!654'&#" N&#G_yZ\klmk}Z5fF 9NJC0<7h:J(u*oDMcFPZd82vRsO 3#3#!!ɸ.Ԇ$N9`V 3##676#732767!ɸ.fʆ#5H2K1i0/N)deеT0Hd01``;&0 #473>32#"&'532654&7>54&#";Ht]h202޸SUWDi;2[UԠ_I@Yr~YW׀c?}<$$/1oX3gQX?@Q` $@   F 21@/0!5!!5!`oX&{' 5ud^X&t' 5ud^&{' 5 d^^&t' 5 db^&u' 5 d?^& ' 5 d~&{' 5 df~& ' 5 dw&{' 5 dbw&u' 5 dfw& ' 5 dlw& ' 5 d&{ 5,'&,,&,',,(Q&,9h9&9,,&9',, &9',',,-&,;=;;=&;,=B&;',,j/s'&'0yL&LLpY&L'LpLA&LY=`Y=&YLD=-&Y'LDL=&Y'LD'LL$J&L[;y`[;&[L[;D&['L[LyOq{FqZG{Py }  ) !3 !## !5hPPh55~ji.,w# + ++ A]A)9IYiy ] A]A)9IYiy ]%"+++ + 013 !#3 #32654&#! )5HHNhPaY.,职~y }(1C3 +3 !32654&+! ) #"35# !35#"&546!`HH5NNPhthNN5H/ó., ji~s'H{d?8   2@ @@ 00 ]1@   990@   <<@ <<KSX << Y5!!dx=xUZxx @   991  2@ OO ?? ]0@   <<@ <<KSX << Y3'#'-Zxxvx<xuP8   2@ OO __ ]1@  990@   <<@ <<KSX << Y'7!5!'7Pwx=xZwxx @  991  2@ @@ PP ]0@   <<@ <<KSX << Y#737Zvxxx76767632&'&'&#"#"'&/#7!#/)85,0F"<;NJX[GR7<"#!2)85,/$#?2WG[XJN;?,!F0O<:" %7xxUZxaxxaxuP8 '7!' 7!'7Pwxx>xaxUwxx>>xxwd?8 !5!3#xwx-xZxY %'3'!!5xZxZxvx檪uP8 22@ O O _ _ ]1@   990@   <<@ <<KSX  <<  Y!#3!'7'8窪xwx-\xwZwx !5!!7#7\xxZxx+xvx7!!5!7'3'xxxxxZxxvxxvxd>%52#!5! 767>54&'&'&>42/+-+-':1 Hxwxܪ-)o=  xwZwx(.46<=69)-d>>3276767654'&'&'&"5476767632+#5!5 6 +/24>A1:'-+/24>xwx  =69)-(.46=<69)-xZxvP>54'&'&'&"3)'7'7!#5#"'&'&'&5476767632# 6 +lxwx>42/+-':1A>42/+ׂ  xwZwx-)96<=64.(-)96=dP8X#532267676767632267676;'7'7#""'&'&'&'&'&""'&'&'& xwx 0$#$   "%'-0$' !  ' '- xwx  ('Z&("  "(&Z'( -xZx$ -#%"&* 'xwZwx ""&*  *&"" dPF%'!5!!'7'7!pxwxpdxwx^:5xZxo:xwZwx* %'7 !^ b9YXxbZ  #!5 xwxoxZx[ !'7'7!#xwxxwZwxZ  !5!3 ixwxDxZx[ 3!'7'7xwxDxwZwx 7#7!5xwZwx=xwxd? !5!3?=xwx-xZx,-eX&7#754767676 #4&'&'&"9xxZvx.-\ZnllnZ\-.BB54'&/#7!!#"'&'&'&54767D !BB54'&x\-..0YXplgtTY0../Z#,@#B"!BB@RNJV]xwx]TQ>]xwx]xLii `iiT4]xZx]4]xwZwx]JiiiiuP8!7'!7!5!7!'7'7!'7!5giiyYuI0]xwx]uIiixK]xwZwx]Kxd?8!!5!!]xwx]7Qix]xZx]xi#'3'#'x\xZx^xhP8^xvx^huP87'!5!'7'7!5$iiQ7]xwx]iix]xwZwx]x737#73jhx^xvZxx\x%hh^xvx^8dP8!7'!!5!'7'iili\]xwx]]xwxiii]xZx]]xwZwx7''3'7#7iii]xZx]]xwZwxliii{]xwx]\]xwx  #7!##PU?,UvU,?UP5#'#5!#5'U,?UvU?ԄU4 753!5373U?ԃUPqPU?U 433!'3ɕPU?UqPU?,Ud?8!!!!5!!c$R&xwxxxxZxxuP8!5!'!5!7'!5!Q$܊xwx&RFxxxwZwxxd?8#''''#53777?(FncxwxFn-FnxZxFnuP8577773'7'7#'''unFxwxcnF-nFxwZwxnF3'!!!!#!5!5!5!'-Zx((ت&&xvxTrx#7!5!5!5!3!!!!7Zxx((&&xxrTxd?8 5!!5!35!dxqxUZxxa 3'#'3#3#-ZxxbvxrxVuP8  '7!5!'7%!#'#5PwxqxUwxxw( 737533-vxxvxrxv4k?9 !#3?xvxתx~\xuI9 !'73#'7!uxvxxvvx7?~ 5!! !!  d }*^V 3! !!d}*p  d HP~ !! !!    ^V #!# !!!d e n ^V !! !3 3!!!E*dr*r$| \d^V )3! !3#!5#3 3 ȃ\Pdx @t %#!5#3'!3!3! !33'ȡdxd:tZdd\nt^V%#!3!3! !3!5#3ĹtIt\Px^V%3 3!!! !!3 37r*kd d| ^V %#!5#3 3!3!! !!33 37ȃ:͊` \h u}~ 7!! !5#35! u\Pdx f:bȃ  zM!#7!!#Mc"?,^xc?x^zM35!3!5!73zpc?Jx^cr+a?^xJ^V 3 3# '! !! !  e   dCuP8)5A '7!"'&'&'&'#5367676762!'7$"!&'&'!27676Pwx 21@=:C.2  21@=:C.2 _x_R#)l$h$#R#$Uwx@21.2@@21.2@xw#w;' , utP'7!5!'7!5!'7!5!'7Pwx===xUZwxתתxwZd?D5!3!!#!dx3xUZxmmxuPD '7!#!5!3!'7Pwxͪ3xUwxmmxwdPD3!'7'7!#!5xwxwwxwxmxwZwxmxZxd?D5!333!!###!dx⪪YxUZxmmmmxuPD '7!###!5!333!'7PwxYxUwxmmmmxwdPD333!'7'7!###!5d xwxdxwxmmxwZwxmmxZx7?@  !JBJAu}@ 7'!5! PJBł}BB7}@7'! ! 6BB A}BBh %!3!3۠ՈR+nm+A&6FVfv ]A]+ +0132#&'&#"327673#" B!OO!BzcI7͙7Ic_L 0"'&547632654'&#"563 3276767&#"\m`cu\6% GGnth r5?,/H@3H5,Y:$UeI+HQ\N,tqzSd69->eSY׮l !5!!5!!5>+5!#7#53!5!!5!733!Kcd04+^^``k](673#"'&'#7&'&$32 '&#" 32$767&'&YjiEd80~i?/c`RQQ$g'-"SRR:;nSz_'BTc_ N@DROg`8@91/90@cmpxyvn]] !3!^DC?`%! !3f<?I!!"$54$3!!!W?JGcGK@ sJxNL``ȟMOx]I&/!!!!3!!"''&'&54$;7#"ؖI$$$GA?d`,,cFU;}YI7ʟ 7c``JxH NGx]g% $54$)!!3!+*(FiNv%FrO:0QI&'&'&'!5!2#!5!676767!5?JGcGK@ 'JxNLȟMOx]I&/'7!5!!5!&#!5!2+4'&'&'3276765 I^Q$$GA?d`,,#FT;}YI7ʟ 7c;JxH HNGx]g )5%2767!5&'&!5(*FiNv%FtFgP:1R, //01!!,wq@gg120!#!# }wq@gg1<03!3wJ}w; ]@    91990@0QVPZ spvupz  Z pp{ t  ]]!! !!5 7AJI3!-10!!ת !#!5!3!!5!--+}ת W+и и и / + +и 01!!#!5!3#-Ө-5B<%?P%73% %#'TUUTUTTUDGrXY %=} *@    91903##'%\sB}}`s-Pb;=v& Xus=e& X s 127#"#"'&'&'#"'&547632676;#"3cd3668+MI6641C;ItY^^SI6?+((C;ItK@tkHMfpEF?$Tx5@ejre!93Ex5@#/;&'#"'&54763267632#"'&%27#""327654'&1C;JsY^^TI6?+((C;JsY^^TI666cd3778s~d3778]$Tx5@ejre!93Ex5@ejreMHMfpEFHMfpEFI%!3!~,I%!3IfIA//+к99к901%&'&'3!!#4'!&'7`'JAW`LqR]+X* Pʋs^(Rs57756u5 +  // 9 9 901 7&'7%%'6 676r{EG%y44RW!L!$Ҿ &!L {JP+3#+fJ+ 7+и//9 90137#'PMVo)gnJ+3#3#@+fJ+{//и/ܸи ܸܸ и и// // 9 9 9 9013737##'[P]ME+qd @oxpAn!3# ih^T3 3##"T^32#4&#"#P(*7332653#"RP7*uM>2&#""&'7327~9GA~9G⧅}}uM& i i%uM& i' i% iJuM-6?67632&#"#"'&'7327&'&5476767654'&'SOJMG79GcBnnVsSOJMG79G]InoSu=,EG%,=,HK%DAF7K|oUDAF71IosV/HgjG$4.JhgH$uMMQZc67632&#"!67632&#"#"'&'7327!#"'&'7327&'&54767!!67654'&SOJMG79G~SOJMG79GcBnnVsSOJMG79GSOJMG79G]InoSu~=,HK% =,EG%DAF77DAF7K|oUDAF7$çDAF70IosV!.JhgH$+/HgjG$uMmqu~67632&#"!67632&#"!67632&#"#"'&'7327!#"'&'7327!#"'&'7327&'&54767!)!67654'&SOJMG79G~SOJMG79G~SOJMG79GcBnnVsSOJMG79GSOJMG79GSOJMG79G]InoSu,~=,HK%2=,EG%DAF77DAF77DAF7K|oUDAF7$çDAF7$çDAF70IosV!.JhgH$+/HgjG$uL.3&#"7#'754'&'#"&'7327#4767>32";EY?w^H6H\O3,,HO;E+@/VfmVmHO?u]HH]sM3 gz.VrmV_zuM<%4'>7'7&#"7"&'7327&'&54767>2=,HK%=Q Hl;EYLmHH7'&#"'"&'7327&'&54767>2=,HK%m#6,=iSH;EcHKs;E]InoSuJ.JghH$6B0+@TH?HK|z1IosV32326ian ^Xbian ^V2NE;=LTNE;=K23276767632.#"#"&'gV^ naibX^ nai2UK=;ENTL=;EN1).#"3".54>323265.#72#"&:QHRdhNi\dnx>@HRdhNi\dnx.ttlH=YOHL\}X[lH=YOHL\}W#"'"#322{dfftX{dfftX#*$0!#.5476767654&'30ND:323267#"''cDXbia]yeEVgia`yS LTNE+~F KUNE,F #"/&'&#"5>32326!!ian^Xbian ^VeoNE;=LTNE;=K`#"/&'&#"5>32326!!ian^Xbian^VeOE;=LSNE; =Kkb%&32767#"'!!'7!5!7&#"5>32%H\ iaBP﹉lZXbian3}o -X"OEd8LSNE;I"#"/&'&#"5>32326!!!!ian^Xbian^VeOE;=LSNE;?Kk˪.#"/&'&#"5>32326#5!7!5!7!!!!'ian^Xbian^VLoKɦoOE;=LSNE;?KL˪s˪sB.32767#"'!!!!'7#5!7!5!7'&#"5>327b K`Jqia'+\+zlh>Tm?u2^Xbianc"%]OE˪Nt˪=LSNE;%N;?@.9*-" *19" <-<<219999990#"'&'&'&#"5>32326#"'&'&'&#"5>32326ian ^Xbian ^Vgian ^Xbian ^VoNE;=LTNE;=KڲOE;=LSNE;=K43267#"'3267#"/'&#"5>327&#"5>29+Vgia@LJZVgia}9+Xbia@MHZXbi a KUOE8KUNE; @^ LTNE8LSNE;f@59#"/&'&#"5>32326#"/&'&#"5>32326!!ian^Xbian^Vgiaq^Xbian3VeLOE;=LSNE;?KҲOE;=LSNE;?Ky5P#"/&'&#"5>32326#"/&'&#"5>32326#"/&'&#"5>32326ian^Xbian^Vgian^Xbian^Vgiaq^Xbian3VײOE;=LSNE;?KҲOE;=LSNE;?KҲOE;=LSNE;?K"32?632.#"#"&'!5!5gV^naibX^naiUK?;ENSL=;EOȪ+  %5 % $%5$[g&Y%ZhӦ69%676767!!"'&'&'!5!!5!676762!!&'&'&[C-87VYYW6 8.CC.8d 6WYYV7 e8-,CE[<0[2332[39\DD+N+DD\93[2332[0<[EC,` !5!676762!!&'&'&!![C.8d 6WYYV7 e8-;++DD\93[2332[0<[EC,`'  ' &  ' &  0' &  .62' '  W63& '  ` 3654'!!5!&547!5!!4434w~0IG00GG2?8>;_8` !!!!"264&'2#"&546HdddeH;k'**z{DbFE``bq+((d:svv`K!!!! &!56뗲`!!!! 3# $c'`!!!!33#$'c`!!!!!!'+]^*^]N䰰` !!!!!3!Np!NNf`07GO!!!!#"3###535463!3267#"&546324&#"'53#5#"&4632264&"?$mmC???DNB&H#$J'`qk[Q_C<17HBB@,I\\I,@p`ctiG6B?9i=$#tu#gSSS`*!!!!>32#4&#"#4&#"#3>32!]?U\Z79EPZ7:DPZZV:;S==:xoHOM]QHPL^P%U20=` ,!!!!3#7#546?>54&#"5>324eeb_--B6'Z0/`4\o$-,N2A+,/-7#!^aO&E++ '>@"     <291<2<<990!!!!!'7!5!7!}/H{};fըfӪL !@  <<<<10!!!!!!ת4!5!7!!!!!!'7!5!7!5!DQ"rn遙RoLT˪˪T˪  )@    <<10!!!!!!!!K T@.B $# <2291/90KSXY" 5 !!@po V@/B$ # <<291/90KSXY"55 !5AǪV 3!! 5 !!@poV !!555 !5BkǪ!5!7!5!7!!!!' 5'`ȉ)P"_=6@ss1stFpo!5!7!5!7!!!!'55'`ȉ)P"_=6ss1stF. 5 5:6:6pr pr . 55556:86:'!67&'&54767&'676'&'{)#Y4JJ4Y#))#Y4JJ4Y#)AAAAGF㞢GGGG➣FG2;;;<<;2;5$?$%5%67$'W eĔd?NĔ])]o& bR)`q% Rd%'%5% >zmzF<˶@6 o@hGp%5'75%7-孈m%˶C@ʴ@hGp/V !5!%5%%%!!'/xvH-rf5LOlUrC@=Vlь=/V%'!5!75%7%5!!' GWb[mmNL>ߪwe=ت=$%#"'&'&'&#"5>32326 5jbn ^Xbh`n ^Vg@ND:3232655jbn ^Xbh`n ^VfNF<>LTNF<>L>)P14%&#"5>32%5%%%3267#"'&'&/' k Xbh`'+kuE%sk ^Vhjbn "Pv1-LTND9ATj͊LTNF<= &TN#wf=J;N} 55 58@'poN} 5 55@'pom`!-%5%%%'5%%5 MM`ZDOA@FZDt@m*_TW&o}䎲w&-r~bUm`!7/%5%%'%5%75%Jvad",,V`bL"_D2,/*/&O{¸[&}P %5$r osaa^~||P 55%$so a||^a)W!%5%5$gV$}]]x|)W3%55%$Vg}$BW|]]RW(%#"'&'&'&#"5>32326%5$ian ^Xbian ^Vg$}NE;=LTNE;=K$]]x|RW(%#"'&'&'&#"5>3232655%$ian ^Xbian ^Ve}$NE;=LTNE;=K$|]]&%5$%67%'Et֋$k}uU)?eKtuu" K 9''567$'567&'%=⃹t֋~}uRU)?Kuu,ަK9'_%!"54763!!"3!슊@^`@ƍ^`_75!27654&#!5!2#@`^@Ȋʣ`^; #";3!!!!#"54763^`0rrndflppꊊ^`&pphƍ3 32654'&+ #!5!!5!32#^`0rrpp9^`phƍ7!!!"'&54763!!"3!Ɋ@_`@,ƍ^`7!!5!27654&#!5!2#@`_@Ȋɖ,`^ȋ '!";!!!!'7!5!7&'&54763!7!!ʉ_`'}E=aLT>scL0R^`5ƍ7 '327654'&/!5!7+!!'7!5!7!5!^`__BV 5cTpX?bLm>U`^`C 7 Xȋ5j )5!7!!'!"'&54763!!"3!.Bqx-qxDɊ@_`@Z54&'&'$  &'&'&547676!!#!5!]\LMLLML\]]\LMLLML\bc1111cbbc1111cbdd''LMmjML''''LMjmML'dbcwvwvcbddbcvwvwcbee$7!!"2767>54&'&'$  &'&'&547676r$]\LMLLML\]]\LMLLML\bc1111cbbc1111cbתa''LMmjML''''LMjmML'dbcwvwvcbddbcvwvwcb$3?"2767>54&'&'$  &'&'&547676''7'77]\LMLLML\]]\LMLLML\bc1111cbbc1111cbxyx''LMmjML''''LMjmML'dbcwvwvcbddbcvwvwcbxyx$7 "2767>54&'&'$  &'&'&547676pxg]\LMLLML\]]\LMLLML\bc1111cbbc1111cbpx''LMmjML''''LMjmML'dbcwvwvcbddbcvwvwcb$73#"2767>54&'&'$  &'&'&547676]\LMLLML\]]\LMLLML\bc1111cbbc1111cb''LMmjML''''LMjmML'dbcwvwvcbddbcvwvwcb$ 2L"264&'2#"&54>"2767>54&'&'$  &'&'&547676ZPnnnoO@v+..]\LMLLML\]]\LMLLML\bc1111cbbc1111cbAoPOmmp1.-rB''LMmjML''''LMjmML'dbcwvwvcbddbcvwvwcb$+E %#'-73%"2767>54&'&'$  &'&'&547676C4f4C4/f/]\LMLLML\]]\LMLLML\bc1111cbbc1111cb1XSXYS''LMmjML''''LMjmML'dbcwvwvcbddbcvwvwcb$!;!!!!"2767>54&'&'$  &'&'&547676]\LMLLML\]]\LMLLML\bc1111cbbc1111cbj''LMmjML''''LMjmML'dbcwvwvcbddbcvwvwcb$37"2767>54&'&'$  &'&'&547676!!]\LMLLML\]]\LMLLML\bc1111cbbc1111cb8''LMmjML''''LMjmML'dbcwvwvcbddbcvwvwcb$!%!!!!#!5!QX>ddYee$ !!!%!!rPX>ת\$   ' 7 %!%!!=kyykyjjX>xjyjjyk$$ 3#!%!!aX>J@ <1<033!!upJ!#!5!3JI!#!5IssI35!3!|33!!Nup| !#3!!!!.NN$J !#3!!!!.$J !3!!!#3GupJ !#33!!!#3.GVfupJ!#3#3!!!!.cGGf$J33!!!'!'Ssj\s=u5Y6pJ!!!!'!#3!7!sjshxj56$$J!!'!#3!#3s6s=5Y6puJ!#3!!!!!'!#37!s:jsjG$-56$]*5$%67654&#"'632#"'732654'&'$@e=M>P7sZw㔰Zs7P>M=e.(Y7O0<0:>~jy[<<[yj~>:0<0O7Y]*327#"&5476%$'&54632&#"ee=M>P7sZw㔰Zs7P>M=e@.(Y7O0<0:>~jy[<<[yj~>:0<0O7Y( 51  ^ bb:d 5! 5bd 5! ^bbb:yg62"'&'!"&462!6"264S몧Q3Q3TW4drOOsOOSQ3CB3RU4CDPrOOqyg"&462!6762"'&'!$264&"aS몧Q33TW4QrOOsOSQ3CB3RU4CDPrOOqbgR 7!6762"'&'$&"26b1[륢S4OsPOtO.D/YR3BPQqOOy;d 3#!!#3%!5!( 󀨨 ds <!##5!#T~N 35!3 3#K#"T^ !!3# K@ih^T !!3 3#K@#"쪠T^~ )3!!&'.'&ZVF%,E=Ώ?~%FVZDA?=~ !53*,Ԫ֪w # #}}wJw 3 3!#wJww@ 1@ 0"# #4$H̭9B( w@ 1@ 02$53 3H4CC1 (B9#uTHF103#F1  !!'+]^*^]䰰3#3#!5!7 !! 'RLxxLux66x<ux6xx6x'B  ' ''ٛ>PNq^D^'B %  !'''tNP^D'B 5  5!''6bNP'B5 5tN>]P'B 5 'Nt>P`32?632.#"#"&'!5gV^naibX^naiUK= ;ENSL=;EOȪcy 33#cu?Ik8ff%q#cy 33#cffI?#q% )!"3!!"'&5463!! '&76)!"3!k:((P:jZYk񼽽jȊ ()9:PZXD  ȋ )5!2#!5!2654'&#5!27654'&#!5! !YZj:P((:kɊj XZP:9)(ƍN$!4&"#47632! #4'& PtPZXD|p:PP::ȀZX8x8Ȋ:1$2653#"&5! '&3 765PtPZX1::PP:8ZX:8Ȋ|84'&'##47673#Z:KK:ZllY:::ZaȌlala4###!5!5!5!333!!!!'5#Y~~~~,,33ͨ^ 3# 57Ѧ^ 3#55=d//m.   5 5 5 :6:6:6pr pr pr .  5555556:86::6:.  5 !5! 5?@Npo. 5 5!55?ްop9 %5 5!@op9 7 5 !5!?)W5$%5$Ti}$_|x]])W5$%$5iT$}B!]]|!&!%'&'57&%5$%67&%7*?;i@]0qw^%KA6#(AF+3273267#"'' 5cCXbh`^xnieEVhjb_zl]@LTND*F JVND+Fpo"%&#"5>3273267#"''55cCXbh`^xnieEVhjb_zl[LTND*F JVND+FͰW&&#"5>3273267#"''%5$cDXbia]ymieEVgia`yl]$}. LTNE+F KUNE,F]]x|W&&#"5>3273267#"''55%$cDXbia]ymieEVgia`yl[}$3 LTNE+F KUNE,F|]] 7%'%5 '瞃۞L О  @Y8@\9@a ' 7%͞G۞О@?Y@<9@}5!%57%!!'71|Iv\' :qߦ[@Z8@_}7!!'7#5!7%%%9Jpv\]FGjq8@ǹ@ 3!!"'&5]9Deo>ܚVf]>#3]J] 4'&#!5!29Deo$VfX,&'&3!3#76l<(enP==Kne(!< _EA_ <]> 3#!5!2765oeD9>יfVu(3(7@% !!!5 5!!37d  hrv! !! $<Ff +   276764'&'&">  &vvrn66\]]\6666\]]\65kk\SS]\6666\]]\6666\!;>32#"&'#'%53%&  s:{{:!8#!rܧ$daad]chaam@j.!3!3:^ &ۺ+#+#+A&6FVfv ]A]A]A)9IYiy ]+ + $%+$01! 4$32! 4$#"35%33!??qqW|A?rpG~+/ 8?+3&+3+A&6FVfv ]A]A]A)9IYiy ]3и/A&&]A&)&9&I&Y&i&y&&&&&&& ],9+ + +0)+001! 4$32! 4$#"!!56$7>54&#"5>32??qqWO\R!>/_N;sa=0>A?rpGM"?U(?N&:$}:iF D+B5+B+A&6FVfv ]A]A]A)9IYiy ]A55]A5)595I5Y5i5y5555555 ]5B9,5B9,/A,,]A,),9,I,Y,i,y,,,,,,, ]ܺ&9;9+ + )"+)?8+?2/+2/2901! 4$32! 4$#"#"&'532654&+532654&#"5>32??qqW v@X[}DuskcZX\[4yk_=hA?rpG]0OLGN<:,+>2+201! 4$32! 4$#""32654&.#"632#"&5432??qqWN\\NN\\Ta/w N 5jA?rpGb[ZbbZ[b#P =  "#/$/ܸ#и/A&6FVfv ]A]A]A)9IYiy ] 9!9+ + !+01! 4$32! 4$#"!#!??qqWkQ1A?rpGK '?K +=+1F+1+A&6FVfv ]A]A]A)9IYiy ]A&6FVfv ]A]AFF]AF)F9FIFYFiFyFFFFFFF ]%F19%/A%%]A%)%9%I%Y%i%y%%%%%%% ]+=9+/4F19%7ܸ+@+ + ":+".I+.C+C4C901! 4$32! 4$#""32654&%.54632#"&546732654&#"??qqWT__TT__jivvWQMKRRKMQA?rpGPIIPQHIPIvSttSv\\=BB=>BB 4@+>)+>+/8+/A&6FVfv ]A]A]A)9IYiy ]A>&>6>F>V>f>v>>>>>>> ]A>>])>9A88]A8)898I8Y8i8y8888888 ]+ +  2+ ,;+,5&+501! 4$32! 4$#"532676#"&54632#"&2654&#"??qqWUa.w O 5kN[[NN\\A?rpG$O <b[[bb[[b &2>+#+#*<+*60+6+A&6FVfv ]A]A]A)9IYiy ]A00]A0)090I0Y0i0y0000000 ]A<<]A<)<9<I<Y<i<y<<<<<<< ]+ + -9+-$%+$3'+3$01! 4$32! 4$#"35733!"32654&'2#"&546??qqW͞u>@EE@?FF?A?rpG>>'*6ޗ{5!!X3 2!@ 2 5!!5!!5!4)4𬬬 !!!!!4)4XXX 333 Nf  !!!@@@ Nf  53353353353𬬬 3333333XXXX 333322s's' !!!!@@@@22s's'!!!!\!!#!!#\!5!Z!!X!5!$Z!!$X3!-Ԭ3!-.*!!@Ԭ!!@.*5!3,,(!3,X5!!@,(!!@X3!!- 2Ԭ3!!- 2* #!!!P@ZԬ 33!!P-#,Ԭ!!!@# 2Ԭ #!!!P@.* 33!!P-#\*!!!@# 2*!5!3,Z,!!3,X !5!!#@PZ,( !5!33$,PZ,!5!!$@Z, !!!#@PX !!33$,PX*!!!$@X!5!!Z !!!!-XV !5!5!!,ZV!!!X!5!!$#Z !!!!$#XV !5!5!!$#ZV!!!$#X5!3!,-,Ԭ !3!!,-XԬV 5!3!!5,-3,*V!3!,-X*5!!!@,Ԭ !!!!@#XԬV 5!!!!5@,*V!!!@X* #!5!3!,-Z,Ԭ !!3!!,-XԬ !5!3!!,-Z,* !!3!!,-X* !5!!!!@Z,Ԭ !5!3!!$,-#Z,Ԭ !5!!!!$@#Z,Ԭ !!!!!#@#PXԬV #5!5!!!!P$@V,* !!33!!$,P#X*V !5!533!!$P-#ZV* !!!!!@X* !!3!!$,-#X* !!!!!$@#XԬ !5!!!!$@#Z,* !!!!!$@#X*5!35!,-𬬬!!!-,XX33*!!@@*DH5!5!xX333x 2 2H !!!!-Rx !!##xmsZxH !!3!!xm3-sZRH !5!5!5!,NX 5!###lZZXH !5!!!5!4l t,ND 3!!!--Dx 333!x,ԬxD 3!3!,(D 5!5!5!3,,D|X 5!333,,(DX 5!35!3̠| 3!!!!-- 2Rx 333!!xs 2 2Ԭx 3!33!!-s, 2ZR !5!5!5!3,,X !5!333xtZ, 2X 5!3!5!33t, 2H !5!!5!4R 5!!###sZZH 5!!5!3!!t,-sZRD 5!5!3!,-DX 5!333!,,ԬD 5!5!333!DX,!5!5!5!3!!!!,,--R5!333!!###s,,ԬZZ !!!!5!5!333!-s t,ZR, 4763!!"Q[yY[`~| 4'&#!5!2.-Yx[Q`~=?x 5!2653#xY[Q[~|2Ψx !"'&533![Q[Yyx2|~>3m 2>#3> 2> # # 3 3>ݲ}#$cc|5!F3F~|5!|iF3P|!XF!@F~|!|iXF!@P5!5!!5iVV333PP~P!!!iXVV#!#P@P~P;(;!O;!O ;!O;!O;!O;!O;#!O#;(!O(q(!((!((!((!'(I(!]((!((3(:(' q( #'+/3!33!33!33!33!33!3mnmnm 4('/7?GOW_gow#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573#'573(;(!%)-13#3#3!3!##!#3#3#3#3#3#3#^^(ll(lm#;( #q:(!&9 '( 9(& &!"9(&!"9(& &"'9(&!&"'9( '9(& '9(& &!'$! $!!!,7r+uv ))xxp) )$7632#"'327$%&#"%632#"'~~~~eMM>yJJJJJ6````qq|qq#u"@91990  9%-p) 327$%&#"%632#"'MM>y````qq|qqr' '/7?G%&'&'6767&'&'7%'676727"'64'7&"'62&47\+;.81F9K58.42d;E9G,:.80G9J6&8.;+d1O9FLL&_`JnLL'`_n<1& j(0=Ju &,A=N:0('<1& j(0=Ju &1<>EB0(n_II'[[JnII'[[p) %/36%632#"'327&#"6767&'&6py AAAA,+-,,-+A@@Rqq|qq%%mܱ[0$ %@%|"p) )73276'&#"7632#"'327$%&#"%632#"'r99:9rr9:99XWXXXXWXMM>yB!!BB!!oe33eje33````qq|qqp @ 104767632#"'&'&pihѵhiihҵhiѶiiiiѶiiiip $32#"$27$%&#pkk<MAk^a``p $32#"$"3pkk<MAk^``p $32#"$327$pkk\MMAk^>``p $32#"$%&#"pkkAk^>``p $  $"327$!pkk]<MMgAk^```p $  $"!pkk]<Ak^`p})6%63"'pRqq)#2y|q*q( 2654&#"!|~}}|v< ( $%632#"'327$%&#"!IMM>y_O````|qqqqH( ( !#%&#")%632OyyMMqq>~``  3327$3!#"'$@1>qq``) %63"æqv`) 2#%&#u)q>` 527$3Muyv`>q "'$33yuMq`p)%632#%&#"puqq>``p03327$3#"'$puMMuyy``>qq!$ !$ !$! !$!$3! 2654&#"4632"&nȊce;~|ddcc||}$!%!!d r<$!%!!We r<$!%!W7 r<$!%!W7 r<$ !%!!!!+c,b r<<!$ 462"! W|VV} ,|VV|V !$! c  !$! b  p(  7& $  %;<*X֖$ !!!!!!,7,rWb<)) Ie$ !!!!%!!,crWbM)MM^??@7`d?\gOOOOy>*<?v^h"3263#!5276;'4?'4?26u'6"gP39.4! '*C0.xV#m14He '1l1 Z+dd?33 #&'&+"'&#"/573;2?"#'57#&'#"#5676!5:+#9,p!j[%+ > 7VCCc":8}V .e3B=Se` e9*=9 3@=}k %C`:d;emu}'S3273&'3327&'67&'67&'67'32654'&'2327654&#"3672 $54767&'&47'&327632#"/#"57#"54?'&5432'&27632#"/#"57#"54?'&5432'&327632#"/#"57#"54?'&5432'&27632#"/#"57#"54?'&5432'&327632#"/#"57#"54?'&5432'&27632#"/"57#"54?'&5432'4327632#"/#"57#"54?'&5432'&27632#"/#"57#"54?'&5432'&27632#"/#"57#"54?'&5432'&27632#"/#"57#"4?'&54327'4327632#"/#"57#"54?'&54327'&27632#"/"57#"54?'&5432&'67&'67&'67'&327632#"/#"57#"54?'&5432'&27632#"/"57#"54?'&5432'&27632#"/"57#"54?'&5432'&27632#"/"57#"54?'&5432'&327632#"/#"57#"54?'&5432B~ %<z*+')+(@&'$||e<-A}]\B-71SLoWj\vLL)(0/ (( .1(%%,* # $ )*f$% +) $ #*+f%%,* $ $ )*  \o  [ %)#&'%&)#`#$ *) $ #+,U  Q  0 E%% +) $ $*+&EC&V*,)-)-*,%&%&fБfU 3HhfeefhH2pu^QFs棥sKQGh!99!  !77!  4 4 22 K44 22 22  11                   7        %&%&%'%&%'%&22  //  g               44 22  ->O`q +&'&54?632332?654/&#"2#"/54762#"/54762#"/54762#"/54762#"/54762#"/54762#"/547672#"/54762#"/54762#"/5476%2#"/5476%2#"/5476%2#"/5476D.2`{4&/<) e>O ,4H3R 07K $   $   #  #  #  $   #  $   $  U $   # " $   #  7Q=KG<s-8PZy9z _e""#/2dt0&2j ,: . 4 . = ,  ,   -  -  -  -   .  .   ,   -   !! WV9`8 !! 7 ! !WVDu9`8N I 7%7&54769 }V&7A 6$ 8'^4? !2 7%7&547!&'6I@Y%14HFS"="l-2DC[9 &! 4$32 4$ #"&54>2JJhhq0^mNMn2Z^Z2K7iwBNmmN1Z00Z} C"32654%"32654&%#"&54767654$ #"&767&54! ggJIhIhhIJgg[ZQoy y}WZ[zADgJIggIJggJIhhIJgU\\Q srW\\^} A4&#"26%4&#"326! 547&'&632 $54'&'&632hIJgggMgJIhhIJg#@@z[ZW}yOOyoQZ[sIhhIJggJJggJIgg ][[Xrq Q\\} "32654&7#"32ɏǾ/`T_ȐɎ;P12Y}1"264&"3264#"54327&5432#"'&'3xyx& کZTdIU  k#5AMYer3#"'%&547654'!#"'4%$53!76=332654&#"#"&54632'#"&54632#"&54632&'&67632#"&'&676'.547>'.76$6&'&54%6&'&6>#"'.54>32#"'.54 [$gi< D""D =if%LW쥨驧r^]]^ !! !! . . *)X,),*))+. } +G  G+vKK9__9KKݧꧦ]]_""""s!!""W&. - . - a)," "  ))    !) /     p%-5AMYdp|5#!4'&'5#2#"&546"264"264"2647>'.7>'.676&'&>&'&7>'.%7>'.676&'&676&'&53!76=3%#"'676%27+%&547654'7327&'$%'#327%654'&54718楣. . . .  - -Y - -))G))))U*)>- - ~- - VK; yA C0B Ax ;K'6FJ> $06# >JF6&@@1AeA1@@H磤椣筁 . . . .E - -- ,1))),(9)())u- , - - G77W6 W77G D&& ee˥ &&D "(=pp=("u !!'!Pn8hv "!!'!##+572367676MoL)>u eI3?ba8hA:F;/Itxv !!'!  ##' Mo_h[ei[i8hi[ef[l[@36273 ##'5) U.WW1@ US Vdv#,5>~3+&=43+&=4%3+&=43+&=43+&=43+&=43+&=4%33 #&'&+"'&#"/573;2?"#'57#&'#"#5676!5\:V\9\:\:]:&]9[\::+#9,p!j[%+ > 7VCCc":8 #8d#7$6$8;$7i$7 #9pPL  )Z. ;6ZV Z3%Y63 .87p  3DMy!674#!!6?676545&#'323276767654#3#&'&'454632767!672!&=75$/563&43!32+'!67#>54&53# ? I :W0 96;E,Q 2:&l6x0 bm! o۸"\>%Ef~e2U6g!6V#p5C+ C ? P9 @7H4XmM7RV /M(=H: ,qLUD)8Wqke-Pex NW =$ U  /0c)H?2@[nDF8T$.J? !' !T4XKGwL5_K !'7W4Z~wDS&5476322632%632#"'&'#64'#"'&'&54654&'&54767632xJX%&XA,B:\8 [EMH95##Fl% !9@!#jL p_Mi#"?8" %lF##58HN4hok@RRr*%te BB9'7*$%) "fXS5EIf" )%#,7'9CB >E3#"'4332327$'#"$4727%672567654&5&oJ7.b9M D ,B3 qY 5**]d=HN9% sW$,J ]T-MMm@ed: ,'Z M'cM&T)$$ < I2%!"&54676737#&'&54>;7!"&546767!7!"&54>3!6763!26P+=6/2D>R+>2,+v*>>+2  ,2 =,2  =,3>,2463!2!2#!!#!32#3#!>*v+,1>+R=D206=+P#,>3,=  2,= 2,  2+>{"D%4&#!"!0#"3!!"3!#";#"3&'6737#&'6737!"'67!7!&'63!67!2I0!6OS SS: SS>SS]]J]]]]h\\, Bv*>K%39LKIOKHLKIhghghghgE?-L!D72654'6#"'4#"'54#"'54#"'675674767#%$4:JILLHOKHLKIhghgighgD>-sJ1 b6'SS cRR SS?SS\\K\\;\\]]!A*>K{!C%254+'3254+'!254#!'!24+!&#!"463!!2!!#!3#3SS?SS *vA!,]]j\\\\K\\IKLHKOIKL93%N-?EghghghgiL!C32=732=7325732'654&#'%2&'&5&'5&'IKLHKOHLLIJ:4$N->DghgighghSS=SS SSb SS'6a!0J)K>*B \\]]:]]J]]}O &*.26:> 3656;2#'7+"/#"'+"5&54775%"'5476;25'7&56%635&56;374765'75'76=4'&+ +"'4!#"'4543$365&5&#%#754'&5&&547'5367&547+&'&'735&2?"5%75537'7'3533553535'32767&5%2?&#%55'5757757751:e,$?F?Y>F_LA3ELH3,8LYLlEF'!0< k#gF  EeY!! Gp&iq.8ZN$%`BCf F4"4._?ee3&{E(1-+$Kt8 -  $Gs sM rEF"2 >_plTErf^5.>=9|5"-l)d ,&>vv]cccWpC-+ d8 Bpp>W]oaxvuPp82,D ^8, ^B$K+ "1R[+e*; 2 W QP I&? gpo% w ^SA$ 2 9i-5n02 Ai&IY^P]D%\??\OWC ,,1 /211/=;7777=321811{908hN%b\Dh,)h?17I21!122223 21&2%2#"'&=477654'#"'5473Bq4|l anN ilm b 9 b؍MOb>YaYƮ58l7P P@ $0<FX + &=6&# 3 6=%&#"';27!5%67%!&'&'2+"'&=476r cR~UY082.ԍ_W_V"+}IR8D).P9H'S]ٱZYHYoX(I_ ;.2lOP%.G6R%&I8d)Nl>54'67&54&#"&'632.547#"'&'#"'3267654'7327323.#'654'567654&&5476;'&'%&+"#"8DH$$yU ?L[>!WtJ([Fho*m.2\=w\`|UP7:/E" @7?EP]Eix pF@T5ym,"&eB@q(A _% #+B7!N &".OS$XE/K(Aa]dLP*'FCaYr=C44mo C (FKWYFvbph'UD'R< $d#+?Vm#327&"#"'7'632&'$54#&73254'&#"'5&567#&''5$'67'654'6'5$'67'654$'67&'654'''5$56732#"'&#"&'$'63&47"7&'7&'7&'7&'54'6546767675477&545?&''5&#" '6%35&'.54>23#67!&#"W  OB7[l#> F_Vh " "@.,=6tJ4Vp1EQJqMi vhpHI!:JJJ =4m\8B*?o v!"t,`s&*_~P1>5='g=>24<+-s[,*&sd1PT>3J@='h<42J-H#*YT_Y)*)X^TY*$D  ?>}>  *0t"J.  &b54CUE ''!`9 !,(MTE *! }q~=/+)f[4f !B" <@0&9c?"V+GoMK~a? }b9e\ P&0@k"?c*GEJX ?e}9 \4 \6 '''' 6\ N(&'65&'67327&+!65+"3yyys{w ccޱqXeXc6 6 c ,35'533#3!'#'5!5!5#53!5!5#!!-ʷ}} ckvG G @<<3ffX苜qXGccGJ 326&#!2+73 ### 3(ttvgnؐB(33#!!#'!'57!5#'5735׫$"q~q+!#!573#'5!3!'573!#'73!#'5;jjŠJss<wѡIjj8/w{,32#' 3%+ &5%6323'#57'53^VQ6>ѨABؒ6ʞG2k >Y3~||~Obs32732753"'#"'4323$4'5;+"'#"'53275'&'&5?5572%#&'&5%634%476=%@.!%,BE,#!-Q2" $nL/PuHED832#"&546324&"26%! !  Őb{=&*<<*(;E;R::R;KJ67Ϛ{ɬ)::)*<<**<<*):<'L67I&' &' &' &' &' &' &' &'  @FLRX^djp3264'&#"&47367'676756273#'#'5&'&'7&'677&'67'%%&'&'%6767%&'0/CB^0/AC/pkTcR|'N(OfUippqUfO''NQaQh!$ b)dLQk KRt!% c'd&//^000'N'|P_PfppoQ`Qy'N'P\ QgppmQ \Py,  M N>&`7" bK*V&"g{ M M %1=! !  54 #&'&#"#46324632#"&%4632#"&67KJ]_EASvwSAF͒D10EE01DD10EE01D7IL6a]U@SS@U1DD10EE01DD10EE %1=! !  54 3327673#"&4632#"&%4632#"&67KJ]_F@SwvT@E͑D10EE01DD10EE01D7IL6a]U@SS@U1DD10EE01DD10EE %1! !  5# '&'32654&#"32654&#"67KJ;lWPihQV<=UU=-1\ H0e%FKSwZGr=;=NN$E| 1 ?'_>?@7`d@\hPPPPy?+<>w_VG{?,rCA+ +"'5$76%&'547327676=&#~jt1/Q}](+VRxbO P >nS]] =fP+! &56;2'5$%75#"3ui1.P~N](7P,VSZycOpO >S\^ f0:1>7#'#53'&'&54767&'&=33676=3#326'&i($lm$(($[Uu&tU[$&uU[[UV$|ddb e|$% ZSSZ %_TYYT* $4&#"326&5432!!##53&w衤礡PP䤣L~||* $32654&#"%#"54767!5!33#b衤礡7䤣L~|| $&$76+"'&5'476!7!ttsstEus pid5s qttrtt<֤ꧦg\ulS5264&#"#43233#!5 z{ym㗗y{(|j#53533#632#4654&#"#*jjoon}mZyH{zF2 1"32654'#"&4767!!53#5!!3!!#3!!pOO87O:=0LmkL/>Λ2  1O79NN970LؙL1KӘJJ-'<%#5#535&'&'5'73'3#'73'676=35'73'33◰zhNgeMjzzTThOʍ7NjYYӖy?! #!!!'!27674'&#.d ;6zFH%QM_\ǃ$P<]$!#"#&5463 67!2#654&#"V⩁"T]ts]U"X"1((1"u." 6&'67>3"#"54767&'&#52&͕LVa{.+ؔ)0zHUM\&ϖ=Bll)'ҕ*l8lB=j&'5 %$ 56?63#'[Wtutu4ZZ//[[5  @Eo&<"3264,'532'&54632264&" &$#"#"&547>B_^^l;͓hI^9l:͓hI (+|TlgMLx)+{TlϔgMM M>54'.#"32463227#"&5454&#"#"&'&54767632254&K2q'$#K1o'#0ߴGdAoc.% 3t88bWDs-Kx68<32>32#&'567'45'#&+"#4'3>$4&+"?w(K>R0D32>32gYYYD,.:?#)v$E?w(K>Ro}vvxJvaAjtAO]ƀwϧ!5!3##'!5!~2k<@i8080k<j)127632#"'#576&#"4'5267>327&'"SkQmyz,~zi2@:$(.-)zW] ݾgvx-aX[&ŝ9{'Q32263227632&#""'&#"#"'&#"#'3232762327632&#"#"'&#"#"'&"#'Es- p86rV+)|m^?_354.#"!&'.54>325467675#53533#63232>54.#"P#3JTRJWVJQSOMJ4"?*&ElnhPL$ llill %LOhnlD')----+)QPQ((QPQ)+/ 6klj$?6FWWF6?$jlk6 }++--JHNRh|&'4>32"'4>32&'4>32&54>32&54>32#!5!'!567>54.#"32767>4.#"327732>4.#"327>54.#"732>54.#"M_ 6694S55.+C55C&.66 V\+55 c$M##$ 6$#$s`%#$d0"%)h #"#_33@]22-"40446/*33UJ"+33^1/K=0T* ####  #&$$&##&$$&#  B #### *"$$" U!'-2!35!#3!53573#'5#5!35!75!!5'57!s\\ss]]s JRRIJ~֛E77__vtt4!v7CQ^&54767&'&'5676767&'&54>32! 535#5##3654."!2>4.#"  <$))+N-N*)N-M,**%:  @ v<-MTM-?K5:66459<5&?HPPIK* ')+K**K+)' *KIPPH>&5<:6uN|l||l|-I+N))N+@6:55:5Q)5>o654&547!&54='&'654'67.5476;+"'5#"=6&'76767%25#654&'Fz-6 Z8. ,N0H!h6%`+EH )#M ;,Jga#iR k' M +1^hgo8:(@s.Pmz nx?.#1p#41`&>%!ac,,LHJ x}647| + OJJ)!0 P[32>4.#"32>54.#"!5&54767&'&546767&'&4>32'&'.#":e79e89f76e`[S &(*UM,N)(N-KV)&& \@ECApd88dpg669:%N&KRS* 'TM**MT' *SRK&N۠:9}qyyq}c $Tdhy67&'&"!3!67>54.#"!&'.54>325467675#53533#63232>54.#"!57!&'.54>3234'67632!P#3JTRJWVJQSOMJ4"?*&ElnhPL$ llill %LOhnlD')----s=BDw@>=))==AwDB=+)QPQ((QPQ)+/ 6klj$?6FWWF6?$jlk6 }++-- !yCB{C!$$!C{BCy! JHLP&'4>32"'4>32&'4>32&54>32&54>32#!5!5!M_ 6694S55.+C55C&.66 V\+55 c$))_33@]22-"40446/*33UJ"+33^1/NNOOU%)5!5!!35!#3!53573#'5#5!35!s\\ss]]s ^^/oo#E77v4@4767&'&'5676767&'&54>32!&535#5##3  <$))+N-N*)N-M,**%:  @%v<5&?HPPIK* ')+K**K+)' *KIPPH>&5<:6n5|l||l|L3?HN654&5473#!&5454'+#"#7&'654'67654&547;2547#";65'"3%:U"-6 Bu Zg0krX0c-h8E+`%s H>4wM-'9.QY / o8:qhPSmh #%Bz1"0@)5"@YR0.&54767&'&546767&'&4>32; &(*UM,N)(N-KV)&& 9:%N&KRS* 'TM**MT' *SRK&N۠:9C##"'##56'##"/547?^'5@_*SU&/UL ;Yԧ9UP(` XI.s222732#&547636=4'&# #4'&#"*t pz&=<xQ>hG:V Hek%PF5NP B|-&pA&NFX &&5 <F:^;" V gdG7236;2"##'65##"'&5476;235&'&=476e x<JT`(GeRUdfB3 VNTMT,P$ 66$0_ u3dUdt_}s*$"Rt0XX__/ik=ZG8*F 1 . ъf)MC =g9EkO 9!(-);&  ]t!y" & 2| ba$ U+  #8M35733!&54?'7'327!!"'&%#'7367654'77'7'&#"'676ի,&T>=c#]K9.U:1ʈ%`T?7>54&#"5>32&54?'7'327!!"'&%#'7367654'77'7'&#"'676]T@1$J=c#]K9.U:1ʈ%`T?32&54?'7'327!!"'&%#'7367654'77'7'&#"'676Z _3lFHe5^\VOosHGJI)`VKm1Sj,&T>=c#]K9.U:1ʈ%`T?=c#]K9.U:1ʈ%`T?=c#]K9.U:1ʈ%`T?=c#]K9.U:1ʈ%`T?=c#]K9.U:1ʈ%`T?=c#]K9.U:1ʈ%`T?32#"&e|e(<X<ħñ"32#"&$2#".46e|e(<X<ħñ"@<#"4.#"e|e:<#"< !<"#;zch =B4.#"$32>4."e|e:<#"< !<"#;"< !<"#<@;zch =B54.#"##"'5##"$'&'0!5!5&'.4>32!!676767'%''H&(G()G'%H(%'V W3WImuw>DE}AB|GE=md^JW4W Vs'H''H'(H''H`XAK|@X1(ԁ3"|}DD}|" 2/ "1X@|AX1# / 673&/'67 &'"&'6?&'3 ' K[]><+Gg['fBBe&\h?(K?]\K !;32T $ #AC,MMMv A5p_9D-M**  B@0"@R//>wA&oc/D&3.YaQ/5"1'"uE62/u= =!m- .... y 7%  %  32+#".=!"&'&'#&=4;7337_% 8)0/_^^M^1/ 9534<&&<&*(D>?GGzB6C{GG?>D9/C}&632#"&'.#"'#!#!#Ҹ62K#+~KF0R!9'/Nx_TV_T 'NQ9;:#8HL"CD|))Z) 532>4.#";267#&=&$32735&'.4>22[02[24Z1/[)'5*+X A323#67#&"#"/&'&547&#""'6%676V n*[n%'ZxL0<{2;&b;0&8a>!U*~EmLK}`? {a7c[ O&0>j!>a)E~CKW ={d{7 [+M57LL75M-Z '*''*' Y (5[ J5( \d (5J [4 ''/7O_2#".54>&'32367&%2327654'&''67&'&'&'676765467654'&#"7>326323#"'##"'&'#"&'&54767&'&54767232&'&#"6&%6767&'&'&#"676&5467&'&6732767&$$$$OG3%V cc V%4GL944m/122102/.303112.OF}6&V e"w?>v"pt #87! vn":;@A<:"nx !66# sp%./13/.UVT\<>"$!! !"#">kc V &6|FO 93399 <>#"#><  "$ZTU./43..V5$##$59gT;&'9Z^^Z9'':Tg9'(''&()I8:9889: Z_59eU;'( :8.>euvc>-7:bccb;7-?cwud?/8KWZZW **D@@D+8(':Te95^&)(&''(DA:AD.*!Y[[Y!& !-x67&'67&'4&6%67.'%4'6&#"&'6767&54?67&'&#"#&'#&'5&'"'67&'&47632>4.#"%2#".4>'7,3 3%/0),7=*#0*+3.22'8  YfT,1'').UfY >98 "2 B2;F_ XB?2C 3" 894ihgikce"S[XVWXZ#ejpMcNTvJKrZ1VlLWMI p jk%nA V{ww[11[ ww{V @#fd-#JM 7B/""0C7 NK",df#νhhοggQUXXUd %3!'#!52#"62#".54>" h9|M463%&$$5 O Dn; $$$$33'554#$/[QwGSGUW GJGX .5CK&5432632!!#!##53&4&#"326!&&#"327&54654'XP}}P~C;7?_Xej;A>7'sssLFF~||ב-  䤣lrrq)-5DL&'&6767&'"'&'&'&5'476!7!! 76'&'&'6'&utss-5 l&kpid=pDi/tEust,2}ts5sqtt-ԛ1 k&iꧦ g\}ul  An?\27/rtts,͓}qt)8GO'"'!!##53&'&54326!7!&'&36'&&5'47&#"327674'U`P}zpidu>7;C˂;C>xtsK) ||LGD g\uls螝՞䤣hkrr .4&#"326&54762!7!!!##53&w衤礡ᩨhn&䤣羚 o[tꝇ|| +D#"'&'&'&47>76327'7'%'27>764'&'."(F3"D"&%#}bV`ZZ^;D"&&$[X]:3G9:]:F=~=HS]^X&% iiD^29i\=<<92-1X?:<91*=X62'%'!!#5!5!5&'&'.546767''7'''7"2767>54&'&'&4p69].(EGGE@Z-<81VDEGFF'19T]9T:G5>+.11./:95>+.11./:9 \2:a(Eb_E@( %CE_bG(Hij:ο\ij+.wBAw./+.wABw./4+F!!#"'&'.546767675!5!' 2767>54&'&'&"<-Z@EGGEDVRbfNZ@EGGEDV18kbbjC9:/.11.+>59:/.11.+>5疑 (@E_bEC%##(@Eb_EC% kajP/.wBAw.+/.wABw.+ +F####"&'&'&54767>32333'7 '%32676764'&'.#"ܖU (@E_bEC%##(@Eb_EC% Uܭkaj/.wBAw.+/.wABw.+<-Z@EGGEDVRbfNZ@EGGEDV18kjC9:/.11.+>59:/.11.+>55 @  10432#"732654&#"陽…5 @  10432#"K +@kk k kKTX8Y104632#"&732654&#"ϑϑϘuSSuuSSu͒ΐSuuSSvvdPK!)7eK RX@ *.,&"($ k3,k($kk8991@&"6k0k 8<2<299990Y4632632#"'#"&7323&547&#"%6547232654&#"dϑRDDRϑRDDRϘuS?>Su^222Z>?SuuS ͒!!ΐSuXqpWv28ML88LM{WpqXuSSvTZ`z8Rm3#"2767>54&'&/2"'&'.5467676"2767>54&'&/2"'&'.5467676R#)$#R#$ $LK:C.25521@=:C.25521@=R#)$#R#$ $LK:C.25521@=:C.25521@=zZF)(JG()K.2IF21.2FI21F)(JG()K.2IF21.2FI21 J7Qk>767632"'&'.'!"'&'.546767632$"2767>54&'&'$"2767>54&'&'#61@=HK:C.25521@=:C.5%'21@=:C.25521@=HK:C.6#R#$$#R#$$R#)$#R#$ $5[51.2IF21.4`]21.2FI21.5[F)(GG()FF)(JG()KR 5%%%xr6׊eMM^xxV)7654'&'575#!&54767'5!s_vR$N::N$Rv_{aT,X@X,Ta{4b\)1%==%1)\b4ߴ:`\KDDK\`* 4&#"326&'&5432#w衤礡$PP䤣L~{Y,326& '6 !!#!5!(+~uP.Gjt ~||, # !!#!5!L>>0oJ;||,'!5!737!!!!##53z{{{z{|zhz|{||WxT% ! !5! #3 35!'T??LLwLLJ|A|JZt|J|,$264&"&7673% %&uuu>hH]%VgVYFhݦuuv#gGέҔEgEY$&'&5%6;2#"'!!##53uN)$#<^tfFp!E&J <ԩ;  ||lPj'#"'&#"'&'&'&47>7632327>76&'&'&/&'&'&47>762!2!%327>764'&'.#"&#"327>764'&'&s* 0$+$$$ 1#*# ZaZ%% NT12 4 #HH  ")mROeb  , 0  +   ) . $J . %'.D"&B 1 $C mR )Ky    !   V!Edz267>54&'."#"'%"'&'.5467676;27>4.'&+"'&'.54676762%632$"267>54&'&.&&.&m,mQjP(!N!"(! aVf&&bZ55!("!N!(PjoQm,.&&.&q    l?W,>&#< A#"< " (( " <"#A <#&>,W?~    lOOj3!#!"'.'&47676?6767>'.'&#"#"'.'&47>763276;%32676764'.'&#"676764'.'&#"32eOuRd2!  HH# 7   ZTN +Za21#+$0 4$$$+$0 's  *   * OK) Rd#!>& 3"9*$"D. ' - D! 2 . , T% #: & (  IZx-4H67&'&'&+"'&'&'&476767632%632 #"'%#"'&'&'&54767676;276276767654'&'&'&"276767654'&'&'&""'&'&'&547676762"'&'&'&547676762'&'&'&547654'&'&'&";276-&#"+"276767654'&5476%327%&"'&'&476762I  Q\C--%("(/*0.,+"( /X]\9<\X/"$)0*3')"* %1*0CR[        22 2 2 2 %'   &J  &%C\d#_*]OhXC%&  J&   O]*       ")&`&"'$"/' <%ZS  % SZ%< /'* "%5"-($# ;8\= !  !  " /VC "  !  !  [uV/+    V^au 767>54&'&'&#"&54767632 '.5467&54732#"#"676767#"'&#"'67654 ozwbda_f_zx|wbdaM,krnulspsnunNJ*D$ lQ$" 6*D?"5'K(2- # >   :72 331cd툍i`4331cd퍇>mwn<;;8ro졘wp:;;BV0/M8:D@*|sa  -F(7 "*=8&0!2  1-5$& 6:B4V^ (B\w.'%&'&"632%6767>54$2"'&'.546767" 767>54&'&'&'2 '&'&547676?'*&$ 1$-+h+-$F3782**?1 $&>>9|wbdabc`zwbda_f_zxspsnunˎspsnulwI_"2[$  "" gI $[2!v 55 55 31cd퍅caf31cd툍i`43d;8ro졘wp:;;8rown<;x,A-57'36%33#3#!2#!3#3##$'#7$@d5{sVd]F0 0F]dVs{5⒒d@( jPP,PP` 0 ")- !676762!"'&'&'&54!X$#R#+/RFF$#R#$1Sh,  k-"s!}P476?6763&'&'&547632676767654'&547632!54'&'&54'&&#"'&/&'&'&#"#"'&'&/&'&#"&'&'&?6'&'#"'&'&#"!'476='654'&545454'327654'&'&327654'&/%4-)"$0JK&  )7    %0'# #6 +-L __^/s4* 1( .266 |/(1   \   #:7  lS&   x71]/~[#<$  o_%@,: $";vR $X$+|!5DX&PY;9Do6 b'n2  83eF] 4T&  &  /50$?- 1@& 3l K  C"P1 :03<D:5XI.)D&[+-1:   q/A8   g+jl9Lp{7654'"'&#"+"'&54?67676763276323273#5%6767'&#"6"/67#"27632327654'73654'676547&p/l0&J!cS%YE]{@C"$4>-;% ,(6Y>m!N$X6"/,(4sS?X$U>"sJ?K(`./4+2K2.0>S Zp0+1^' ;cs  /^"|Y/ 428ۇϕl%%ot5oA='Y$ aT* ''G+- %_kj~r}jL`І|\gK@/.85c($ (2LS>54/##326?%%3254'654'3>7632#"&547>32'% ;66I   }g ?6qn   -> 9@ H67;  zh| 8 >6!q    B5>%+?F4&'&/76765'7! !'!654'!4'!!$467>2"&'&!654' 33 ^^^RXI#J2VlP# ~!88!~ Kppph,p<(##(#id (2LS.#"227654&'''%'654+.#"65.'&54632#"'.6#"%  I66; o |>?%6!q   9  ;76H   |h> 86qm    BX{[%G'23 %%.'&"27>7%$!"#232%"'&'.4676762%#"#2%k      A>>dIID`nS   SnGYn 5>5 n)(%$#"#64'232%%&'&'&"27676&22k**!n``n!##3W 2327632#"'&'&5476'( > !~GH ".4F+@xH )0$'*' 23277632#"'&'&54763'( e` }{*279HF`0@xJL 1 ,A  ' 7 Ɏ877Ɏ77ɍ8ɍ? tt7tt7t7tt7uB2632#"'&'#"'&54767'&54763267632676 Q   x L$3 z(   6X3  6*=P*> "#  R26#"'#"'&'+"'&'#"'&547&'&54767&&5476326763276T 디% $$YyX$ zc0 + j :  (̢1#: _$ #- Խ =1 '2ĺ pD #!!!!!%!!!!!!!!#!5!36HVBBXBBUHVPBXyBpD !!!!!!""p"p"#pD35#7!!#!5!3rrsrspD!!%!!!!!!r"p"#p"#Rb !!#!5!3ppEU l3!!'#'!!#!!3!5@,r,,_ r,,_>v #!!!!!'!!!!!!!!#!5!3hm_|P_H_pDK#";54&'&'&#'!326767657'&'&'.+3!76767>5{dIB,$2$*DEh{LGC_RQ|66R_CIJ{hED*$2$,BFd{LGC_RQ66R_CIJKIB`OT|87O\FGKzdGB+%2%+BIdzKGF\OT87O`BHL{dGB+%2%+BId  #!! !!! 373#'7#ZAA:Llحmllmzlmllm|}}|d d}cT`C54'&54762327632#"'&+"'&5476=#"#"'&476323(L,68x86,L zFvd0000dvFz L,68x86,L zFvd0000dvFz zFvd0000dvFz L,68x86,L yFvd0110dvFy L,68x86,LV^&'##"&'&'&4767>32367675&'&'.5467676236767>32#"&'&'&'#"'&'.546767675&   R.-R  R-.R "  *!""! ((\(( !""!#%   " R.-R  R-.R    %#!""! ((\(( !""!**!""! ((\(( !""!#%    R.-R  R-.R "   %#!""! ((\(( !""!*  " R.-R  R-.R   Sa4&'&'&'.546767622676767>32#"&'&'&'.#"'&'.54676767>5"#"&'&'&4767>32(,$ ((*& :.r06$&**& )'De!  'd8:b&$$&b:8d'  )a@/!  ')*&$6/r/6$&*)'  ')?c'  &d8:b&!$&b:=_& (bCc"  &d8:b& $&b:=_& (a?/!  ')*&$6/r/6$&*)'  ')De!  'd8:b&$$&b:8d'  )a@)' ((*& :.r06$&**& ((T`0267632#"'&'&'!&'&'&54676763267632#"'&'#"'&'&'&5476767!6767632#"'&'"'&'&'&54767#"'&'&'&5476767632!#"'&'&'&54767#"'&'&'&476767632&'&5476767632!#"'.'&5476767632&'&54767676Z   ( &            <   4          % (      (   2     6           %    <    %  (   W_2767653"4'&'&Wspsnullunsps;8rown<;;j>-'O^__^Oq44H4"hdd0!% %!-@jjjk**37'73 #'xxxx.xx.x..x  pD #'!5!73!GFdFGrEGdGErFGqFGdGFqGEd@L     - FOFc,OO,cFd,PO,dGOP T` '%%%%%% % -wD{wwe#w%f{wwy||y{xxe#w%f{wwxEy||y % %  Zp/AppA/}}ET`     - Zq NqqN  NrqN qrT`% % -ZyllylyyT`%% %% -ZtGcVGttGVcGGstGWcGtsGcpD/3%!!%#'''%!5!%777xo:U.cF.d;UǩoxoU:e.Ec.U9oE.f:UūoxoU9g.Ff.U:oxo9U. 54'&5476276767632+"#"32;2#"'&'&/"'&5476=&'&'#"'&'&547676;232?&547'&#"+"'&'&54767632676'K,68x86,L qA'C<4GW>L d  f L>WG4L d  d L>WG454&'&/54'&5476276767632+"#"32;2#"'&'&/"'&5476=&'&'#"'&'&547676;232?&547'&#"+"'&'&54767632676o**YK,68x86,L qA'C<4GW>L d  f L>WG4L d  d L>WG42#'"372"'&'&/"'&476="'&547>Q!//VZ *nN+G80j@6RR6@j0/P1N TP#00VZ ,lO@W+G80j@6RN6@j03L/N  ]H,`,H Yc!77\4OO4VA7gU3',H^ ]H,`,L&3c!77\4OO7VA7fV4&,H^67654'&"327632#"'&'&/#"'&5476=#"'&'&5476763232?'&#"#"'&'&5476763254'&5476276767632#"'&#"#"'&#"327676%32767654'&'&#"#"Z8%1T1%85e %ZF\ +m8BS/?JV@6RTXN6@VGB1QB8n* \FZ% e53e!&ZFZ *n8BS/?JV@6RR6@VGB1QB8m+ \FZ&!e3DA 5<; > +F$H$F+ > ;<5 AcJ2QD++DQ2J (5H,'9,J&0f) T|\`j4OO7g`\|T 'g/& H,9',I4( (3J,&9-H &0f) T|\`j4OO4j`\|T 'g/&J,9',H5(""'!$(:UJJU:($!'""nFw"2767>54&'&'767632"'"'&'.'"'&'.546767"'&'.546767632.546767632=>343343>==>343343>x>%85670-),(-%8/[0!-(,)-02y/8%0%)-02y/8%-(.'&$W/:#-(,)-02;>/;),)-02;>/8%-( 06{IF{6006{FI{605+'g>:c.&".c;=g'+&1N%&W'+&.c:>k#"$.c:>g'+,B:>g'+&.c;=?nF\v%"'&'.546767"'&'.546767632.5467676267632"'"'&'.27654&'&'&"67&'&'&'276767&5467'&'&#"32767>54&/76767>54&'&'&#"Z0%8/y20-),(-!0[/8%-(,)0-<1:3%>(-%8/|/8%-(>%85670-),(-%8/[0!-(,)-02y/8%0M=  H C# B/g H /*x#$  8## H g/B PP  $#x*/%N1&+'g=;c."&.c:>g'.5 ?=;c.&&.c;=? 5+'g>:c.&".c;=g'+&1N8GG$> >$ c.,bB$#>  Ir0C >'#> LM >#$Bb,.$ >#'> C0rI T`)T:e&'#"&'&'&4767>3267'&#"327%32676764'&'.#"7632#"#.4767676324676762>322##"&'"'&'.5#"'.'&467"&'&'&4767>&'&'.'&'>76?&'326767767>5&'&'.#"767>7.'&/32>7674&'&'67'&'.#"67'&'.'67676767"2767>54&'&'"'&'.54?&'2767>54'7654&'&'&"67'&54676762:    $4 4$ww4 4 xy   %" !()-+U$"! ((\(( !"&S+-)(! '7M"# V2% A()-.R$"! ((\(( !"(O-,*(A"#2P"# "M    ! *4 2 kk  4 2 uKK        i2 4* !== 2 4  `_  wR#$$#R#$$  8 < c !<>     8 < d!!<>   "%UV*) !!$3R  R3&!-(-%Z& "#%(.2$( &&S+,))A!$3R  R3'A))XT$""#%(`$( "      i3+!x== 3 _`        !+3 kk 3 uKJ   F)(GG()F$    %3 3%ww3 3 xy   V^3N^"2767>54&'&/2"'&'.4676762 '&'&547676% %-z35++++++53z35++++++5pWDM69?=;9JHDM69?=;9JHSspsnunˎspsnul}}(.h<;h.((.h; +F$$> +F$H ;<5 A~ ;<5 A+DQ2J (5H,'9,J&0f) T|\`j4OO7g`\|T 'g/& H,9',I4( (3J,&9-H &0f) T|\`j4OO4j`\|T 'g/&J,9',H5(G+DQ2J$(:U$(:U3!'""!'""A''7'753'75377537'7'#5''#5'7#5'7'7<B-DH2#"2767>5!"&54$3!57!#"'&'.5467676#_>I-743TP>CPNDG-2.1/&D9 88 '.* !-8D_2{j@F'%.3r@Md7+4V/2&'&54676762"'&'.546767Zy*,&''&%1]~|45,-++-,54|45,-++-,5(+&a4|d΃fz4a&$(F*.j=3"&'&'&54767>32rJ6464NN4646Jp`684F@NLBD64:866D@NLBD668^~* i654'&#"632327632!"'&5!267&'&#"#"'&54763247632327654'&547632#" 6+Jo.^V|;-˙it36?̺fQMeEJS?(*$ s]vh2K)*NL13^v:Mc*ZeC03N35%&-Kt\K%9S >BWN=!$?$8(F!5{^?Z Q67654 547&'&+327#"'#536767&'&'&5432&5476323254'&5432?-BO>=v06&%K`dC+(k$'eM?$#=Hb B=)+8=.m9eb PB>$3g:84!EB7WPfG+1KHP<Ff#&T'0P+A'<}DC/'"05276767654'&'4rceNS((((`hm@DDF/CD}>C/GFCG !&547>2; 0!!6P<:! !$ ! "#{! !{54&#">32!5!>??qq>0ţ=as;N_/>!RL}A?rFi:}$:&N?(U?"Mt 6+A]A)9IYiy ]1.+. + !'+!+9*'!901! 4$32%4&#">32+32#"&'32654&'26??qq|=_ky4[\XZcksuD}[X@v hA?rs ?<:32#"&'32654&#"75!5!??qqYe2hvvhDw_X@ϰ?A?r%aVUa/  23/4/3и/4ܸA]A)9IYiy ]A&6FVfv ]A] +  + +,&+,/&,901! 4$32#"&54632"32654&#"7>325.??qq\NN\\NN\qºN w/aTJjA?rZbbZ[bb*= P# + + 01! 4$32%!35!??qqlUA?rv]K 1=++ +A]A)9IYiy ]A&6FVfv ]A]A ]A ) 9 I Y i y ]/9;9;/A;;]A;);9;I;Y;i;y;;;;;;; ]5+ )+ +28+201! 4$32#"&5463232654&'>54&#"2#"&546??qq_TT__TT_⾭vijvkKRRKMQQA?rlHQPIIPPI\vSttSvB>=BB=>B &23/4/ܸA]A)9IYiy ]3'и'/-A-&-6-F-V-f-v------- ]A--]+ +  +*0+*# 901! 4$32254&#"326#"&'4632#"&??qq鿹ºO w.aUJk<\NN[[NN\A?rK < O$[bb[[bb $0Ӻ%+%+++A]A)9IYiy ]A++]A+)+9+I+Y+i+y+++++++ ]+ .+ (01! 4$32!5##7##"&5463232654&#"??qq$ŸuF?@EE@?FpA?r*'$ =$>  767654'&'!5%3!!  '&'&54767̆mommom4mommomP\|~{{~||~{{~|96oooo6996oo  oo6}9:݈@>}~Ա~}>@@>}~,,~}> =6P  767654'&'!!567>54&#"5>32  '&'&54767̆mommom4mommom)4 \=)N=kP`aF7I׺\|~{{~||~{{~|96oooo6996oo  oo6_A.Xx;_x55'(IZV@>}~Ա~}>@@>}~,,~}> =B\  767654'&'#"&'532654&+532654&#"5>32  '&'&54767̆mommom4mommomttLUDWx~zB\RGr=\|~{{~||~{{~|96oooo6996oo  oo6yt'(xrjw_Z\bd @>}~Ա~}>@@>}~,,~}> ='A  767654'&'!33##!5  '&'&54767̆mommom4mommomh*˪+\|~{{~||~{{~|96oooo6996oo  oo6 @>}~Ա~}>@@>}~,,~}> =7Q  767654'&'!!>32#"&'532654&#"  '&'&54767̆mommom4mommomz#G#KSLVAC\|~{{~||~{{~|96oooo6996oo  oo6c ۻ)%}|X@>}~Ա~}>@@>}~,,~}> =%>X  767654'&'"32654&.#">32#"32  '&'&54767̆mommom4mommomllm=|< /Vڵ =|^\|~{{~||~{{~|96oooo6996oo  oo6EKۼ>-O@>}~Ա~}>@@>}~,,~}> = :  767654'&'!#!  '&'&54767̆mommom4mommom\N\|~{{~||~{{~|96oooo6996oo  oo6`E#@>}~Ա~}>@@>}~,,~}> =#9E_  767654'&'"2654&%.546  &54632654&#"  '&'&54767̆mommom4mommoms慄htdthutԄ9tihvvhit0\|~{{~||~{{~|96oooo6996oo  oo6,{{|kl{Eggss\hh\]hh@>}~Ա~}>@@>}~,,~}> =2>X  767654'&'53267#"&54632#"&2654&#"  '&'&54767̆mommom4mommom=|< .Vڴ=}mmlJ\|~{{~||~{{~|96oooo6996oo  oo6DJټ@>}~Ա~}>@@>}~,,~}> =+8Ca  76767654'&'&'"32654'.  735733!  '&'&'&5476767̆mo5885om4mo5885omT,+VUVV++2QPPQΠP3p\|~-,g%&݈@>}~~}>@@>}~~}> = $!5!#%  '&'&54767{\|~{{~||~{{~|#:9q @>}~Ա~}>@@>}~,,~}> =6>7>54&#">32!5  '&'&54767I7ݺFa`Lk=N)\\|~{{~||~{{~| ZI('55x_;xX._@>}~Ա~}>@@>}~,,~}> =(B>54&#">32+32#"&'32654&  '&'&54767ir׸G\\Bz~xWDUL2\|~{{~||~{{~|db\Z_wjrx('°t=@>}~Ա~}>@@>}~,,~}> = '! !335#$  '&'&54767hno\|~{{~||~{{~|  @>}~Ա~}>@@>}~,,~}> =7>32#"&'32654&#"!5  '&'&54767CAVHSK#G#\|~{{~||~{{~|=|}'' %@>}~Ա~}>@@>}~,,~}> = $>2#"&546.#"32654&#">32  '&'&54767PmmlC|=ϵѴV/ <|=\|~{{~||~{{~|+޸KE@>}~Ա~}>@@>}~,,~}> = !35$  '&'&54767>h\|~{{~||~{{~|@fE@>}~Ա~}>@@>}~,,~}> = +E2"&46' 654&'>54& 74632#"&  '&'&54767Yt愄/tԃuhtt-tihvvhit0\|~{{~||~{{~|{lk|{{Essgg]hh]\hh@>}~Ա~}>@@>}~,,~}> =$>%32#"3267#"&'"&54632  '&'&54767!C}= дѳV. <|=Allm\|~{{~||~{{~|Q/=޸JDg@>}~Ա~}>@@>}~,,~}> =  :2#"&546$  !5##7  '&'&54767eddedddB¡\|~{{~||~{{~|>-/#&%q @>}~Ա~}>@@>}~,,~}>uPj !!5!!Pp#@pppt 7%FN4NGuP85 zD<22pJJt '-ZKFGNuP!!u\lE>~~>uu+"&'.546?!".4>3!'.5467>2p4,,$$,,42.p ,.".2."., puP8!5! %JZPJJuP8!5! %JHJJuP8 #3#3#3!!5 xx<<oJpppJJuP8 55!#3#3#3oPxx<<΄ΊXXXXuP8!!5 %JJJPD! 6>l>>PD ! DR>l>>P  BlvvuPb3!5 5! '&'.u$##+* ZJMM*+##$0U%!JJ!%UuP84676763!5 5! u$##+* ZJMM*+##$0U%!JJ!%U0!! ^r{VXeoouP855!Dq΄Ξ0uj%5!!53  !<9h9>uj%5!!53  !<9h9>+Z !73#57!!+ Id&+ъ2&+Z 5!'53#'!!!+dI|&22 !'!'!53 !Odcndh 2 3#5!7!!! ndnd;ch dd !53#'5!'! !]n2n22r-hJdc;dJdd 7!573#5!! !2+2n2nr-hLJd;cdJ<6767632"'&'&'! <'CZmo~yti^Z\X^Vqoti^?)X6nGCZ.//+]Y݀z_X0//+]>Iʞ BP "&*.37#37#37#37#5!!!!3'#3'#3'#3'#<<< 7&#"7'7 !%*BF8WU{FC*9oX:WubP 55!5!!'!XXddPRt '327'' !!iFB*8X:*CF9XUpt>*%&#">7'&'&">327&5467>7tBEH#&NKX$W/,0$" D5Hp*G6$"!0,0Y"W!F&'&#GGCuaP'467#"!4676?'&'.5!3!.5P5#$%"//"%X$# 5eeJ(0Y! "X0(Jet*.'.54?'#"&'2767.'32t)H5 X"$ #0,0X"KN&#EHEBCGG&'&KW"Y0,0$"E6GsPX'<6%"'&'.54676$4676762"'&'&&'.54676762$/+z >_ $#R#af#R#)>xbQu 88RK68# 88  vc<*676767632#"'&'&'&%.5467.546A ''+/54<3o8n23'9%%%%bb%%%&:?$ fLLf#&#/:&'X23X'rr'X32XV2c"'&'.54?654&'&'&#!"#!".4?64/&4676763!23!2767>54/&546767622 Z ;:td Z   c   uu  c  2c"'&'.54?654&'&'&+"#!".4?64'&4676763!2;2767>54/&546767622pW\xj IJ \W   8  uP^'#76767&'&/3#>7!5!!5!.'PSJl..&GG&GlHSi7*nK Kn**7OUnm'66'1U=Hd)dH=m'*'$&'&#"'67667 h7Hm^:-3 RE SRQO1̡LHO'57$'&54&#""OER 3-:^mH7hH܏1OQ S #u ! ! j.u-10 3%!#3!Zddd/ #3!53#5ddZd{3 #pph # 3hp&T&T[[ '#'#'##'x\xxjjxx\x,x\ehhP8\xYY73373737+.x\xxjjxx\x.x\8Phhe\x,OlD=072767>54'&'&'&"7#7676767632#"'&ew@RNJV !'7$"!3!&'&'&'!#!2767676wx !1cbbc1! "1cbbc1" `x]\LM&  &ML\;RR &ML\]]\LM&ZwxZQvcbddbcvQZ[RwcbddbcwR[xV''LM\7=e=7\ML'e;6\ML''''LM\6d 8   2@ @@ 00 ]1@   990@   <<@ <<KSX << Y5!!dx yxUZxxu 8   2@ OO __ ]1@  990@   <<@ <<KSX << Y'7!5!'7 wxy xZwxxd 8ڶ 22@ PP_ _O O]1@    9220@   <<@ <<@ <<@ <<KSX <<<< Y5!'7'7!dxxwxxUZxxwZwxxd 8!!5!! s]xwx]ix]xZx]xiu 87'!5!'7'7!5 ii]xwx]iix]xwZwx]xd 8!7'!!5!'7'XiiiI]xwx]h]xwxiii]xZx]]xwZwxd 8 !5!3# Y#xwxݪ-xZxYu 8 #3!'7'7xwx-\xwZwxd 8 !5!53#5! Y]xwx]Q7ii]xZx]Eiiu 8 !'7'7!#3!7'Q]xwx]iic]xwZwx]\iiu 8%77777773'7'7#'''''''uFFxwxcnFFFxwZwxnF,X@,,X ,,X@',,,X,,X@',,,X ',,,X@',',,@,@',,@',,@',',,@',,@',',,@',',,@',',', ,@',, ',,@',',, ',,@',',, ',',,@',',',@',@',',@',',@',',',@',',@',',',@',',',@',',',',@',, ',,@',',,',,@',',, ',',,@',',',@',@',',@',',@',',',@',',@',',',@',',',@',',',' ',@',', ',',@',',', ',',@',',', ',',',@',',','@'',@','',@','',@',','',@','',@',','',@',','',@',',','',pX,p,pX@',,p,pX ',,p,pX@',',,p,pX',,p,pX@',',,p,pX ',',,p,pX@',',',,p,p@',p,p@',',p,p@',',p,p@',',',p,p@',',p,p@',',',p,p@',',',p,p@',',',',p,p ',p,p@',',p,p ',',p,p@',',',p,p ',',p,p@',',',p,p ',',',p,p@',',',',p,p@'',p,p@','',p,p@','',p,p@',','',p,p@','',p,p@',','',p,p@',','',p,p@',',','',p,p',p,p@',',p,p ',',p,p@',',',p,p',',p,p@',',',p,p ',',',p,p@',',',',p,p@'',p,p@','',p,p@','',p,p@',','',p,p@','',p,p@',','',p,p@',','',p,p@',',','',p,p '',p,p@','',p,p ','',p,p@',','',p,p ','',p,p@',','',p,p ',','',p,p@',',','',p,p@''',p,p@',''',p,p@',''',p,p@',',''',p,p@',''',p,p@',',''',p,p@',',''',p,p@',',',''',ppp,p@',p,p ',p,p@',',p,p',p,p@',',p,p ',',p,p@',',',pp@'p,p@','p,p@','p,p@',','p,p@','p,p@',','p,p@',','p,p@',',','pp 'p,p@','p,p ','p,p@',','p,p ','p,p@',','p,p ',','p,p@',',','pp@''p,p@',''p,p@',''p,p@',',''p,p@',''p,p@',',''p,p@',',''p,p@',',',''pp'p,p@','p,p ','p,p@',','p,p','p,p@',','p,p ',','p,p@',',','pp@''p,p@',''p,p@',''p,p@',',''p,p@',''p,p@',',''p,p@',',''p,p@',',',''pp ''p,p@',''p,p ',''p,p@',',''p,p ',''p,p@',',''p,p ',',''p,p@',',',''pp@'''p,p@','''p,p@','''p,p@',','''p,p@','''p,p@',','''p,p@',','''p,p@',',','''p,p',pp,p@',',pp,p ',',pp,p@',',',pp,p',',pp,p@',',',pp,p ',',',pp,p@',',',',pp,p@'',pp,p@','',pp,p@','',pp,p@',','',pp,p@','',pp,p@',','',pp,p@',','',pp,p@',',','',pp,p '',pp,p@','',pp,p ','',pp,p@',','',pp,p ','',pp,p@',','',pp,p ',','',pp,p@',',','',pp,p@''',pp,p@',''',pp,p@',''',pp,p@',',''',pp,p@',''',pp,p@',',''',pp,p@',',''',pp,p@',',',''',pp,p'',pp,p@','',pp,p ','',pp,p@',','',pp,p','',pp,p@',','',pp,p ',','',pp,p@',',','',pp,p@''',pp,p@',''',pp,p@',''',pp,p@',',''',pp,p@',''',pp,p@',',''',pp,p@',',''',pp,p@',',',''',pp,p ''',pp,p@',''',pp,p ',''',pp,p@',',''',pp,p ',''',pp,p@',',''',pp,p ',',''',pp,p@',',',''',pp,p@'''',pp,p@','''',pp,p@','''',pp,p@',','''',pp,p@','''',pp,p@',','''',pp,p@',','''',pp,p@',',','''',ppd?8 !5!53#5!s]xwx]ii]xZx]EiiuP8 !'7'7!#3!7']xwx]siic]xwZwx]\ii 3'#'##-Z-x\xxx\.x\n #\733737#x\xxx\xZ'x\# n\xO'=%"'&'&'&767670327676764'&'&'&pk_V1..1Vbrx`Xk_V1..1V_kpIxXE?#!!';B]YQS@?#!!';BQ9.-\ZnllnZ_.x$-\ZnllnZ\-.)xF!F@RNJV>lmGСBk>DdW0Xdtsݓ.W@#.  -&.%)/K TX)8Y299ܴ]<<999991@ &$-/22907&54&'>5!2;#"#!532654&+CI02Kl>>l5UU5D>kB0GmstݔdXЎW2  5 1Vd22h' %#3 5' :' 73 ٪L^8bb:'B 7''ٛ>PNq'B '''ٛ>PNq^D'B ''>PN'B%  '''tNP'B5  5''bNP#u  u-3!3!!#!#!5 L3ͨ--Ӫ--333333#######5Ϩ---Ӫ---:k7!!  767654'&'$  $'&'&547676h08rtrrtr@rtrrtr VGFFGrGFFG;:rs죟sr:;;:rssr:Ŭɪ:k3?  767654'&'$  $'&'&547676!!#!5!rtrrtr@rtrrtr VGFFGrGFFGssB;:rs죟sr:;;:rssr:ŬɪKss:k3?  767654'&'$  $'&'&547676   ' rtrrtr@rtrrtr VGFFGrGFFG]x3w32x3B;:rs죟sr:;;:rssr:Ŭɪ3x23w3xuM %' io& i' i% iJuM327!5!>2&#"!!"&' ;E 2&#"!!!!"&' ;E $;E Ϊ@z٨zuM&#"%"&'73275%>2";EC;EJ綠mzzuM*3&#"&'67"&'7327&'&54767>2";EIq(P >6D;E]InoSu=,HK%)AH!+p$ z1IosV2";E+@/V]H6H\nUm;D [>wfP3,,I6x/Ur]HH]lVzM>wrN3 F4uM!3#!!>2&#"!!"&'732w~9F 9 }9Gr0}}uM+3#>2&#""&'73273264&c)~9GcBnnVs~9F (6o~ç|K|oU}uMp.3#327264&#">2&#"632#"'"&'z;E-8pƖqS;E;DܛWI3>6я]z!zuM 13#64&"327&'&767>2&#""&'˔֐;E]InoSu;EcBnnVszяϐ-1Io7sV2&#"!!"&'73273!#3;~9G9G ūI}ޭ{ tMm-&#"!2#567&'!"&'7327!5!>2";Ed_``!;D ܻ`;`*I6ƌebIz`:H:`*F4uM#&#"7'"&'7327'7'7>2";Exx;EzxXyxzyxإzuM*327#467>2&#"#4'"&' ;E-A 4yy;E Z>Vy|-2PIϼ+zEa82JzuM'&#"63"&'7327&'&53>2";E*y;E\?Vy~+&8'zLFaI1zuM>32&#"#"&'7327!5KL~9GALK~9G⧅}}gkb>32&#"#"&'73275!KL~9GALK~9G⧅}}Р? 5 5FѶeѦ 55FѶ///m' //& 0'' /'' 0' // ' 0N:A%#"'&'&'&#"5>32326#"'&'&'&#"5>32326 5jbn ^Xbh`n ^Vhjbn ^Xbh`n ^Vg@PNE;=LTNE;=KPD:32326#"'&'&'&#"5>3232655jbn ^Xbh`n ^Vhjbn ^Xbh`n ^VePNE;=LTNE;=KPD:327&#"56767326 5jbDS4WVhjbm\Y@/Xbh`ES3VXbhZmMp[Y@1Vg@PD4KUNE;@LTNE4LRN"*,@J^po_N5<#"'3267#"/'7&#"5>327&#"5>32732655jbDS4WVhjbm\Y@/Xbh`ES3VXbh`n[Y@1VePD4KUNE;@LTNE4LRND:@J^T 5!5!-5 !5!uu/0\^ҲЪ~T -55!55!usҲЪ᪪/0N%#"/&'&#"5>32326!! 5jan^Xbh`n^Vf@PD:32326!!55jan^Xbh`n^VfPD:323265-5ian^Xbian^VgsuOE;=LSNE; =KJ/0:ҲЪ !(#"/&'&#"5>32326-5 5ian^Xbian^VeuOE;=LSNE; =KJҲЪ/0, -55!55!us%ҲЪ᪪(/0٪, 5!5!-5 !5!uu%/0\~ҲЪ^6 5 5 -55uu/0V/ҲЪа/6 -555 5uuҲЪ۰/'/0K/& 55p/ѦѶ& 5 5p/om//&' /G&' H{ 5!5 5!@Ѫop9{ !5! 5 !5!@Ѫ555@pNpop 55 5@p pU(".#"#"&'5327>76325hV^ n`hbX^ nbj@TL>7632 5hV^ n`hbX^ nbj?TL>֪VJ<:DNTL<:DNDop$+5!5!.#"#"&'53276767632 5hV^ n`hbX^ nbj@>֪VJ<:DNTL<:DNDf $!!!5!676762!!&'&'&!!C.8d 6WYYV7 e8-;Z{+DD\93[2332[0<[EC,W7!!%5$$}y]]x|W%!5505%$}$y|]]W !!'7!5!%5$ZZ N$}qPP]]x|W !!'7!5!55%$ZZ N}$qPP|]] K75!5!%5$!:[]3֪k-QtXVv K75!5!55$%$][:!3֪kVXQ-qK!5!7!5!7!!!!'%5$&`ȉ)P"_=6!:[]ss1st-QtXVvqK!5!7!5!7!!!!'55$%$&`ȉ)P"_=6][:!ss1stVXQ-y:E#"'&'&'&#"5>76326#"'&'&'&#"5>32>%5$ian ^Xbib` ^Vgian ^Xbian g!:[](NE;=LTN9 A=KOE;=LSNE;C E-QtXVvy:E#"'&'&'&#"5>76326#"'&'&'&#"5>32>55$%$ian ^Xbib` ^Vgian ^Xbian e][:!(NE;=LTN9 A=KOE;=LSNE;C EVXQ-6A#"'3267#"/'7&#"5>327&#"56767326%5$jbDS4WVhjbm\Y@/Xbh`ES3VXbhZmMp[Y@1Vg!:[]$PD4KUNE;@LTNE4LRN"*,@J-QtXVv6A#"'3267#"/'7&#"5>327&#"5676732655$%$jbDS4WVhjbm\Y@/Xbh`ES3VXbhZmMp[Y@1Ve][:!$PD4KUNE;@LTNE4LRN"*,@JVXQ-7 5@pppo%5555òi ' '!]#\e#N\#]x#L   !77 ! \ݿ##N]##4 !7 7:\#]x#L]ݿ#\eL#1 4  %''' !]ݿ#\eL#1\ݿ#]j#7P~ % ! !!5 5!3!   7?~% !!3 *^V !!^*  ^V!!!^ ' '!##L  !  ##4%7 7#L4L#1 4  ! L#1#7P~ % ! !3!߆^V ! !! !ECuR #7!5!7Zxx/{xx:xu-R '!5!'xx vx:xH% 7!!7vx{/xxxƪxvH-% 3'!!'Zxx vxx$!%!!W7 r$!!!W7 $!!,7r32 &}f[_ &}f[, %$R/ %$R !2+##5332654&+!ʿ[qrqqϐђАfT$@  $ !? %29999991@&  B  $/999990KSX9Y"@&]@Bz%%%&'&&& &66FFhuuw]]#.+;#"&! 32654&#A{>ٿJxn?M~hb–m؍OH#(07#5#"''7&546;7&'&#"5>327354326=-?\g`n;) T`TeZx_958>cc3Vfa<}NV{ E..''rOs+Ax.ٴ) 3{ B333#;#"'&'##53w1ѪKsQ fև3͏oNP r>6!#4&#"#3676323#d||BYZucce22wxLj%3###3!E3A1wH33 3###%̟8ǹiEL#\ !!#!5!sP=g՚oAX` !!#!5!qjLl}e`R%sw-@ 221/053#5# !232#"MT+焀\\xEEf! !+53265##-}-MDnh %!#3!3҈R={0#3 632#54&"$\^TރQr)m`Tῆrr:T*D  # #3 3 67632#54&#"f:9:54'&'&s~&&~~ڢ~.]=@N\N\.]=zz❞zz}qa !SM!R}|pas?#-n@.  '&$ /$ .9999991@ .'& ) )./9999999046$327#"''7&7&#"4'32>s~&Ġn~ڢĠnՑꏧw֜\w֜\zvijޝzwkj!^`|g^` .@   <<<2221/03#3#3#3#):@  1/<0@22 # #3.]F; -@    1@  /<<03!#!#!"9q><@  9/1 ]@ /<220KBPX@     @     @ Y333 # # \Xds3{ 1@   <2<2??]1/<2<20%3#3#3#3#\ 7@  91/0@ BKSXY" !!!!&TdD՚ohh $@    1/<<2203#3#3#hhh7o !@  /221/220!!!!5!!o&.-ժo1/,@! ',01*$ 022122<20!"'53 !"563 676!2&# !27# '&%4rmyymrO4%%4Trmyymr4*B6!*:'(8) 6AB6 )*!6oP@   <<222<<<<21@   /<2<<22<<2203!3!!!!#!#!5!!5!!n""xxyyrr3@21/03!!!ժ,o7@   /<<2<<21@ /<2<203!!!!#!5!!5!CCPPxyr7@ KTX@8Y221/0@ 0 @ P ` ]73#3#>@ 10@ BKSXY"47!5!32654'3! $x˿ßwNetwc #/9@1E- !'E0<2<21@ 0*$002654&#""$54$322654&#""$54$32,,,,PIIPPIIPPIIPPIIPs'(@ ) (1@ #(046$32#"$&732>54.#"s~&&~~ڢ~\ww֜\\ww֜\zz❞zz}``}|``s,P@  ! #.# -9991@ ! ((-99046$32'#"$&73277654.#"s~&&~l~\wj\ww֜\zz➞ikwz|`^jI|``; -@   1@   /2203!3!#,dq9d (@   <<<<1/03#3#3#QIh ?@     <2<2??? ]1/<2<20#53#533#3#3#h+Is'+>@- )(( ,9//)]1@+(#,046$32#"$&732>54.#"3#s~&&~~ڢ~\ww֜\\ww֜\zz❞zz}``}|``s>,P@  %$#& !.! -9991@ #&$%((-99046$327#"$&732>54''&#"s~&Ġn~ڢ~\ww֜\pw֜\zvikzz|``|?l^`sr%1=G@8&,20><2<21@/; 5 )##>9//0! #"&547 !&54632! 32654&#"4&#"326sS_  _mz,,,,,,,,gs'O;H66H;O'sz<11<;22<11<;//d #@   <<1/<203!!#!5!IIjk=;;sr3?Kf@F4%+6:0L2<2<29/<<1@=(I C (7##11L9///<20! #"&547"333###3&54632! 32654&#"4&#"326sS_ ̻A;z,,,,,,,,gs'O;H6ߊ6H;OO4z<11<;22<11<;//;@   2<21/220]!!!33##!!!>ժFh);@ 1/<0)3!3;+y=@ B <1/20KSX@Y!# 5!!!8ks#O@%$!  /<<22<2<21@  /<<<2<<<2032653#2#4&##"#3"3ʊyʊy+VVF%F.@ KTX@8Y1/0!##u-s+f@- ,&'  #+ /<<<222<2<21@+*   #*'"/<<<2<<<29/<205!5"3332653#!!2#4&##"#35ʊAyʊy>FV>=VF=6-@ 1/20!3!3M-$36767#"&546?>7>5#53!Ya^gHZX/'-93B$BS #C98ŸLVV/5<4,5^1Y7:X!##:o#5!#&X3!3hXo!533oXKK'464';6;'769'96:'469&496'96;&9;6'468&456&;46';64&466'466&;:6&7;6'765'86:'56;&8;6'766&:66&:;6'76;'764&:46&:76&586&996&666&5:6&786':64&746';66&;66&866&656&9:6'967&:56&876&546&486&5;6&;86'965&986&566&686&776&::6&8:6&756&766&6:6&886&556&896&956&856&7:6&966'966rid{jXn`+v)4>@01, *$6E591@ $ *052220#"'&'&#"#"'&547673!27676323 4'&'3ft[na`zxz{n[tfCGo~[U]LKfdKJ]U[~oFCD@@DDDk63366336Fk!<@!  # E"91@  ! "2220!"$"# 33276762324rTRrƒ>IxddyI?ВP8[ 77 [8G<r&,>`&s   !3#!! ! H0x:;hLH+fabgp{ "326&33###" rhո  983#!#!#3! !9҈_:o%+kj{"-#5#"&547!#3!63!54&#"5>32"326=?/j`TeZ߬ofasP`A"..''f{bsٴ)e767!!3##!#!!&aO)p(?x4&A D+k`76765!!3##!#!![(bR-f}v(UԓR:d6T356765!!#!T:WO)fb0d+L`356765!!+!L3DS{X^}з3oP! !!+##-}) `! !!### >?h˸ʹ`3'Ps'y2qu{&Ry.se3#%3# '&76   1L  F<HqC{3#%3#"32654&' ! hJ IHn98s j&m'yryq{'yo'y.n:W '/7?GO%3#%3#3#%3#3#%3#"264"264$"264"264$"264"264$"2642+ '&' &547"#"&546;&546 676 3#J"{iihiihiihiihiihiihiihG4UU32UU4IF]97R̬\dfʬ\ʫZee̫ZҜf!!!2+5327654&#!#!qmL>87||ժFwrKK"9+32+532765||BuƣF1n!&edH08L*!!!2!"'&'5327654'&+5!#!^eicUQsj~cd\]ժ˚8+lhzy$1KKIJJ+7L402!"'&'5327654'&+5!;#"&5#533!AicUQ^cdjTmcd\[jKsբe8+lh%12KKKJN`>¨{Rg|1&'&547632&'&#";#"32767#"$546p<HmmFEMUUU8%~` !!!!#+`Ӕo{V 3 3#!+!# ! !J9҈_҈_%s%>+{'{ 5@M"326=%#5#"'#5#"&5463!54&#"5>3205>32"326=63!54&#"߬o?nQ?`TeZxeZ߬o5y`[A3f{bsٴ)Lfa' fa..''~D''f{bsٴ)hn< - 3676! ! '&'!# !  J-p;:xżP.g%H}[[Xr%H{{{"-82 '&'#"&5463!54&#"5>3 6"326="32654&y7!``TeZ*qO߬o{ǝ>REa..''f{bsٴ)nq !3!2653! '!#%{J®sv%_r\4h{{(3%#"&5463!54&#"5>3232653#5# "326=H`TeZ||Cu߬oߍo..''{fcPf{bsٴ) !!#3 3%Lj_:+{N{ ("326=5#"&5463!54&#"5>323߬o?`TeZ^\3f{bsٴ)ͪfa..''5 )!#!#333#%~gY_:gci5R{N{"-0!5#"&5463!54&#"5>32333#"326=!#u?`TeZxgƚÛ߬oGfa..''~mc3f{bsٴ)V !+53276?!#3 3%lKMJ|ثL*+2_:q?=$%2@{VN{'2!5#"&5463!54&#"5>323+5326?"326=u?`TeZ^N|lLT3߬ofa..''wj8zHB3f{bsٴ)s'{f 37!!_(^M*c37#xIS 33#!!#53ʨ_YQx 33###53YR j% 3#! '&#5376 !&'! 76;:~ ż ~HjiF wvҵCҤֆ {'23##"'&'#53676"!&'&!3276o ~~ oV?s?VLVVM{~͐~sUUu%gstgs j$. 676! ! '&'!     ':/##.;:xŽ.$#.yHH5==5[[4=<4HHHq{ 1"32654&!"32654&'267632#"'&'#",nn霜ǝ98 !#!5!)+Vy`3#\{V4&#"#367632#PQfeCBVd{#4&#"#3>32d||Bu\ ed#Ib !5!5!5!b>>I5:@ K TX8Y991@ _]0 P]3#5qeo7@ KTKT[X8Y10@ @P`p]#o+w #!5!!5Pp+ɪF #";##"$54$3@/+X 3333! +m3#mD U%3 3# # #3>:9w+: #'+/37ڷ/$0(7,48<<<<<#+ 3'<<<<< <<<<< <<<<<9̰XKRX8K bf TX30<32#4&#"#9`M1Cuȸ||MM 7BuƸ||e,'"xMfca?'Gzed\V5<!"'&76763!!32653#5#"&5#3!#"&5332765!"3ە^SWsv||CusCuȸ||WVۃ^SBWLa{fcBVfcf__{{V H!&#5#"&5332654/&763!6763232653#5#"'&=4&#"#9`M1Cuȸ||MM 7c%Zk>8nClbd||xe,'"xMfca?'Gz2XO{fcx{䟞[t`&-V 332673 &Vv aWV` v ޞKKJL[`&ASN~`6@  F991B /2<0KSXY%2767653)5!3$Wq2!dj±/8s4tVg` ##4673>=3|u˷d7<T "yX`#!5!e/я`!#3#4&#!5!2snJvy–X`35!26&#!5! #X-뒦yX4=!3!#T\[CLzl` 3!2%!4&#!Wn`–X` !#4&#!5!2nKy–X`!#4&#!+5265#5!2nã rLy–a;- 1 <05!3!----Ӫ&=&= && `&$u`&$`&$\X`&%BCZ`&&Xh`&'d`&(Q`')ZX`&*`&,&Q`&-ZXV`&.X`&/:X&0X`&2%X`&4X(`&5Vd`&7Id`&8{C!`&:nV`&;X`&<I`&=`&><t&)X&%X&/d&8X3>=3##67'#3x]GgG.i=dB`ԛ":T)C '9 '9 X& ~X' ' ' X&c ~X&c ' ' X&c ~X&c'9'9&L~&L'&&cL~&cL'I&I0a&I+p~a&I+p'x~\F&x?&,~ x&>'xx\F&x?&,x x&> (f'X >f'}D>\/&E 8>>/&F 8 (f'~X >f'~&D8\/&E~88>/&F~8 (f'X >f'2D>\/&E8>>/&F8 (f'X >f'2D>\/&E8>>/&F8 ' \ ~&P /&\I> ~/&PI>)7%#"'$47332767654'&54767;#"'&/cͷ?Ahž#62 #dGG&+@XA:g!axLn 6r'|>X %+53276=3+HZ#c,1VV,1jٻ~X%+53276=3;#"+MZ#c,11,c7nVV,1jj1,JoX&~c~X&~cpn"56$3=gi~wun52&$=Ԛuw~ig* '/&'&#"#67632O,$e5Fqp[?8WH7  $0GJI  '327673#"'&'O,$a9Gqp[?8W7  $,KJI Pq,l&fq,Pr,i,k ;#"'&=3!1,cK\WL71,\W+Ps,Pt,l't,fPu,l'u,fPv,l'v,fdw,l'w,f<x,l&fx,UL'yR&0yl9'zRl9&0z @'z>n 6&z>l '{Rl &0{'z>o&zXD&z+p~&z+pyR 3;#"'&1,cKPWskj1,\e'}9&}9X&}~X&}'~m^&~^ '~ &~&~cR~&~cR'&&cR~&cR (f'}X >f&D}\/&E} >/&F} (fX >f0%3#"'&'&'!27# '&5767"#"5$3 "(1{R=IrbJIԖ^` __&m3HZdP^vc–e4)?6 [_w\/&'&'&5672+5327676SSgURHKLXJKݣdht^#4b4bBPH:jV>/);#"'&'+53276767&'&'&5672~AI2hrBV~(;E)Kݣdht^eSgURHK 4b)N"w6a.%PH:jV# ('}?X >&D}?\L&E} >L&F} }RZ}GR &'3;#"'#"'532767654"9aRQS,cKa].-fgsT!"#?zNuIS,!&* 1p*D}'}EZ}G&L}E b&\ ~&3;#"'!5 767654x I*eK2D0# &pgM,>ꅗ:H~ b'}q \ ~&P}q ^ GF%7653323;#"'#"'&''&'#&'$473327676'&/3N0%@nS,cKvDm% I01_@8'TPxmil_Qb_y^@@$:|_2&aS,`[ F{GHܳ&%0l}=J<~ 1%+53276=3327653763#"'&'#"'&+8LcKc,P,+hm,%@n\Kf%#?70`DAbH<;!.,Pd@dczg2&q\ =!1(78#"'&'#"'&'+53276=3327653763;#"'%#?70`DAbH<)+8LcKc,P,+hm,%@nS,cKvD =!1(I;!.,Pd@dczg2&aS,`Z ' ^ G&T  &U 7&V ` <I)"'&5#&'$47332767654'367676;#"/"3276'&'&u&4-JXPxmil_Qf[+!' (s{lHX}a*=RKgL~큻%MGHܳ&%Dl7(2l^F"%GMF ,\v7Ql?[F2 .327654'&#"!"'&'+53276=36767632Ш큺%0LJNA'fKc,P-e_KUskl?[F*#=,PdrNP2T?!'Dmx+8)"'&'+53276=36767632;#"/327654'&#"JNA'fKc,P-e_KUqm*=RKg਑큺%0L*#=,PdrNP2T?!'DKH ,\vl?[F '} ` &\} 2&]} &^} b))5!3%632;#"/%3276'&'&#"@o\Dui*=RKg큻%0Pz\?c!'EMF ,\v?]DQx %3276'&'&#")5!3%6329큻%0Pzu \Duiʸ?]DQx\?c!'Emx))5!3%632;#"/%3276'&'&#"8 \Dui*=RKg큻%0Pz\?c!'EMF ,\v?]DQx'}Rb&d}R&e}R&f}Ru *du %+! '&7.54762;# '!2764"[b=D}a_[9^DU)k_1ocz2t*n@00@p[C+ @Mkl=v8`3$*727&'&5763"327%+5SF7J \X];d}M4F!Ť$/%+532767&'&5476762;#""654'v`kB;(aD hYYh MXD=p`vʨ4/gg/($'UZ'-)74--47)-'bM,(U __ u F'}wdu L&l}F&m}wL&n}'}~\L&}?&}~ ~&}kG'~R~k &~k?&~,~ ~&~8i!D#"'5327654'&'&7676'&'$54733276763;#"'J&P DfXRNB8D-<9_h$$EB|=Q#!v+6(  %{{qe))5!27654'&54767;#"'&/66-62 hGG&+@XA:g!a_h$$EB|=Q#!v+6(  %?+)x.j#$%653;#"'#"'$&733276N1,cKpNyUcE@A(IPmI~jkj1,3.(B"[\ss~B"5 +5327653WPKc,1se\,1j%+5327653;#"SMKc,11,cKVV,1jkj1,^kgt5%327654'&'&#"#"'&#4763&547632;#"bzL,5;(.;D K2KxAZM\HT((&iK*9:X DD(PNNOmf7*(?$GC,,m$%#"'+5326767632%327654'&#"dan@ht4W^Q[a>/4(*X.[4fb0G1P8TYNE5EK&)/4:''5)24fb0$#1P8S1>,E5EX !a%H'}?  +&}?&'}R~'}Rm^ $&'&'&'3;#"'&'#"'&5476 xRot$8pKZI-&8:m*12e CY>)2'+eO,3;I0D-=67654'&#"27&'&5476&'5#"'+5327654'&$"':A4N--0M,Q@(Jxb 41}! @H=.%4-+#%v iEN@TSZ 'D49g=ql)D%'i.C!v-3j  ;AWE L9P)8K6(S/VL_+Y9K1\SJo765&'&'&54767632;#"'&#"#"'$4733276L[/,4PT*uW ##rpl$-AIqYhu?AB[M!3!+ (;=A<^ĸ#0{bV` )gZZrN J'~ o '~ X&~c~X&~c.&y,.&y,&z,&z, &{ &{T#"'53273676537M͞jK`Uq%BUG FA+7T#"'5327367653;#"'&4;IʡjK`Uq%"@Pif<[A FA+7DT)TL* 35'5467676?67654&#">32,X\"$߸g^aOl39ZZ8L{4<+VZ@EL89CFnY1^5YVe !5!5!)5!S2SR7'XF: 'b:= ']C; '<b= ']bH'&'H'''H' ''H'&'H'''H''' H&&'H&&'H' ''H''&H'''H' ''H&''H&''H' ''H&''H&&'H&'' H' '&H&'' H' ' 'H''& H' &'H' ' 'H' '&H' '&H' &' H'''H'&'H''' H'''H'&'H'' 'H''&H''&H'&' H'&'H'&'H''' H'''H'''H''' H'&'H''&H'' 'H'&' H'' 'H'''  H'' &H''& H'''  H'&& H'&& H'' & H'&'H'''H'' 'H'&'H'''H''' H'&&H'&&H'' 'H'''H'''H'' 'H'&'H'&'H'' 'H'&'H'&&H'&' H'' 'H'&' H'' ' H''& H'' &H'' ' H'' &H'' &H'' &  #3 !!#!]W:\w98qq+_N  %*!2#!327&#363&#!3654/654'f;33;$ $#>]a{w DD663! )327&#!36'hPcp~qAA k{qS3%!!!!!!-x9vq dddsd !!!!!#3#oQn.ddqs&&$#"32767!5!# !2deVRuu^oRaG@;@&5dSUmnHFcIf3%!#3!53#.nXddddq dddd fY6765%!#!53265-V?O?nqd J^ dd0 !3 #!3pdw@1q 2 !!!3ddo o !#!! !3!3_Gbn}qR+q  r'( ! '&76 7& 676'&&:żGlllli$ #ab2222jT%%5$c$-6&/.4%&  %5 64&/.$ Pdo&nŢmngzoʷ-[ʚ)'NXd''pui$2Xf| / 3%!!!!rpq ddq $!&%! 65! X!!Y fqba@`|gd5\*$ 3%! 3!dq d+D 3!3%! ! 3! !D5D:9:9d|q  d+l 3%! 3 ! #(\~vbL:H|dq d22{ 3!! #3ndp29V{{",34&'3!5#"&546;54&#"5>3 5#">76/=Kd?Vu`Tw86/^b;:gCzӆ]YfaH..t''UNHGgwt-!>32#"&'!4'&'676763&#"327N:||:^,<<,9RKM_]daadt= z =OsKTdihtJq{#%#"!2&'&#"3276%M]-ULEmGJXHCQRHV,${z$d$$>:##dWS%&-!!5#"323327654'&'&#"N:||v9,<<,^(]_MK^daDDaZKsO=  =Td6Jthio}{!327# 32!.#"}K_mk)#i̩J@b]u-)8 CqzӾ/ 3476%#"!!!#5354763g.9:9|WX -8J_D8d97ddddTVqV{#.=65326=#"325!!"&32767654'&#"jlQR:||:Nry^,<<,9/KM_]=ʌo,*qdaDDad-w=  =OsKihtJH "34'&3'!>32!4&#"! GS5‡OIƁkk h@[:Lded\ПU5 33#!!JKOhV #676#532765!3#%G(=1l$%OQRaеT0Hd01``2 !3 #!3OHіmdi#L&5#"'&5!3J=(G%RQOLiH0T0Z``~J^d{"&1<!>32>32!4&#"!4&#"!3%34'&%34'&OIƁԝTށkkkkd[ GS5 GS5`edJv\П\ПUh h@[: h@[:H{ "34'&%3'!>32!4&#"! GS5‡OIƁkk h@[:hded\ПUqu{ #2#"27&"676'&s3x33x3d4'pp'3(pp({98  kp-$-R-ۀ-qV{-%!!>32#"&4'&'&'676#&#"32N:||9,<<,^؆]_MKdaaKsO= z =oHJthiqV{-%#"325!!3#32767654'&#":||:N<^,<<,9(KM_]daDDad=  =OsK2HHihtJ{3'!>32.#"!N:4I,hdfc˾zo{E67654'&/&'&5432654&/.54632.#"#"&'i'K&'q4=B%%U+.39GSOjqL4vfLJ\_opPx3Zl=vf03"3;@{R?Bsl37'*7CoT78^UNO,, z1$YXDL#/%%7%&7#!!;!"&5#53*\{KsբjU|7N(dUNdudTD` "%&'&5##!5#"&5!3265! GS5CIƁTkkTS hl[:hded0=` 3%! 3!YT^^d\hdTV`3!3%!!3! !bTNdhhdjjjL` 3%! 3 ! #U|p|[hd-s=V`7%! 3+53267>^]_lP|XQ+ۙdi8{dCYXb` 3%!!!5!\vwhddhddh$%s'&'(#)s*;+f-j.j/031s23s4T567)8h9D:=;;<\={-{DEq{FqZGq{H/IqVZ{JdKyLVyMN9{Pd{Qqu{RV{SqVZ{TJ{Uo{V7WX`X=`YV5`Z;y`[=V`\X`]   6/&"27 d3{44{3s s#Տ0,-k37!!5!5%6bJJgq ddd HdH(7!676'&'$32!!7676&#")`"LlDbZE0Q](=ymd͕@9\9pd9hbiddAbs$*0"'5327&+5327&#"56325654&'>54+!ĪeO6?;2:L uWEdJj D d <h@Ѳ|!ŐUl$yXZ#3 !!3#!!5Qpq3d\#66'&#"!! !"'532gd1jKEн܁\`I Kd# F32v cSRav 6978w{z9 j@ VV120K TX@878YKTX@878YKTKT[X@878Y332673#"&v cSRav 6978w{zfGd10KTKT[X@878YKTX@878Y3#@1<203#3#䙋N#!#ęę53#73#'3# 3#3#'3#}}d 3#3#'3#}}d3#3#d 3#3#3#3#dd&;#"'&'#"'$&733$767654'3F??7KX~X\,>%!$'$&73!2%7&'&547676323!!"'654'&'&#"xhn}@AQ+"R:4RQP ioh4"(=)1$+<'g\^sM6,|y$K2S%jAzG' <8BN?0654'&323276767'&54767632#!V)B,4((7(*HTO<?aNbNLZB`.NJ|m+M;3*)3P& ]027EW4,E$2Hf3Џ,' !5;#"'+5327&'&54767632"67654'&'&f$'و'$A??8 D?$ 9P2*I1C299(M.L,0W 5+5DE2.4! k .@%&'&'&547676323!!#'$'&5473!2766'&'&#"B.y9()Wp8c20-=^E>><l/"'"3 9Ld/  #+m=E2X:zFNV}`kL:DbZzWK# :<,; ? &}R~&}R %4'&"2>"'&4762<R8R8z?@?@@?@(8)*8@@@@@?? '~'&'~cRP~&'~cRP' &cL~&cL >&Dz8\K&EzX>K&FzX >&D?\F&E >F&F  >&D\F&E>F&F >&D'}?>\L&E'8} >>L&F'8} 3_+ 5__bV'J@!B  6991/<2990KSXY"]33+532765#ոRQi&&}``01}` 2@  F <<221/<20@  @ P ` p ]33###53ø`<ĤV.` 54!333##"3276!5R w{i&V`p?`3A0c3'q=rUa4'qr[^3'zPq=cZ'sdrUcZ'udrUaZ'sdqaZ'udqvj 3't\q=cZ'wbrUvj V'r}t\cW'wuz|vj0Z's@dt\c:'vus(Dcm:'uDvuvc u'vutvV Y'yPtpVZ'yPsdVZ'yPudV'yPc['vuPj&Z,,!!,,O=32653#"&[hq`=QQ, &&| &3;#"'!5 767654x I*e2D0# &pgM,>ꅗ:H~#'|`'|S'|SF'|8@'|+ '~c~@'|+ '~c ~r'|>P9 9F KSKQZX8Y1/0@  @ P ` p ]3;#"&5Li a^q%qqu `&JOw`73#!!dž$Nd`Vw`#676#732767!5ʆ#5H2K1i0/N)deеT0Hd01``vg`'`&3#3## +@     22221/220!#3!53#^ժ ?!5 ?8'qXw8 U'rXw8'wq8'tXw8 U'uXw8 'w,t$'tz$'uzN@ T1/0333N@T 1/20%3!533yոBy@ T1/0)533ysոBq8@ E EԶ0]991@  /0 6& #" 3 *NYh> éA@E E Զ0]91@    /<20 6& "'!53&54 3 *NNJhh> é!8@ E EԶ0]991@  /0 6& &54 #"'!5 hYNJ>z=x 4@   2291@  /290)33!x³j*]Qix 6@   2291@    /2290%!5!33xtj³瓓]Qi' 4@     2291@    /290#5!33j³]Q=q) #33mCq"q )5333!mm"q)533#mOq $@  1/2<0)3!33OkUq""Oq (@   1 /22<0)533!33OιUΓ""q $@  1/2<0)533!3kιU"Oq $@   <1/2035!!5!3ΓK"Oq $@   1/20#5!!5!3ΓK"q @ 1/0!5!!5kqKq:@!E E ܲ@]ܲ@ ]1@  /<0!&'.4> !2>4."RJr 惃sKR9[ZZ 1ũbbŨ1 p`88`p`88!>@#E E"ܲ@]ܲ@]1@  /2<0%!!5!&'.4> 2>4."RJr 惃sKRQ[ZZ{ 1ũbbŨ1 p`88`p`88O:@!EE ܲ@]ܲ@]1@  /<0#5!&'.4> 2>4."RJr 惃sKRQ[ZZ{ 1ũbbŨ1 p`88`p`88O &@    21 /03"3#!5!>k fO "  21 /03"3#!5!>c f $@   21 /03"3#!5!pk fq7@ E<21@  /<20!!##"&6 !354'&"3.Cf^v ]8mr^<Uf"qɃ]8ƃ;@! E <21@ /2<20%!##"&6 !3!554'&"3.Cf^v7]8mr^K<Uf"Ƀ]8ƃ7@ E<21@  /<20%!##"&6 !!554'&"3.Cf^v]8mr^K<UfɃ]8ƃ ,@   <<1@  /03!!!!!55Փ/ 0@   <<1@   /20#53!!!!!55B/D ,@    <<1@  /0)53!!!!ys55B/= ,@  <<1@  /0!!5!!5!355ߒѓ 0@  <<1@  /20#5!!5!!5!355ՓLѓ ,@    <<1@  /0)5!!5!!5!,55Lѓ *@  <1@   20!!27654'&3!23,R4,,=ٹUiXO]Oz}I_"_Ҥ.@  <1@  /220#533!23!!27654'&ιUiXO,R4,,=B_Ҥ]Oz}I_ *@  <1@   /20!!27654'&533!2#,R4,,= ιUiXXXl]Oz}I_"B_ҭ@@  ܲ_]9@  /999@  10!4'&'5!!5Mc4B_9V@9D@   ܲ_]9@  /2999@  10#5!&'&'&'5!! 5Mc4BX]9V@9$@@   ܲ_]9@  /999@  10#5!&'&'&'5! 5Mc4B X]9Vq=:@   91@ /̲]촍]0!533T9 >@  91@ /2̲]촍]0#5!533hՓL9 :@  91@ /̲]촍]0#5!53hL9+#1@%!$1@  #/2203432>3234&#"!4&#"!}x5%^qZHZlK--Xh|ŕnc%5@'#&1@  $/2220#53432>3234&#"!4&#"!}x5%^qZHZl[K--Xh|ŕnc#1@%!$1@  "/220#53432>324&#"!4&#"!}x5%^ZHZl[K--Xh&|ŕnc= -@   <<1@  /<<0!!5!3!!!KK?1@   <<1@  /2<<0#5!!5!3!!!KK? -@   <<1@  /<<0)5!!5!3!!@KK?=X>@ <<<<1@  /2<<<220%!!5!3!3!!!=KøL??XB@  <<<<1@  /22<<<220#5!!5!3!3!!!%!KøL=??>@  <<<<1@  /2<<<<<0)5!!5!3!3!!!0KøL=??Oq %@   1/203!3!$Uq"KOq *@    1@  /220#53!3!$U"Kq %@  1 /20)53!!kUޓK=C  1@ B/0KSX@Y!!!tFs0hB~ F  1@ B /20KSX@Y!5!!!tFlhhB~BC  1@ B/0KSX@Y!5!!tFlh0B~B+ 8@!  <<1@    /2<20327654'&+!!!2/!m]%i ; @ED\qQE=4."RJrCEoJRXErrJS9[ZZ 1SV/ { 2Ʀ1 "p_88_p`88*#5!5&'.4767675!5!!2>4."RJrCEoJRXErrJS9[ZZ 1SV/ { 2Ʀ1 "p_88_p`88O(#5!5&'.4767675!5!2>4."RJrCEoJRXErrJSQ[ZZ 1SV/ { 2Ʀ1 {"p_88_p`88Q %@   1/0!!#!3BQ *@  1@  /20#5!!#!3ԓ} %@   1/0#5!!#!+Q (@   <1 /0!!#3!3OQ -@   <1@   /20#5!!#3!3ԓ} (@    <1 /0#5!!#3!B /@   <<1@   /20!!!5!3z;  K"qѓB3@   <<1@  /220!53!!5!3z;7 K"ѓm /@    <<1@  /20!53!!5!z;7 K"ѓ+q &B@%(E# E'ܲ@ ]<<ܲ@]1@ # $ /<<02>4."&'.4767673! [ZZRJrCEoJRXErrJS"p_88_p`88~ 1SV/ { 2Ʀ1  (F@ *E#'E)ܲ@]<<ܲ@#]1@' (/2<<02>4."!5!5&'.4767673 [ZZlRJrCEoJRXErrJS"p_88_p`88 1SV/ { 2Ʀ1 O &B@(E# E&'ܲ@ ]<<ܲ@]1@ #  %/<<02>4."5&'.4767673!5 [ZZRJrCEoJRXErrJS0"p_88_p`88 1SV/ { 2Ʀ1 {q*!&'.4767675!5!!!2>4."RJrCEoJRNXErrJS9[ZZ 1SV/ 2Ʀ1 "p_88_p`88 ,%!5!5&'.4767675!5!!2>4."RJrCEoJRNXErrJSQ[ZZ 1SV/ 2Ʀ1 p_88_p`88O*)5!5&'.4767675!5!!2>4."0RJrCEoJRNXErrJSQ[ZZ 1SV/ 2Ʀ1 p_88_p`88 '' '' '' '' '' '' '' ''  :@   @ ? o ]9999991 2<0#'##'##'d2222222dddddV!#!3!3#3jժVV8`!##333#{}`9VVX` %5#"&5332653!"&'5326Cuȸ||aQQRjBfca{+,*X10!5!-ЈX'3I(sInhX#'3h'OW`4X#'3v5]dDZX#'3 |;d07!X#(ẌI$@h$An4$B`$CnhX#7OhWh$Eh@4AnB`4X#7]vDdn4$J4E4@dAdZX%#7d|!70`$OnJEd@0<0^X133ֈXJ=]_<QQ3 r Um Q rZf55q=3=dd?y}s)3s\\?uLsLsyD{={\{fqqq/q999qqJ+o#7=V;=3X55^R\sd5^5bs#5`b?yyyyyys\;\\\3 LsLsLsLsLsLf {{{{{{{fqqqqq9999qqqqqqH==y{y{y{sfqsfqsfqsfq)q3 qqqqqq3sq3sq3sq3sqTx\9\9\9\9\9r\9?u9u9uuFLsqLsqLsqs/qJJJ+o+o+o+o#7#7#7DV={\3X{\3X{\3X/ }}ssfq3 }qqLu3s~\ 9 =LsNgvsq7r+d#7#7N={\3XTT\h3qT]hX\] ` d <qKsday{\9Lsqqy{y{{3sq3sq?LsqLsqTX9 ` d <q3squy{{LfHy{y{qq\9\9LsqLsqJJ+o#7,Gqqq{\3Xy{qLsqLsqLsqLsq=79qqy f u +o3XPP}  yq\9@sq J qefqqqqq|SA4Pq9qq q``9t*KL:+#qqGpPPOJI>>t+o7#7#7q=V=f3X3XXmXXXXLsPqq;VVqXXqvqq77:7/ <66JO<u1ufu]H^H 6&:uuuuuu  3s3soouuuuddLhuTzuuu%q7]yq$U $ zw(j#Lcxh!c+qc3x+x.pppp*pw<.::3efqesDy}uy{\Ls\?yLsLs{=LsN\FqSFq qSZkq=xJvkqJqqdGp;Gpq?qWWGpAOpLsq0q@GGrwxssFqU-~Od$s6sq,J7Opfq9Lsqs5UsssJs\\\T\J#y}}@e(!TLss#y{=6|<}o{p4kq5FA33L ;q;fq<=p;rR>Qdqtqq/4dq+o9998L07/3=;xs*` D3 GLsk7sS[2Lsq@R2@R2s<qsq pv9xssfq;XXX.j}!&4fG8=(5F!A!=2*ISsqsfq<=={=;yt|||\(5F?56].I6r|29y{y{{qLuqLuq(5F!ATX33LsqLsqLsqodq#=#=#=|4QfG8{=;{=;}q -qn6.3sGq/STL ZTL'AtLsqDVT>LLXv&tuA&7\\S&eLsR\Lsuu^x"6^Zq"qDq;' qqF92 F &q/qzw`DDcc/NDdc\\fcXCX.X0.XsXXEX.XXN*CCMXwBS(?99l9lC91***}} ffuuXK5k1CCOOLLLRLLLLLUL<L<LdL\W5kVz*******KKK))*CC1LLLRLLLRLjLL<L<Ld9qd==;;q;q=x==D=;==p==q==.qqB[B[{d{d7]xlxr[")>WE_HHY"~h~h@rx2OsN~sxMn`P{P@@@@`NzBza\d>N c c]ccY]dji:~:~PZ"ZPZ|ZPZ}PPZPZXZPPP|ZPP*FZnP\ZZZFdZWPPWFZdZddDPGd.d#ddadd%dvd-d/Cd$d/dWd/?d1<Nd/dBd/d-d-d/d/F.Z#d{ddddddd.ndmdndyyyy'''''w'w'ww'w Xc^c^%H Ewyyyywwwwwwywwwwwwwwwyy^^^l4wl4w4y4yywyFFFF*F*F*FAA8F3F3FFFF*F*F*Fzzwuuuwwwuu&w&w&wwFF wwwFFGwyFyFFwFFFwwwFFGwy=Fy=FGwGw=F=FwFFFFFV+V+FFV+V+VY]YFFF"F"F"F"FGFGFGF F F F F wwww?w?w?wYSSwSwSSSFFFFYwyyyyMMwwdwSSyy4yFwwFFww```````FwFw     FFwwFF%w%!%!%w%w%!%!Y )#su` z    s 4 s 3  E p 2 O 3wq= {>fq$S9( 3qfyqyqy3/qqq222</=V3X5x=2ZLr u//SH||NYHG p+"M"M>G/Mmu>GVGVGTR>GnzhuuEuOGGOGOGmu\#=nnuV&7yGSG%nzu=nV&7yKySG%t9>GGGOGT_>G=nIzIIVz[quuIuEqOGOGFK\#^YGu@zV&7~77#7OG[[[[BBy{}}}sfq)q)q)q)q)qqqqqq/3sq\9\9???uMuMu9u9LsqLsqLsqLsqJJJJT+o+o+o+o+o#7#7#7#7y=y=DVDVDVDVDV{=;{=;={\3X{\3X{\3X#V={/&qy{y{y{y{y{y{y{y{y{y{y{y{qqqqqqqq\Z9D\9LsqLsqLsqLsqLsqLsqLsqNgvNgvNgvNgvNgv====FqFqFqFqFqFqFqFqyy'iSSSSSS0l7hx qqqqqqoE.k_FqFqSc<qqFqFqFqFqFqFqFqFqyy'i7hxk_FqFqFqFqFqFqFqyyy<pr\\D~{aNsVddddd%%%%9933W q q(()((()( 33?nn=V`Jd=n=dn8N(ffadp5Wnz5?5f5\5l5Y5S999og0u5W55^5b5?5f5\5l5Y5S999og"MVGOGuVGVs`u .;F_( ..D]1u!===P=&C&Cs#&<<oI HZ;jDN hR6nLsbBSV,y('y\XNND?yJ\}WJT9hgd(V FhZ $<|3uuWZ[O=;6Q^^b?fbfl\bya W{=w= =us)9~=}== ]=;;;9fqq y) ysedud    du,dudududvdvdd*ZZd-Opdduudwddxvxddddudud  dududuku7^H^^^@^^^uzz^uwududdud7u7y#_ZZ,dDX===,,ff+uPuuu+uPuuu+u+u+uyyy``>>**yyby*cc| a aXXJr;xxdxxd++* 8 8 P 8 x PFq 8#+7',,,,,,,,,,xxxxxxx||''''''''''''''''''''''q''''''''''llgg'''''''''''''''''pprppppppppp7p7Tpp''''3'''ppppp'''',h,d,,,,+,}}_}} ,,,B,d,,,,,,,,,,},,,dZd2E\,,,,,,,,,,,,,,,,,,,,,,,S,,,,,],,,,,m,,E,,,,A,,,U,,Q,0,,,U,,L,0,C,,X,,B,,X,,,x, ,,,,,,,,,,,,,,1,,,,,,,,,,,X,X,j,, T},y,},),,,,,d %6  dT YxEVIVVx+5X3ppppR >pTVSTWW/V0/0002p@TTTTpnnTVaaTT,f,z,z,z,z,xNNx>NnX~#9Uwlf,,,,,,,,,,                    uuuuuuuuuuuuuu++<uusumOss[YOO Bu xd xu xd xd xu xd xd xu xd xu xu,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,duwOwO::: u+u+u+u+u+u+u+u+u+u+u++u+u+u+u+k  77^^  7^uuHH''''$$"pMMu 9 u H#?{\3X@sy= DVh<GpPqbfr ,qssu@xC@~yyv{\{\ssg)?>8{\(oo:o\:o\csssss$d{=syNsNs6??,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,r+d pv9;<@>speKkT5L mLsqsq s&q:Bz<<|ff7S+o { { #{{{{seq#Sjxt  s&qu 9553wF\ Dq/ / ///}/o } <.VN1X?,XXuXXwwwwXCX.QX0QXsXXEXXXCMXwB.XsXX:j:j:j:j:j:jKH KH ************jj))k))k":jC:jp*XXXiXXXXXXXXXXX9p9lpl"9lplC:j9p:j1J:j:j*********}3}}3}jj 3# 3#  f^f^uBuuBu/KH 5kk kpSI:j1J8"CC:j..TT4,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,c3s$f"=3LrDrlK{fqo/q5 "qqq+o7=HVhL=Xy}s)3s\?uLsLsyD{={\{fqqq/q999qqJ+o#7=V;=3XkZqAjds N:jH k :j:j:j********_9xxxxxxxxvxxvxxvxxxvxvxxxx,p:jj9Jqq9O99:::qd=dd=;;;;;;q;;;q=xxx==D=DD;;;====p==q======...qq,,,,,,,,.jn`#Zn`nn`n#Z`n3>?@<@<@ABPChDDFFGHIIJKPKLLM$MN0NO@OlOP$PQ|QQQRRSS0S`STUUUUVV8VPVhVVW|WWWXX<XlY4ZlZZZ[[[\\]]4]`]]____``$`<`T`x`bLbdb|bbbc chdde e,e\etffDfpffffgg,gDghggggghh,h<hii4iXi|iiiijj,jPjtjjjjjkk@kl lllm m0mTmxmmmn$nHnlnnnnoop$p<p`pxpppqdqrr<rTrlrrrsxtt@t`ttttu|vvvvww(wPwhwwwwwx$xHx`xxxxyLyzz4zdzzzz{{,{D{\{t{{{||$|<|T|l|||}}~~t8X|T`P0\t(@XD L@  8Ph0Pt4D\t<Tl $<Tl,D\t4Ld| $<Tl4xld| $<TlX8$ t,l,<äPňH,ȸhʤ<̌(x\ 0@\(԰ՔLLڐhܜX(ddL`lt` 0THX \hX, 0  T   < L d t   $      T $,@`|8L`|PpHT\,T(<Pd Lh@h<\ Dh$8  <    !!!,!@!T!!!""("t""##<#####$$$,$P$x$$%<%%&&(&(&h&&&&'L'`'t''( (4(\(l(|(() )<)L)\)))))*0*`*x*****++(+@+P+`+,,,$,4,,--h-x---...../t/00011(1@1X1p123t3456L6778`889:@:P:;`< <==>L?X?@@@@@@A8BBChCCDdEEFxG$GHdHtI$IIJhJKLMHMNOpOPXQ QRPRSXST`UU(U8UHUV$V4VDVTW$WX0X@XXXpXY(YYZxZZZZ[P[\P\h\\\]]]]^T^d_X```aabbb bhbxbbcccd8dde\eefhgghphiHijjjklhlmmnnnno@oPo`ooppqqqrHrrsLstu$u<uvvw<wLw\wlw|xxy<yTylyyzz{d{||}<}|}~0x<Tlxh4\,TXt8Ph<\8, ,`$pl84$`<Lx Dhx(8Ph,D\t4t$th8|8| 8DTdd0t ,<LP,\h x¨@tXŴTƜǤ<,ɠ<ʬ(˔̀(8ͬ͜dt$4P`ѴҨ4d՜ ֐׈$؜xl|h(8߈ߘdt\pH`L,T<dt,HtHh$Xx<,(08P,LH0Ld|,D ,D $@Lx\,HPdx D  D   D   @    < @Xp 8Ph @XhP4DTd@ dP\T@`<0h  \ !,!""`"##$T$$$%L%&&h&&''T''( ()*X++,\-8../T0012234845647789H::;>0>??|@@H@A(AB<BBClDDE8E`EFFFG0GHHIIJ@KKpLtLMNO\OP(PQQ|QRhRSSTT|ULUVVVWHXXXY(YxYZLZ[D[\d\]]\]^p^_$_`@`a ab bcctcd de(eef\fg8gghLhiHij(jk kkl@lm$mn no$oppxpqdqr4rrssssst0tHt`txtuuu0uHu`uxuuuuuvv v8vPvhvvvvww@w|wwx0xxxxxxyy,yDy\ylyyyzz(z@zXzzz{{{4{L{d{|{{{{{| |$|<||||}d}|}}~~,~~~~~ $<TlpLdx(@Xp0H`x t(@ 8 8Phh(@x0H`x 8tp @XpP8P(@ $<Tl4p4L(@Xp0H`x|`x 4Ld|h4L4Ld| $<Tl$`80,Tl$< 8 @X d|\t $<Tl48 8Ph 4L \t80H`x@,h0|\ d(HL€¸P`Ĩ0 0hxƈƘƨXȤX8Hʄ$˘˨˸8Hp̀ l8hΘ8Tτϸ@pьDlҰhX0Քp׬<(XDۀ۸\ܨpݘ0޸4ߌߠߴ`tX\pD\,,  pP Pd8X,D\t4Ld| $<Tt $<Tl,D\t$<Tl,D\t4Tl,D\t4Tt4Ld| $<Tl,D\t4Ld| $<Tl,D\t4Ld| $<Tl,D\t $<Tl,D\t $<Tl,D\t4Ld| $<Tl   8 P h        4 P l        , H d        8 P h        4 P l     4Ld0H`x,D\t $@\x $4L\t $<Tl,D\t4Ld| $<Tl,D\t,DTd,D\t0H`x0H`x0Hdt(@\l,,,,,,,,,,,,,,,,,Xh,D  t !,!`!"<"|""##T#l#l#l#l#l#l#l$&&(&@&`&|&&&'H''(((()X)))* *<*l***+$+\+++,,,4,X,,- -T-|-..,.h../8/8/8/8/8/8/8/8/8/8/8/8/8//01@112343h3334 4444445 5 545H5\5p555555566$67H78899:l;<;>`>@D@AxB BxCCCCDD@DxDDEFGGHDHIxIIIJKL4LLMN0OOP PPRSTTTUdV|WxX(Y@YZZ[t[\\t\\]]]]^_` `adbDbpcdde<ef@ffghhhhiPiijj0jkdll<lm<mmmmn n,nLnlnnnno o$o4oLolooooop pp4pTpdptppppppqqq<qdq|qqqqqqrrtttuuvdwwxzz8zhzz{{`|||}H}}~(~t~~@LH8pd 4L 8TpdL4t4,l,l$|d8t<|4`4xpX |4pDx80(0448L\<\,`Lx ptT,`p4`HPt$  @` XLdüĈŤ,Ɣ@DŽ x ɌxˤX̜p4lϠ,М Pєlӌ xD\ռ@t֨tDڴۜ܀@DހX|ߤL|<Xd0X,`<<p ``TP$\h,xh4 0 d|P0 h@tP<\| 4\|4Tt$pt,   , < L |   D `    $ D `      p    T   $Px| |"X$$$%%4%h%%&&L&&' 'D'h''''((@(`(((() ),)P)x)))* *P*|***+ +L+t+++, ,L,x,,,- -H-l---..@.l.../,/`///040l0011D1x112202\2223303d33344H4|445585l5566T6667,7\778(8889 9T9t999::$:@:\:x::::; ;H;\;x;;;;<< <<$>>>>??h??@tA AB8BCDE EEFF0F\FFFGG0G\GH$HILJKL,MN4PR STTUXVpWLXLYdZ0[\\t]^$^_`(`abbcd|ddee0e`eff<ffg g<glggh,hXhhiilijjXjkktllplmmnnoxpq0r\rsHssttLtttttu$uxv<vwxyXzxzz{T{{||d|||}},}@}\}x}}}}~ ~(~L~p~~~~ (Lp,P| (Lh@l8\(\ 8l (Lh@l8\(\ 8l@l@l,`LxH| \ (Lh@l8\(\ 8l@l@l,`LxH| \@l@l,`LxH| \,X \,`H| \LL(txXP4|hLtXpLhlP4Ld|`@Dd$Ì,hĀĘ$lż4ưD`ȨPɰXD$Τ8πXд,XфѰLҀҸ$P|Ө0`Ԑ<`՜4p֔X$`l| Lۀ۴Lܜ ݐPxޠ l0\|$ | HD p8DT0D(h4p<L@<Ld| $<Tl,D\t4Ld| $<Tl,D\t4L\l|h0H(x`< d      d   T d   ,T|4X| L` P,dTth LtX8Xx<`!$`$%&(&'(H()*+4+,-H-`--.$.|../ /`//00T0l00000011,1D1\1t1111122242L2d2|222223 3$3<3T333344,4D4\4t4444455545L5d5|555556 6$6<6T6l66666677,7D7\7t7777788848L8d8|889P9`9p99999:X:p:::;;;0;H;H;H;H;H;H;H;H;H;H;H;H;H;H;H;H;H;p;;<(<<>,>D>\>t>>>>???0?H?`?x?????@@ @8@P@h@@@@ATAB@BXBpBBBCC,CDCTCCCCDELEFFF4FLF\G8GHhHHHHHI\IJLJdJ|JJJKDKL,LDL\LtLLLLLMMM4MLM\N(NOO,OOPP(PQ8QQQRR(R8RSlST TTUTUlUUUUUUVV,VDVWWWWWWWWWWXX4XPXxXXXYYHYpYYYZZ<ZdZZZ[[0[X[[[[\(\P\x\\\]]D]l]]]^^8^`^^^__0_X____`$`L`x```aa@alaaabb8b`bbbcc,cXccccd dLdtdddee@efffggghh\hhij jXjkkpkl lmLmn`no@pp|pqqTqr\rs\sttuvv|vwwlwxxx x0x@xPx`xpxxxxxxxxyyy y0y@yPy`ypyyyyyyyyzzz z0z@zPz`zpzzzzzzzz{{{ {0{@{{||}}\}~X~p ,<L\l|(<X D|t,| $<Tl<\| l,Hd (D`| Xp $Dd|(TXt4lt |DpdD@L$@\`pdl |LhtDhp|PPtT0DX| 0Tx \8\(<Pdˆœ°$8T|ZT+h >2   : `   (Z4;b ;; 0    " F m " : %: h: ; ;Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. DejaVu changes are in public domain Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. DejaVu changes are in public domain DejaVu SansDejaVu SansBookBookDejaVu SansDejaVu SansDejaVu SansDejaVu SansVersion 2.29Version 2.29DejaVuSansDejaVuSansDejaVu fonts teamDejaVu fonts teamhttp://dejavu.sourceforge.nethttp://dejavu.sourceforge.netFonts are (c) Bitstream (see below). DejaVu changes are in public domain. Glyphs imported from Arev fonts are (c) Tavmjung Bah (see below) Bitstream Vera Fonts Copyright ------------------------------ Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is a trademark of Bitstream, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces. The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Bitstream" or the word "Vera". This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Bitstream Vera" names. The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. Except as contained in this notice, the names of Gnome, the Gnome Foundation, and Bitstream Inc., shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from the Gnome Foundation or Bitstream Inc., respectively. For further information, contact: fonts at gnome dot org. Arev Fonts Copyright ------------------------------ Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the modifications to the Bitstream Vera Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces. The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Tavmjong Bah" or the word "Arev". This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Tavmjong Bah Arev" names. The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. Except as contained in this notice, the name of Tavmjong Bah shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from Tavmjong Bah. For further information, contact: tavmjong @ free . fr.Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. Glyphs imported from Arev fonts are (c) Tavmjung Bah (see below) Bitstream Vera Fonts Copyright ------------------------------ Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is a trademark of Bitstream, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces. The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Bitstream" or the word "Vera". This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Bitstream Vera" names. The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. Except as contained in this notice, the names of Gnome, the Gnome Foundation, and Bitstream Inc., shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from the Gnome Foundation or Bitstream Inc., respectively. For further information, contact: fonts at gnome dot org. Arev Fonts Copyright ------------------------------ Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the modifications to the Bitstream Vera Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces. The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Tavmjong Bah" or the word "Arev". This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Tavmjong Bah Arev" names. The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. Except as contained in this notice, the name of Tavmjong Bah shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from Tavmjong Bah. For further information, contact: tavmjong @ free . fr.http://dejavu.sourceforge.net/wiki/index.php/Licensehttp://dejavu.sourceforge.net/wiki/index.php/LicenseDejaVu SansDejaVu SansBookBook~ZZ  !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghjikmlnoqprsutvwxzy{}|~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~                           ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~                            ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~                            ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~                            ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~                            ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~        !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ sfthyphenAmacronamacronAbreveabreveAogonekaogonek Ccircumflex ccircumflex Cdotaccent cdotaccentDcarondcaronDcroatEmacronemacronEbreveebreve Edotaccent edotaccentEogonekeogonekEcaronecaron Gcircumflex gcircumflex Gdotaccent gdotaccent Gcommaaccent gcommaaccent Hcircumflex hcircumflexHbarhbarItildeitildeImacronimacronIbreveibreveIogonekiogonekIJij Jcircumflex jcircumflex Kcommaaccent kcommaaccent kgreenlandicLacutelacute Lcommaaccent lcommaaccentLcaronlcaronLdotldotNacutenacute Ncommaaccent ncommaaccentNcaronncaron napostropheEngengOmacronomacronObreveobreve Ohungarumlaut ohungarumlautRacuteracute Rcommaaccent rcommaaccentRcaronrcaronSacutesacute Scircumflex scircumflex Tcommaaccent tcommaaccentTcarontcaronTbartbarUtildeutildeUmacronumacronUbreveubreveUringuring Uhungarumlaut uhungarumlautUogonekuogonek Wcircumflex wcircumflex Ycircumflex ycircumflexZacutezacute Zdotaccent zdotaccentlongsuni0180uni0181uni0182uni0183uni0184uni0185uni0186uni0187uni0188uni0189uni018Auni018Buni018Cuni018Duni018Euni018Funi0190uni0191uni0193uni0194uni0195uni0196uni0197uni0198uni0199uni019Auni019Buni019Cuni019Duni019Euni019FOhornohornuni01A2uni01A3uni01A4uni01A5uni01A6uni01A7uni01A8uni01A9uni01AAuni01ABuni01ACuni01ADuni01AEUhornuhornuni01B1uni01B2uni01B3uni01B4uni01B5uni01B6uni01B7uni01B8uni01B9uni01BAuni01BBuni01BCuni01BDuni01BEuni01BFuni01C0uni01C1uni01C2uni01C3uni01C4uni01C5uni01C6uni01C7uni01C8uni01C9uni01CAuni01CBuni01CCuni01CDuni01CEuni01CFuni01D0uni01D1uni01D2uni01D3uni01D4uni01D5uni01D6uni01D7uni01D8uni01D9uni01DAuni01DBuni01DCuni01DDuni01DEuni01DFuni01E0uni01E1uni01E2uni01E3uni01E4uni01E5Gcarongcaronuni01E8uni01E9uni01EAuni01EBuni01ECuni01EDuni01EEuni01EFuni01F0uni01F1uni01F2uni01F3uni01F4uni01F5uni01F6uni01F7uni01F8uni01F9 Aringacute aringacuteAEacuteaeacute Oslashacute oslashacuteuni0200uni0201uni0202uni0203uni0204uni0205uni0206uni0207uni0208uni0209uni020Auni020Buni020Cuni020Duni020Euni020Funi0210uni0211uni0212uni0213uni0214uni0215uni0216uni0217 Scommaaccent scommaaccentuni021Auni021Buni021Cuni021Duni021Euni021Funi0220uni0221uni0222uni0223uni0224uni0225uni0226uni0227uni0228uni0229uni022Auni022Buni022Cuni022Duni022Euni022Funi0230uni0231uni0232uni0233uni0234uni0235uni0236dotlessjuni0238uni0239uni023Auni023Buni023Cuni023Duni023Euni023Funi0240uni0241uni0242uni0243uni0244uni0245uni0246uni0247uni0248uni0249uni024Auni024Buni024Cuni024Duni024Euni024Funi0250uni0251uni0252uni0253uni0254uni0255uni0256uni0257uni0258uni0259uni025Auni025Buni025Cuni025Duni025Euni025Funi0260uni0261uni0262uni0263uni0264uni0265uni0266uni0267uni0268uni0269uni026Auni026Buni026Cuni026Duni026Euni026Funi0270uni0271uni0272uni0273uni0274uni0275uni0276uni0277uni0278uni0279uni027Auni027Buni027Cuni027Duni027Euni027Funi0280uni0281uni0282uni0283uni0284uni0285uni0286uni0287uni0288uni0289uni028Auni028Buni028Cuni028Duni028Euni028Funi0290uni0291uni0292uni0293uni0294uni0295uni0296uni0297uni0298uni0299uni029Auni029Buni029Cuni029Duni029Euni029Funi02A0uni02A1uni02A2uni02A3uni02A4uni02A5uni02A6uni02A7uni02A8uni02A9uni02AAuni02ABuni02ACuni02ADuni02AEuni02AFuni02B0uni02B1uni02B2uni02B3uni02B4uni02B5uni02B6uni02B7uni02B8uni02B9uni02BAuni02BBuni02BCuni02BDuni02BEuni02BFuni02C0uni02C1uni02C2uni02C3uni02C4uni02C5uni02C8uni02C9uni02CAuni02CBuni02CCuni02CDuni02CEuni02CFuni02D0uni02D1uni02D2uni02D3uni02D4uni02D5uni02D6uni02D7uni02DEuni02DFuni02E0uni02E1uni02E2uni02E3uni02E4uni02E5uni02E6uni02E7uni02E8uni02E9uni02ECuni02EDuni02EEuni02F3uni02F7 gravecomb acutecombuni0302 tildecombuni0304uni0305uni0306uni0307uni0308 hookabovecombuni030Auni030Buni030Cuni030Duni030Euni030Funi0310uni0311uni0312uni0313uni0314uni0315uni0316uni0317uni0318uni0319uni031Auni031Buni031Cuni031Duni031Euni031Funi0320uni0321uni0322 dotbelowcombuni0324uni0325uni0326uni0327uni0328uni0329uni032Auni032Buni032Cuni032Duni032Euni032Funi0330uni0331uni0332uni0333uni0334uni0335uni0336uni0337uni0338uni0339uni033Auni033Buni033Cuni033Duni033Euni033Funi0340uni0341uni0342uni0343uni0344uni0345uni0346uni0347uni0348uni0349uni034Auni034Buni034Cuni034Duni034Euni034Funi0351uni0352uni0353uni0357uni0358uni035Auni035Cuni035Duni035Euni035Funi0360uni0361uni0362uni0370uni0371uni0372uni0373uni0374uni0375uni0376uni0377uni037Auni037Buni037Cuni037Duni037Etonos dieresistonos Alphatonos anoteleia EpsilontonosEtatonos Iotatonos Omicrontonos Upsilontonos OmegatonosiotadieresistonosAlphaBetaGammauni0394EpsilonZetaEtaThetaIotaKappaLambdaMuNuXiOmicronPiRhoSigmaTauUpsilonPhiChiPsi IotadieresisUpsilondieresis alphatonos epsilontonosetatonos iotatonosupsilondieresistonosalphabetagammadeltaepsilonzetaetathetaiotakappalambdauni03BCnuxiomicronrhosigma1sigmatauupsilonphichipsiomega iotadieresisupsilondieresis omicrontonos upsilontonos omegatonosuni03CFuni03D0theta1Upsilon1uni03D3uni03D4phi1omega1uni03D7uni03D8uni03D9uni03DAuni03DBuni03DCuni03DDuni03DEuni03DFuni03E0uni03E1uni03E2uni03E3uni03E4uni03E5uni03E6uni03E7uni03E8uni03E9uni03EAuni03EBuni03ECuni03EDuni03EEuni03EFuni03F0uni03F1uni03F2uni03F3uni03F4uni03F5uni03F6uni03F7uni03F8uni03F9uni03FAuni03FBuni03FCuni03FDuni03FEuni03FFuni0400uni0401uni0402uni0403uni0404uni0405uni0406uni0407uni0408uni0409uni040Auni040Buni040Cuni040Duni040Euni040Funi0410uni0411uni0412uni0413uni0414uni0415uni0416uni0417uni0418uni0419uni041Auni041Buni041Cuni041Duni041Euni041Funi0420uni0421uni0422uni0423uni0424uni0425uni0426uni0427uni0428uni0429uni042Auni042Buni042Cuni042Duni042Euni042Funi0430uni0431uni0432uni0433uni0434uni0435uni0436uni0437uni0438uni0439uni043Auni043Buni043Cuni043Duni043Euni043Funi0440uni0441uni0442uni0443uni0444uni0445uni0446uni0447uni0448uni0449uni044Auni044Buni044Cuni044Duni044Euni044Funi0450uni0451uni0452uni0453uni0454uni0455uni0456uni0457uni0458uni0459uni045Auni045Buni045Cuni045Duni045Euni045Funi0460uni0461uni0462uni0463uni0464uni0465uni0466uni0467uni0468uni0469uni046Auni046Buni046Cuni046Duni046Euni046Funi0470uni0471uni0472uni0473uni0474uni0475uni0476uni0477uni0478uni0479uni047Auni047Buni047Cuni047Duni047Euni047Funi0480uni0481uni0482uni0483uni0484uni0485uni0486uni0487uni0488uni0489uni048Auni048Buni048Cuni048Duni048Euni048Funi0490uni0491uni0492uni0493uni0494uni0495uni0496uni0497uni0498uni0499uni049Auni049Buni049Cuni049Duni049Euni049Funi04A0uni04A1uni04A2uni04A3uni04A4uni04A5uni04A6uni04A7uni04A8uni04A9uni04AAuni04ABuni04ACuni04ADuni04AEuni04AFuni04B0uni04B1uni04B2uni04B3uni04B4uni04B5uni04B6uni04B7uni04B8uni04B9uni04BAuni04BBuni04BCuni04BDuni04BEuni04BFuni04C0uni04C1uni04C2uni04C3uni04C4uni04C5uni04C6uni04C7uni04C8uni04C9uni04CAuni04CBuni04CCuni04CDuni04CEuni04CFuni04D0uni04D1uni04D2uni04D3uni04D4uni04D5uni04D6uni04D7uni04D8uni04D9uni04DAuni04DBuni04DCuni04DDuni04DEuni04DFuni04E0uni04E1uni04E2uni04E3uni04E4uni04E5uni04E6uni04E7uni04E8uni04E9uni04EAuni04EBuni04ECuni04EDuni04EEuni04EFuni04F0uni04F1uni04F2uni04F3uni04F4uni04F5uni04F6uni04F7uni04F8uni04F9uni04FAuni04FBuni04FCuni04FDuni04FEuni04FFuni0500uni0501uni0502uni0503uni0504uni0505uni0506uni0507uni0508uni0509uni050Auni050Buni050Cuni050Duni050Euni050Funi0510uni0511uni0512uni0513uni0514uni0515uni0516uni0517uni0518uni0519uni051Auni051Buni051Cuni051Duni0520uni0521uni0522uni0523uni0524uni0525uni0531uni0532uni0533uni0534uni0535uni0536uni0537uni0538uni0539uni053Auni053Buni053Cuni053Duni053Euni053Funi0540uni0541uni0542uni0543uni0544uni0545uni0546uni0547uni0548uni0549uni054Auni054Buni054Cuni054Duni054Euni054Funi0550uni0551uni0552uni0553uni0554uni0555uni0556uni0559uni055Auni055Buni055Cuni055Duni055Euni055Funi0561uni0562uni0563uni0564uni0565uni0566uni0567uni0568uni0569uni056Auni056Buni056Cuni056Duni056Euni056Funi0570uni0571uni0572uni0573uni0574uni0575uni0576uni0577uni0578uni0579uni057Auni057Buni057Cuni057Duni057Euni057Funi0580uni0581uni0582uni0583uni0584uni0585uni0586uni0587uni0589uni058Auni05B0uni05B1uni05B2uni05B3uni05B4uni05B5uni05B6uni05B7uni05B8uni05B9uni05BAuni05BBuni05BCuni05BDuni05BEuni05BFuni05C0uni05C1uni05C2uni05C3uni05C6uni05C7uni05D0uni05D1uni05D2uni05D3uni05D4uni05D5uni05D6uni05D7uni05D8uni05D9uni05DAuni05DBuni05DCuni05DDuni05DEuni05DFuni05E0uni05E1uni05E2uni05E3uni05E4uni05E5uni05E6uni05E7uni05E8uni05E9uni05EAuni05F0uni05F1uni05F2uni05F3uni05F4uni0606uni0607uni0609uni060Auni060Cuni0615uni061Buni061Funi0621uni0622uni0623uni0624uni0625uni0626uni0627uni0628uni0629uni062Auni062Buni062Cuni062Duni062Euni062Funi0630uni0631uni0632uni0633uni0634uni0635uni0636uni0637uni0638uni0639uni063Auni0640uni0641uni0642uni0643uni0644uni0645uni0646uni0647uni0648uni0649uni064Auni064Buni064Cuni064Duni064Euni064Funi0650uni0651uni0652uni0653uni0654uni0655uni065Auni0660uni0661uni0662uni0663uni0664uni0665uni0666uni0667uni0668uni0669uni066Auni066Buni066Cuni066Duni066Euni066Funi0674uni0679uni067Auni067Buni067Cuni067Duni067Euni067Funi0680uni0681uni0682uni0683uni0684uni0685uni0686uni0687uni0691uni0692uni0695uni0698uni06A1uni06A4uni06A6uni06A9uni06AFuni06B5uni06BAuni06BFuni06C6uni06CCuni06CEuni06D5uni06F0uni06F1uni06F2uni06F3uni06F4uni06F5uni06F6uni06F7uni06F8uni06F9uni07C0uni07C1uni07C2uni07C3uni07C4uni07C5uni07C6uni07C7uni07C8uni07C9uni07CAuni07CBuni07CCuni07CDuni07CEuni07CFuni07D0uni07D1uni07D2uni07D3uni07D4uni07D5uni07D6uni07D7uni07D8uni07D9uni07DAuni07DBuni07DCuni07DDuni07DEuni07DFuni07E0uni07E1uni07E2uni07E3uni07E4uni07E5uni07E6uni07E7uni07EBuni07ECuni07EDuni07EEuni07EFuni07F0uni07F1uni07F2uni07F3uni07F4uni07F5uni07F8uni07F9uni07FAuni0E3Funi0E81uni0E82uni0E84uni0E87uni0E88uni0E8Auni0E8Duni0E94uni0E95uni0E96uni0E97uni0E99uni0E9Auni0E9Buni0E9Cuni0E9Duni0E9Euni0E9Funi0EA1uni0EA2uni0EA3uni0EA5uni0EA7uni0EAAuni0EABuni0EADuni0EAEuni0EAFuni0EB0uni0EB1uni0EB2uni0EB3uni0EB4uni0EB5uni0EB6uni0EB7uni0EB8uni0EB9uni0EBBuni0EBCuni0EBDuni0EC0uni0EC1uni0EC2uni0EC3uni0EC4uni0EC6uni0EC8uni0EC9uni0ECAuni0ECBuni0ECCuni0ECDuni0ED0uni0ED1uni0ED2uni0ED3uni0ED4uni0ED5uni0ED6uni0ED7uni0ED8uni0ED9uni0EDCuni0EDDuni10A0uni10A1uni10A2uni10A3uni10A4uni10A5uni10A6uni10A7uni10A8uni10A9uni10AAuni10ABuni10ACuni10ADuni10AEuni10AFuni10B0uni10B1uni10B2uni10B3uni10B4uni10B5uni10B6uni10B7uni10B8uni10B9uni10BAuni10BBuni10BCuni10BDuni10BEuni10BFuni10C0uni10C1uni10C2uni10C3uni10C4uni10C5uni10D0uni10D1uni10D2uni10D3uni10D4uni10D5uni10D6uni10D7uni10D8uni10D9uni10DAuni10DBuni10DCuni10DDuni10DEuni10DFuni10E0uni10E1uni10E2uni10E3uni10E4uni10E5uni10E6uni10E7uni10E8uni10E9uni10EAuni10EBuni10ECuni10EDuni10EEuni10EFuni10F0uni10F1uni10F2uni10F3uni10F4uni10F5uni10F6uni10F7uni10F8uni10F9uni10FAuni10FBuni10FCuni1401uni1402uni1403uni1404uni1405uni1406uni1407uni1409uni140Auni140Buni140Cuni140Duni140Euni140Funi1410uni1411uni1412uni1413uni1414uni1415uni1416uni1417uni1418uni1419uni141Auni141Buni141Duni141Euni141Funi1420uni1421uni1422uni1423uni1424uni1425uni1426uni1427uni1428uni1429uni142Auni142Buni142Cuni142Duni142Euni142Funi1430uni1431uni1432uni1433uni1434uni1435uni1437uni1438uni1439uni143Auni143Buni143Cuni143Duni143Euni143Funi1440uni1441uni1442uni1443uni1444uni1445uni1446uni1447uni1448uni1449uni144Auni144Cuni144Duni144Euni144Funi1450uni1451uni1452uni1454uni1455uni1456uni1457uni1458uni1459uni145Auni145Buni145Cuni145Duni145Euni145Funi1460uni1461uni1462uni1463uni1464uni1465uni1466uni1467uni1468uni1469uni146Auni146Buni146Cuni146Duni146Euni146Funi1470uni1471uni1472uni1473uni1474uni1475uni1476uni1477uni1478uni1479uni147Auni147Buni147Cuni147Duni147Euni147Funi1480uni1481uni1482uni1483uni1484uni1485uni1486uni1487uni1488uni1489uni148Auni148Buni148Cuni148Duni148Euni148Funi1490uni1491uni1492uni1493uni1494uni1495uni1496uni1497uni1498uni1499uni149Auni149Buni149Cuni149Duni149Euni149Funi14A0uni14A1uni14A2uni14A3uni14A4uni14A5uni14A6uni14A7uni14A8uni14A9uni14AAuni14ABuni14ACuni14ADuni14AEuni14AFuni14B0uni14B1uni14B2uni14B3uni14B4uni14B5uni14B6uni14B7uni14B8uni14B9uni14BAuni14BBuni14BCuni14BDuni14C0uni14C1uni14C2uni14C3uni14C4uni14C5uni14C6uni14C7uni14C8uni14C9uni14CAuni14CBuni14CCuni14CDuni14CEuni14CFuni14D0uni14D1uni14D2uni14D3uni14D4uni14D5uni14D6uni14D7uni14D8uni14D9uni14DAuni14DBuni14DCuni14DDuni14DEuni14DFuni14E0uni14E1uni14E2uni14E3uni14E4uni14E5uni14E6uni14E7uni14E8uni14E9uni14EAuni14ECuni14EDuni14EEuni14EFuni14F0uni14F1uni14F2uni14F3uni14F4uni14F5uni14F6uni14F7uni14F8uni14F9uni14FAuni14FBuni14FCuni14FDuni14FEuni14FFuni1500uni1501uni1502uni1503uni1504uni1505uni1506uni1507uni1510uni1511uni1512uni1513uni1514uni1515uni1516uni1517uni1518uni1519uni151Auni151Buni151Cuni151Duni151Euni151Funi1520uni1521uni1522uni1523uni1524uni1525uni1526uni1527uni1528uni1529uni152Auni152Buni152Cuni152Duni152Euni152Funi1530uni1531uni1532uni1533uni1534uni1535uni1536uni1537uni1538uni1539uni153Auni153Buni153Cuni153Duni153Euni1540uni1541uni1542uni1543uni1544uni1545uni1546uni1547uni1548uni1549uni154Auni154Buni154Cuni154Duni154Euni154Funi1550uni1552uni1553uni1554uni1555uni1556uni1557uni1558uni1559uni155Auni155Buni155Cuni155Duni155Euni155Funi1560uni1561uni1562uni1563uni1564uni1565uni1566uni1567uni1568uni1569uni156Auni1574uni1575uni1576uni1577uni1578uni1579uni157Auni157Buni157Cuni157Duni157Euni157Funi1580uni1581uni1582uni1583uni1584uni1585uni158Auni158Buni158Cuni158Duni158Euni158Funi1590uni1591uni1592uni1593uni1594uni1595uni1596uni15A0uni15A1uni15A2uni15A3uni15A4uni15A5uni15A6uni15A7uni15A8uni15A9uni15AAuni15ABuni15ACuni15ADuni15AEuni15AFuni15DEuni15E1uni1646uni1647uni166Euni166Funi1670uni1671uni1672uni1673uni1674uni1675uni1676uni1680uni1681uni1682uni1683uni1684uni1685uni1686uni1687uni1688uni1689uni168Auni168Buni168Cuni168Duni168Euni168Funi1690uni1691uni1692uni1693uni1694uni1695uni1696uni1697uni1698uni1699uni169Auni169Buni169Cuni1D00uni1D01uni1D02uni1D03uni1D04uni1D05uni1D06uni1D07uni1D08uni1D09uni1D0Auni1D0Buni1D0Cuni1D0Duni1D0Euni1D0Funi1D10uni1D11uni1D12uni1D13uni1D14uni1D16uni1D17uni1D18uni1D19uni1D1Auni1D1Buni1D1Cuni1D1Duni1D1Euni1D1Funi1D20uni1D21uni1D22uni1D23uni1D26uni1D27uni1D28uni1D29uni1D2Auni1D2Buni1D2Cuni1D2Duni1D2Euni1D30uni1D31uni1D32uni1D33uni1D34uni1D35uni1D36uni1D37uni1D38uni1D39uni1D3Auni1D3Buni1D3Cuni1D3Duni1D3Euni1D3Funi1D40uni1D41uni1D42uni1D43uni1D44uni1D45uni1D46uni1D47uni1D48uni1D49uni1D4Auni1D4Buni1D4Cuni1D4Duni1D4Euni1D4Funi1D50uni1D51uni1D52uni1D53uni1D54uni1D55uni1D56uni1D57uni1D58uni1D59uni1D5Auni1D5Buni1D5Duni1D5Euni1D5Funi1D60uni1D61uni1D62uni1D63uni1D64uni1D65uni1D66uni1D67uni1D68uni1D69uni1D6Auni1D77uni1D78uni1D7Buni1D85uni1D9Buni1D9Cuni1D9Duni1D9Euni1D9Funi1DA0uni1DA1uni1DA2uni1DA3uni1DA4uni1DA5uni1DA6uni1DA7uni1DA8uni1DA9uni1DAAuni1DABuni1DACuni1DADuni1DAEuni1DAFuni1DB0uni1DB1uni1DB2uni1DB3uni1DB4uni1DB5uni1DB6uni1DB7uni1DB8uni1DB9uni1DBAuni1DBBuni1DBCuni1DBDuni1DBEuni1DBFuni1DC4uni1DC5uni1DC6uni1DC7uni1DC8uni1DC9uni1E00uni1E01uni1E02uni1E03uni1E04uni1E05uni1E06uni1E07uni1E08uni1E09uni1E0Auni1E0Buni1E0Cuni1E0Duni1E0Euni1E0Funi1E10uni1E11uni1E12uni1E13uni1E14uni1E15uni1E16uni1E17uni1E18uni1E19uni1E1Auni1E1Buni1E1Cuni1E1Duni1E1Euni1E1Funi1E20uni1E21uni1E22uni1E23uni1E24uni1E25uni1E26uni1E27uni1E28uni1E29uni1E2Auni1E2Buni1E2Cuni1E2Duni1E2Euni1E2Funi1E30uni1E31uni1E32uni1E33uni1E34uni1E35uni1E36uni1E37uni1E38uni1E39uni1E3Auni1E3Buni1E3Cuni1E3Duni1E3Euni1E3Funi1E40uni1E41uni1E42uni1E43uni1E44uni1E45uni1E46uni1E47uni1E48uni1E49uni1E4Auni1E4Buni1E4Cuni1E4Duni1E4Euni1E4Funi1E50uni1E51uni1E52uni1E53uni1E54uni1E55uni1E56uni1E57uni1E58uni1E59uni1E5Auni1E5Buni1E5Cuni1E5Duni1E5Euni1E5Funi1E60uni1E61uni1E62uni1E63uni1E64uni1E65uni1E66uni1E67uni1E68uni1E69uni1E6Auni1E6Buni1E6Cuni1E6Duni1E6Euni1E6Funi1E70uni1E71uni1E72uni1E73uni1E74uni1E75uni1E76uni1E77uni1E78uni1E79uni1E7Auni1E7Buni1E7Cuni1E7Duni1E7Euni1E7FWgravewgraveWacutewacute Wdieresis wdieresisuni1E86uni1E87uni1E88uni1E89uni1E8Auni1E8Buni1E8Cuni1E8Duni1E8Euni1E8Funi1E90uni1E91uni1E92uni1E93uni1E94uni1E95uni1E96uni1E97uni1E98uni1E99uni1E9Auni1E9Buni1E9Euni1E9Funi1EA0uni1EA1uni1EA2uni1EA3uni1EA4uni1EA5uni1EA6uni1EA7uni1EA8uni1EA9uni1EAAuni1EABuni1EACuni1EADuni1EAEuni1EAFuni1EB0uni1EB1uni1EB2uni1EB3uni1EB4uni1EB5uni1EB6uni1EB7uni1EB8uni1EB9uni1EBAuni1EBBuni1EBCuni1EBDuni1EBEuni1EBFuni1EC0uni1EC1uni1EC2uni1EC3uni1EC4uni1EC5uni1EC6uni1EC7uni1EC8uni1EC9uni1ECAuni1ECBuni1ECCuni1ECDuni1ECEuni1ECFuni1ED0uni1ED1uni1ED2uni1ED3uni1ED4uni1ED5uni1ED6uni1ED7uni1ED8uni1ED9uni1EDAuni1EDBuni1EDCuni1EDDuni1EDEuni1EDFuni1EE0uni1EE1uni1EE2uni1EE3uni1EE4uni1EE5uni1EE6uni1EE7uni1EE8uni1EE9uni1EEAuni1EEBuni1EECuni1EEDuni1EEEuni1EEFuni1EF0uni1EF1Ygraveygraveuni1EF4uni1EF5uni1EF6uni1EF7uni1EF8uni1EF9uni1F00uni1F01uni1F02uni1F03uni1F04uni1F05uni1F06uni1F07uni1F08uni1F09uni1F0Auni1F0Buni1F0Cuni1F0Duni1F0Euni1F0Funi1F10uni1F11uni1F12uni1F13uni1F14uni1F15uni1F18uni1F19uni1F1Auni1F1Buni1F1Cuni1F1Duni1F20uni1F21uni1F22uni1F23uni1F24uni1F25uni1F26uni1F27uni1F28uni1F29uni1F2Auni1F2Buni1F2Cuni1F2Duni1F2Euni1F2Funi1F30uni1F31uni1F32uni1F33uni1F34uni1F35uni1F36uni1F37uni1F38uni1F39uni1F3Auni1F3Buni1F3Cuni1F3Duni1F3Euni1F3Funi1F40uni1F41uni1F42uni1F43uni1F44uni1F45uni1F48uni1F49uni1F4Auni1F4Buni1F4Cuni1F4Duni1F50uni1F51uni1F52uni1F53uni1F54uni1F55uni1F56uni1F57uni1F59uni1F5Buni1F5Duni1F5Funi1F60uni1F61uni1F62uni1F63uni1F64uni1F65uni1F66uni1F67uni1F68uni1F69uni1F6Auni1F6Buni1F6Cuni1F6Duni1F6Euni1F6Funi1F70uni1F71uni1F72uni1F73uni1F74uni1F75uni1F76uni1F77uni1F78uni1F79uni1F7Auni1F7Buni1F7Cuni1F7Duni1F80uni1F81uni1F82uni1F83uni1F84uni1F85uni1F86uni1F87uni1F88uni1F89uni1F8Auni1F8Buni1F8Cuni1F8Duni1F8Euni1F8Funi1F90uni1F91uni1F92uni1F93uni1F94uni1F95uni1F96uni1F97uni1F98uni1F99uni1F9Auni1F9Buni1F9Cuni1F9Duni1F9Euni1F9Funi1FA0uni1FA1uni1FA2uni1FA3uni1FA4uni1FA5uni1FA6uni1FA7uni1FA8uni1FA9uni1FAAuni1FABuni1FACuni1FADuni1FAEuni1FAFuni1FB0uni1FB1uni1FB2uni1FB3uni1FB4uni1FB6uni1FB7uni1FB8uni1FB9uni1FBAuni1FBBuni1FBCuni1FBDuni1FBEuni1FBFuni1FC0uni1FC1uni1FC2uni1FC3uni1FC4uni1FC6uni1FC7uni1FC8uni1FC9uni1FCAuni1FCBuni1FCCuni1FCDuni1FCEuni1FCFuni1FD0uni1FD1uni1FD2uni1FD3uni1FD6uni1FD7uni1FD8uni1FD9uni1FDAuni1FDBuni1FDDuni1FDEuni1FDFuni1FE0uni1FE1uni1FE2uni1FE3uni1FE4uni1FE5uni1FE6uni1FE7uni1FE8uni1FE9uni1FEAuni1FEBuni1FECuni1FEDuni1FEEuni1FEFuni1FF2uni1FF3uni1FF4uni1FF6uni1FF7uni1FF8uni1FF9uni1FFAuni1FFBuni1FFCuni1FFDuni1FFEuni2000uni2001uni2002uni2003uni2004uni2005uni2006uni2007uni2008uni2009uni200Auni200Buni200Cuni200Duni200Euni200Funi2010uni2011 figuredashuni2015uni2016 underscoredbl quotereverseduni201Funi2023onedotenleadertwodotenleaderuni2027uni202Auni202Buni202Cuni202Duni202Euni202Funi2031minuteseconduni2034uni2035uni2036uni2037uni2038uni203B exclamdbluni203Duni203Euni203Funi2040uni2041uni2042uni2043uni2045uni2046uni2047uni2048uni2049uni204Auni204Buni204Cuni204Duni204Euni204Funi2050uni2051uni2052uni2053uni2054uni2055uni2056uni2057uni2058uni2059uni205Auni205Buni205Cuni205Duni205Euni205Funi2060uni2061uni2062uni2063uni2064uni206Auni206Buni206Cuni206Duni206Euni206Funi2070uni2071uni2074uni2075uni2076uni2077uni2078uni2079uni207Auni207Buni207Cuni207Duni207Euni207Funi2080uni2081uni2082uni2083uni2084uni2085uni2086uni2087uni2088uni2089uni208Auni208Buni208Cuni208Duni208Euni2090uni2091uni2092uni2093uni2094uni20A0 colonmonetaryuni20A2lirauni20A5uni20A6pesetauni20A8uni20A9uni20AAdongEurouni20ADuni20AEuni20AFuni20B0uni20B1uni20B2uni20B3uni20B4uni20B5uni20D0uni20D1uni20D6uni20D7uni20DBuni20DCuni20E1uni2100uni2101uni2102uni2103uni2104uni2105uni2106uni2107uni2108uni2109uni210Buni210Cuni210Duni210Euni210Funi2110Ifrakturuni2112uni2113uni2114uni2115uni2116uni2117 weierstrassuni2119uni211Auni211BRfrakturuni211D prescriptionuni211Funi2120uni2121uni2123uni2124uni2125uni2126uni2127uni2128uni2129uni212Auni212Buni212Cuni212D estimateduni212Funi2130uni2131uni2132uni2133uni2134alephuni2136uni2137uni2138uni2139uni213Auni213Buni213Cuni213Duni213Euni213Funi2140uni2141uni2142uni2143uni2144uni2145uni2146uni2147uni2148uni2149uni214Buni214Eonethird twothirdsuni2155uni2156uni2157uni2158uni2159uni215A oneeighth threeeighths fiveeighths seveneighthsuni215Funi2160uni2161uni2162uni2163uni2164uni2165uni2166uni2167uni2168uni2169uni216Auni216Buni216Cuni216Duni216Euni216Funi2170uni2171uni2172uni2173uni2174uni2175uni2176uni2177uni2178uni2179uni217Auni217Buni217Cuni217Duni217Euni217Funi2180uni2181uni2182uni2183uni2184 arrowleftarrowup arrowright arrowdown arrowboth arrowupdnuni2196uni2197uni2198uni2199uni219Auni219Buni219Cuni219Duni219Euni219Funi21A0uni21A1uni21A2uni21A3uni21A4uni21A5uni21A6uni21A7 arrowupdnbseuni21A9uni21AAuni21ABuni21ACuni21ADuni21AEuni21AFuni21B0uni21B1uni21B2uni21B3uni21B4carriagereturnuni21B6uni21B7uni21B8uni21B9uni21BAuni21BBuni21BCuni21BDuni21BEuni21BFuni21C0uni21C1uni21C2uni21C3uni21C4uni21C5uni21C6uni21C7uni21C8uni21C9uni21CAuni21CBuni21CCuni21CDuni21CEuni21CF arrowdblleft arrowdblup arrowdblright arrowdbldown arrowdblbothuni21D5uni21D6uni21D7uni21D8uni21D9uni21DAuni21DBuni21DCuni21DDuni21DEuni21DFuni21E0uni21E1uni21E2uni21E3uni21E4uni21E5uni21E6uni21E7uni21E8uni21E9uni21EAuni21EBuni21ECuni21EDuni21EEuni21EFuni21F0uni21F1uni21F2uni21F3uni21F4uni21F5uni21F6uni21F7uni21F8uni21F9uni21FAuni21FBuni21FCuni21FDuni21FEuni21FF universaluni2201 existentialuni2204emptysetgradientelement notelementuni220Asuchthatuni220Cuni220Duni220Euni2210uni2213uni2214uni2215uni2216 asteriskmathuni2218uni2219uni221Buni221C proportional orthogonalangleuni2221uni2222uni2223uni2224uni2225uni2226 logicaland logicalor intersectionunionuni222Cuni222Duni222Euni222Funi2230uni2231uni2232uni2233 thereforeuni2235uni2236uni2237uni2238uni2239uni223Auni223Bsimilaruni223Duni223Euni223Funi2240uni2241uni2242uni2243uni2244 congruentuni2246uni2247uni2249uni224Auni224Buni224Cuni224Duni224Euni224Funi2250uni2251uni2252uni2253uni2254uni2255uni2256uni2257uni2258uni2259uni225Auni225Buni225Cuni225Duni225Euni225F equivalenceuni2262uni2263uni2266uni2267uni2268uni2269uni226Auni226Buni226Cuni226Duni226Euni226Funi2270uni2271uni2272uni2273uni2274uni2275uni2276uni2277uni2278uni2279uni227Auni227Buni227Cuni227Duni227Euni227Funi2280uni2281 propersubsetpropersuperset notsubsetuni2285 reflexsubsetreflexsupersetuni2288uni2289uni228Auni228Buni228Cuni228Duni228Euni228Funi2290uni2291uni2292uni2293uni2294 circleplusuni2296circlemultiplyuni2298uni2299uni229Auni229Buni229Cuni229Duni229Euni229Funi22A0uni22A1uni22A2uni22A3uni22A4 perpendicularuni22A6uni22A7uni22A8uni22A9uni22AAuni22ABuni22ACuni22ADuni22AEuni22AFuni22B0uni22B1uni22B2uni22B3uni22B4uni22B5uni22B6uni22B7uni22B8uni22B9uni22BAuni22BBuni22BCuni22BDuni22BEuni22BFuni22C0uni22C1uni22C2uni22C3uni22C4dotmathuni22C6uni22C7uni22C8uni22C9uni22CAuni22CBuni22CCuni22CDuni22CEuni22CFuni22D0uni22D1uni22D2uni22D3uni22D4uni22D5uni22D6uni22D7uni22D8uni22D9uni22DAuni22DBuni22DCuni22DDuni22DEuni22DFuni22E0uni22E1uni22E2uni22E3uni22E4uni22E5uni22E6uni22E7uni22E8uni22E9uni22EAuni22EBuni22ECuni22EDuni22EEuni22EFuni22F0uni22F1uni22F2uni22F3uni22F4uni22F5uni22F6uni22F7uni22F8uni22F9uni22FAuni22FBuni22FCuni22FDuni22FEuni22FFuni2300uni2301houseuni2303uni2304uni2305uni2306uni2307uni2308uni2309uni230Auni230Buni230Cuni230Duni230Euni230F revlogicalnotuni2311uni2318uni2319uni231Cuni231Duni231Euni231F integraltp integralbtuni2324uni2325uni2326uni2327uni2328uni232Buni232Cuni2373uni2374uni2375uni237Auni237Duni2387uni2394uni239Buni239Cuni239Duni239Euni239Funi23A0uni23A1uni23A2uni23A3uni23A4uni23A5uni23A6uni23A7uni23A8uni23A9uni23AAuni23ABuni23ACuni23ADuni23AEuni23CEuni23CFuni23E3uni23E5uni2422uni2423uni2460uni2461uni2462uni2463uni2464uni2465uni2466uni2467uni2468uni2469SF100000uni2501SF110000uni2503uni2504uni2505uni2506uni2507uni2508uni2509uni250Auni250BSF010000uni250Duni250Euni250FSF030000uni2511uni2512uni2513SF020000uni2515uni2516uni2517SF040000uni2519uni251Auni251BSF080000uni251Duni251Euni251Funi2520uni2521uni2522uni2523SF090000uni2525uni2526uni2527uni2528uni2529uni252Auni252BSF060000uni252Duni252Euni252Funi2530uni2531uni2532uni2533SF070000uni2535uni2536uni2537uni2538uni2539uni253Auni253BSF050000uni253Duni253Euni253Funi2540uni2541uni2542uni2543uni2544uni2545uni2546uni2547uni2548uni2549uni254Auni254Buni254Cuni254Duni254Euni254FSF430000SF240000SF510000SF520000SF390000SF220000SF210000SF250000SF500000SF490000SF380000SF280000SF270000SF260000SF360000SF370000SF420000SF190000SF200000SF230000SF470000SF480000SF410000SF450000SF460000SF400000SF540000SF530000SF440000uni256Duni256Euni256Funi2570uni2571uni2572uni2573uni2574uni2575uni2576uni2577uni2578uni2579uni257Auni257Buni257Cuni257Duni257Euni257Fupblockuni2581uni2582uni2583dnblockuni2585uni2586uni2587blockuni2589uni258Auni258Blfblockuni258Duni258Euni258Frtblockltshadeshadedkshadeuni2594uni2595uni2596uni2597uni2598uni2599uni259Auni259Buni259Cuni259Duni259Euni259F filledboxH22073uni25A2uni25A3uni25A4uni25A5uni25A6uni25A7uni25A8uni25A9H18543H18551 filledrectuni25ADuni25AEuni25AFuni25B0uni25B1triagupuni25B3uni25B4uni25B5uni25B6uni25B7uni25B8uni25B9triagrtuni25BBtriagdnuni25BDuni25BEuni25BFuni25C0uni25C1uni25C2uni25C3triaglfuni25C5uni25C6uni25C7uni25C8uni25C9circleuni25CCuni25CDuni25CEH18533uni25D0uni25D1uni25D2uni25D3uni25D4uni25D5uni25D6uni25D7 invbullet invcircleuni25DAuni25DBuni25DCuni25DDuni25DEuni25DFuni25E0uni25E1uni25E2uni25E3uni25E4uni25E5 openbulletuni25E7uni25E8uni25E9uni25EAuni25EBuni25ECuni25EDuni25EEuni25EFuni25F0uni25F1uni25F2uni25F3uni25F4uni25F5uni25F6uni25F7uni25F8uni25F9uni25FAuni25FBuni25FCuni25FDuni25FEuni25FFuni2600uni2601uni2602uni2603uni2604uni2605uni2606uni2607uni2608uni2609uni260Auni260Buni260Cuni260Duni260Euni260Funi2610uni2611uni2612uni2613uni2614uni2615uni2616uni2617uni2618uni2619uni261Auni261Buni261Cuni261Duni261Euni261Funi2620uni2621uni2622uni2623uni2624uni2625uni2626uni2627uni2628uni2629uni262Auni262Buni262Cuni262Duni262Euni262Funi2630uni2631uni2632uni2633uni2634uni2635uni2636uni2637uni2638uni2639 smileface invsmilefacesununi263Duni263Euni263Ffemaleuni2641maleuni2643uni2644uni2645uni2646uni2647uni2648uni2649uni264Auni264Buni264Cuni264Duni264Euni264Funi2650uni2651uni2652uni2653uni2654uni2655uni2656uni2657uni2658uni2659uni265Auni265Buni265Cuni265Duni265Euni265Fspadeuni2661uni2662clubuni2664heartdiamonduni2667uni2668uni2669 musicalnotemusicalnotedbluni266Cuni266Duni266Euni266Funi2670uni2671uni2672uni2673uni2674uni2675uni2676uni2677uni2678uni2679uni267Auni267Buni267Cuni267Duni267Euni267Funi2680uni2681uni2682uni2683uni2684uni2685uni2686uni2687uni2688uni2689uni268Auni268Buni268Cuni268Duni268Euni268Funi2690uni2691uni2692uni2693uni2694uni2695uni2696uni2697uni2698uni2699uni269Auni269Buni269Cuni26A0uni26A1uni26A2uni26A3uni26A4uni26A5uni26A6uni26A7uni26A8uni26A9uni26AAuni26ABuni26ACuni26ADuni26AEuni26AFuni26B0uni26B1uni26B2uni26B3uni26B4uni26B5uni26B6uni26B7uni26B8uni2701uni2702uni2703uni2704uni2706uni2707uni2708uni2709uni270Cuni270Duni270Euni270Funi2710uni2711uni2712uni2713uni2714uni2715uni2716uni2717uni2718uni2719uni271Auni271Buni271Cuni271Duni271Euni271Funi2720uni2721uni2722uni2723uni2724uni2725uni2726uni2727uni2729uni272Auni272Buni272Cuni272Duni272Euni272Funi2730uni2731uni2732uni2733uni2734uni2735uni2736uni2737uni2738uni2739uni273Auni273Buni273Cuni273Duni273Euni273Funi2740uni2741uni2742uni2743uni2744uni2745uni2746uni2747uni2748uni2749uni274Auni274Buni274Duni274Funi2750uni2751uni2752uni2756uni2758uni2759uni275Auni275Buni275Cuni275Duni275Euni2761uni2762uni2763uni2764uni2765uni2766uni2767uni2768uni2769uni276Auni276Buni276Cuni276Duni276Euni276Funi2770uni2771uni2772uni2773uni2774uni2775uni2776uni2777uni2778uni2779uni277Auni277Buni277Cuni277Duni277Euni277Funi2780uni2781uni2782uni2783uni2784uni2785uni2786uni2787uni2788uni2789uni278Auni278Buni278Cuni278Duni278Euni278Funi2790uni2791uni2792uni2793uni2794uni2798uni2799uni279Auni279Buni279Cuni279Duni279Euni279Funi27A0uni27A1uni27A2uni27A3uni27A4uni27A5uni27A6uni27A7uni27A8uni27A9uni27AAuni27ABuni27ACuni27ADuni27AEuni27AFuni27B1uni27B2uni27B3uni27B4uni27B5uni27B6uni27B7uni27B8uni27B9uni27BAuni27BBuni27BCuni27BDuni27BEuni27C5uni27C6uni27E0uni27E6uni27E7uni27E8uni27E9uni27EAuni27EBuni27F0uni27F1uni27F2uni27F3uni27F4uni27F5uni27F6uni27F7uni27F8uni27F9uni27FAuni27FBuni27FCuni27FDuni27FEuni27FFuni2800uni2801uni2802uni2803uni2804uni2805uni2806uni2807uni2808uni2809uni280Auni280Buni280Cuni280Duni280Euni280Funi2810uni2811uni2812uni2813uni2814uni2815uni2816uni2817uni2818uni2819uni281Auni281Buni281Cuni281Duni281Euni281Funi2820uni2821uni2822uni2823uni2824uni2825uni2826uni2827uni2828uni2829uni282Auni282Buni282Cuni282Duni282Euni282Funi2830uni2831uni2832uni2833uni2834uni2835uni2836uni2837uni2838uni2839uni283Auni283Buni283Cuni283Duni283Euni283Funi2840uni2841uni2842uni2843uni2844uni2845uni2846uni2847uni2848uni2849uni284Auni284Buni284Cuni284Duni284Euni284Funi2850uni2851uni2852uni2853uni2854uni2855uni2856uni2857uni2858uni2859uni285Auni285Buni285Cuni285Duni285Euni285Funi2860uni2861uni2862uni2863uni2864uni2865uni2866uni2867uni2868uni2869uni286Auni286Buni286Cuni286Duni286Euni286Funi2870uni2871uni2872uni2873uni2874uni2875uni2876uni2877uni2878uni2879uni287Auni287Buni287Cuni287Duni287Euni287Funi2880uni2881uni2882uni2883uni2884uni2885uni2886uni2887uni2888uni2889uni288Auni288Buni288Cuni288Duni288Euni288Funi2890uni2891uni2892uni2893uni2894uni2895uni2896uni2897uni2898uni2899uni289Auni289Buni289Cuni289Duni289Euni289Funi28A0uni28A1uni28A2uni28A3uni28A4uni28A5uni28A6uni28A7uni28A8uni28A9uni28AAuni28ABuni28ACuni28ADuni28AEuni28AFuni28B0uni28B1uni28B2uni28B3uni28B4uni28B5uni28B6uni28B7uni28B8uni28B9uni28BAuni28BBuni28BCuni28BDuni28BEuni28BFuni28C0uni28C1uni28C2uni28C3uni28C4uni28C5uni28C6uni28C7uni28C8uni28C9uni28CAuni28CBuni28CCuni28CDuni28CEuni28CFuni28D0uni28D1uni28D2uni28D3uni28D4uni28D5uni28D6uni28D7uni28D8uni28D9uni28DAuni28DBuni28DCuni28DDuni28DEuni28DFuni28E0uni28E1uni28E2uni28E3uni28E4uni28E5uni28E6uni28E7uni28E8uni28E9uni28EAuni28EBuni28ECuni28EDuni28EEuni28EFuni28F0uni28F1uni28F2uni28F3uni28F4uni28F5uni28F6uni28F7uni28F8uni28F9uni28FAuni28FBuni28FCuni28FDuni28FEuni28FFuni2906uni2907uni290Auni290Buni2940uni2941uni2983uni2984uni29CEuni29CFuni29D0uni29D1uni29D2uni29D3uni29D4uni29D5uni29EBuni29FAuni29FBuni2A00uni2A01uni2A02uni2A0Cuni2A0Duni2A0Euni2A0Funi2A10uni2A11uni2A12uni2A13uni2A14uni2A15uni2A16uni2A17uni2A18uni2A19uni2A1Auni2A1Buni2A1Cuni2A2Funi2A7Duni2A7Euni2A7Funi2A80uni2A81uni2A82uni2A83uni2A84uni2A85uni2A86uni2A87uni2A88uni2A89uni2A8Auni2A8Buni2A8Cuni2A8Duni2A8Euni2A8Funi2A90uni2A91uni2A92uni2A93uni2A94uni2A95uni2A96uni2A97uni2A98uni2A99uni2A9Auni2A9Buni2A9Cuni2A9Duni2A9Euni2A9Funi2AA0uni2AAEuni2AAFuni2AB0uni2AB1uni2AB2uni2AB3uni2AB4uni2AB5uni2AB6uni2AB7uni2AB8uni2AB9uni2ABAuni2AF9uni2AFAuni2B00uni2B01uni2B02uni2B03uni2B04uni2B05uni2B06uni2B07uni2B08uni2B09uni2B0Auni2B0Buni2B0Cuni2B0Duni2B0Euni2B0Funi2B10uni2B11uni2B12uni2B13uni2B14uni2B15uni2B16uni2B17uni2B18uni2B19uni2B1Auni2B1Funi2B20uni2B21uni2B22uni2B23uni2B24uni2B53uni2B54uni2C60uni2C61uni2C62uni2C63uni2C64uni2C65uni2C66uni2C67uni2C68uni2C69uni2C6Auni2C6Buni2C6Cuni2C6Duni2C6Euni2C6Funi2C71uni2C72uni2C73uni2C74uni2C75uni2C76uni2C77uni2C79uni2C7Auni2C7Buni2C7Cuni2C7Duni2D30uni2D31uni2D32uni2D33uni2D34uni2D35uni2D36uni2D37uni2D38uni2D39uni2D3Auni2D3Buni2D3Cuni2D3Duni2D3Euni2D3Funi2D40uni2D41uni2D42uni2D43uni2D44uni2D45uni2D46uni2D47uni2D48uni2D49uni2D4Auni2D4Buni2D4Cuni2D4Duni2D4Euni2D4Funi2D50uni2D51uni2D52uni2D53uni2D54uni2D55uni2D56uni2D57uni2D58uni2D59uni2D5Auni2D5Buni2D5Cuni2D5Duni2D5Euni2D5Funi2D60uni2D61uni2D62uni2D63uni2D64uni2D65uni2D6Funi2E18uni2E22uni2E23uni2E24uni2E25uni2E2Euni4DC0uni4DC1uni4DC2uni4DC3uni4DC4uni4DC5uni4DC6uni4DC7uni4DC8uni4DC9uni4DCAuni4DCBuni4DCCuni4DCDuni4DCEuni4DCFuni4DD0uni4DD1uni4DD2uni4DD3uni4DD4uni4DD5uni4DD6uni4DD7uni4DD8uni4DD9uni4DDAuni4DDBuni4DDCuni4DDDuni4DDEuni4DDFuni4DE0uni4DE1uni4DE2uni4DE3uni4DE4uni4DE5uni4DE6uni4DE7uni4DE8uni4DE9uni4DEAuni4DEBuni4DECuni4DEDuni4DEEuni4DEFuni4DF0uni4DF1uni4DF2uni4DF3uni4DF4uni4DF5uni4DF6uni4DF7uni4DF8uni4DF9uni4DFAuni4DFBuni4DFCuni4DFDuni4DFEuni4DFFuniA644uniA645uniA646uniA647uniA64CuniA64DuniA650uniA651uniA654uniA655uniA656uniA657uniA662uniA663uniA664uniA665uniA666uniA667uniA668uniA669uniA66AuniA66BuniA66CuniA66DuniA66EuniA68AuniA68BuniA68CuniA68DuniA694uniA695uniA708uniA709uniA70AuniA70BuniA70CuniA70DuniA70EuniA70FuniA710uniA711uniA712uniA713uniA714uniA715uniA716uniA71BuniA71CuniA71DuniA71EuniA71FuniA726uniA727uniA728uniA729uniA72AuniA72BuniA730uniA731uniA732uniA733uniA734uniA735uniA736uniA737uniA738uniA739uniA73AuniA73BuniA73CuniA73DuniA73EuniA73FuniA746uniA747uniA748uniA749uniA74AuniA74BuniA74EuniA74FuniA780uniA781uniA782uniA783uniA789uniA78AuniA78BuniA78CuniA7FBuniA7FCuniA7FDuniA7FEuniA7FFuniF000uniF001uniF6C5uniFB00uniFB03uniFB04uniFB05uniFB06uniFB13uniFB14uniFB15uniFB16uniFB17uniFB1DuniFB1EuniFB1FuniFB20uniFB21uniFB22uniFB23uniFB24uniFB25uniFB26uniFB27uniFB28uniFB29uniFB2AuniFB2BuniFB2CuniFB2DuniFB2EuniFB2FuniFB30uniFB31uniFB32uniFB33uniFB34uniFB35uniFB36uniFB38uniFB39uniFB3AuniFB3BuniFB3CuniFB3EuniFB40uniFB41uniFB43uniFB44uniFB46uniFB47uniFB48uniFB49uniFB4AuniFB4BuniFB4CuniFB4DuniFB4EuniFB4FuniFB52uniFB53uniFB54uniFB55uniFB56uniFB57uniFB58uniFB59uniFB5AuniFB5BuniFB5CuniFB5DuniFB5EuniFB5FuniFB60uniFB61uniFB62uniFB63uniFB64uniFB65uniFB66uniFB67uniFB68uniFB69uniFB6AuniFB6BuniFB6CuniFB6DuniFB6EuniFB6FuniFB70uniFB71uniFB72uniFB73uniFB74uniFB75uniFB76uniFB77uniFB78uniFB79uniFB7AuniFB7BuniFB7CuniFB7DuniFB7EuniFB7FuniFB80uniFB81uniFB8AuniFB8BuniFB8CuniFB8DuniFB8EuniFB8FuniFB90uniFB91uniFB92uniFB93uniFB94uniFB95uniFB9EuniFB9FuniFBD9uniFBDAuniFBE8uniFBE9uniFBFCuniFBFDuniFBFEuniFBFFuniFE00uniFE01uniFE02uniFE03uniFE04uniFE05uniFE06uniFE07uniFE08uniFE09uniFE0AuniFE0BuniFE0CuniFE0DuniFE0EuniFE0FuniFE20uniFE21uniFE22uniFE23uniFE70uniFE71uniFE72uniFE73uniFE74uniFE76uniFE77uniFE78uniFE79uniFE7AuniFE7BuniFE7CuniFE7DuniFE7EuniFE7FuniFE80uniFE81uniFE82uniFE83uniFE84uniFE85uniFE86uniFE87uniFE88uniFE89uniFE8AuniFE8BuniFE8CuniFE8DuniFE8EuniFE8FuniFE90uniFE91uniFE92uniFE93uniFE94uniFE95uniFE96uniFE97uniFE98uniFE99uniFE9AuniFE9BuniFE9CuniFE9DuniFE9EuniFE9FuniFEA0uniFEA1uniFEA2uniFEA3uniFEA4uniFEA5uniFEA6uniFEA7uniFEA8uniFEA9uniFEAAuniFEABuniFEACuniFEADuniFEAEuniFEAFuniFEB0uniFEB1uniFEB2uniFEB3uniFEB4uniFEB5uniFEB6uniFEB7uniFEB8uniFEB9uniFEBAuniFEBBuniFEBCuniFEBDuniFEBEuniFEBFuniFEC0uniFEC1uniFEC2uniFEC3uniFEC4uniFEC5uniFEC6uniFEC7uniFEC8uniFEC9uniFECAuniFECBuniFECCuniFECDuniFECEuniFECFuniFED0uniFED1uniFED2uniFED3uniFED4uniFED5uniFED6uniFED7uniFED8uniFED9uniFEDAuniFEDBuniFEDCuniFEDDuniFEDEuniFEDFuniFEE0uniFEE1uniFEE2uniFEE3uniFEE4uniFEE5uniFEE6uniFEE7uniFEE8uniFEE9uniFEEAuniFEEBuniFEECuniFEEDuniFEEEuniFEEFuniFEF0uniFEF1uniFEF2uniFEF3uniFEF4uniFEF5uniFEF6uniFEF7uniFEF8uniFEF9uniFEFAuniFEFBuniFEFCuniFEFFuniFFF9uniFFFAuniFFFBuniFFFCuniFFFDu1D300u1D301u1D302u1D303u1D304u1D305u1D306u1D307u1D308u1D309u1D30Au1D30Bu1D30Cu1D30Du1D30Eu1D30Fu1D310u1D311u1D312u1D313u1D314u1D315u1D316u1D317u1D318u1D319u1D31Au1D31Bu1D31Cu1D31Du1D31Eu1D31Fu1D320u1D321u1D322u1D323u1D324u1D325u1D326u1D327u1D328u1D329u1D32Au1D32Bu1D32Cu1D32Du1D32Eu1D32Fu1D330u1D331u1D332u1D333u1D334u1D335u1D336u1D337u1D338u1D339u1D33Au1D33Bu1D33Cu1D33Du1D33Eu1D33Fu1D340u1D341u1D342u1D343u1D344u1D345u1D346u1D347u1D348u1D349u1D34Au1D34Bu1D34Cu1D34Du1D34Eu1D34Fu1D350u1D351u1D352u1D353u1D354u1D355u1D356u1D538u1D539u1D53Bu1D53Cu1D53Du1D53Eu1D540u1D541u1D542u1D543u1D544u1D546u1D54Au1D54Bu1D54Cu1D54Du1D54Eu1D54Fu1D550u1D552u1D553u1D554u1D555u1D556u1D557u1D558u1D559u1D55Au1D55Bu1D55Cu1D55Du1D55Eu1D55Fu1D560u1D561u1D562u1D563u1D564u1D565u1D566u1D567u1D568u1D569u1D56Au1D56Bu1D5A0u1D5A1u1D5A2u1D5A3u1D5A4u1D5A5u1D5A6u1D5A7u1D5A8u1D5A9u1D5AAu1D5ABu1D5ACu1D5ADu1D5AEu1D5AFu1D5B0u1D5B1u1D5B2u1D5B3u1D5B4u1D5B5u1D5B6u1D5B7u1D5B8u1D5B9u1D5BAu1D5BBu1D5BCu1D5BDu1D5BEu1D5BFu1D5C0u1D5C1u1D5C2u1D5C3u1D5C4u1D5C5u1D5C6u1D5C7u1D5C8u1D5C9u1D5CAu1D5CBu1D5CCu1D5CDu1D5CEu1D5CFu1D5D0u1D5D1u1D5D2u1D5D3u1D7D8u1D7D9u1D7DAu1D7DBu1D7DCu1D7DDu1D7DEu1D7DFu1D7E0u1D7E1u1D7E2u1D7E3u1D7E4u1D7E5u1D7E6u1D7E7u1D7E8u1D7E9u1D7EAu1D7EB dlLtcaronDieresisAcuteTildeGrave CircumflexCaron uni0311.caseBreve Dotaccent Hungarumlaut Doubleacute arabic_dot arabic_2dots arabic_3dotsarabic_3dots_aarabic_2dots_a arabic_4dots uni066E.fina uni066E.init uni066E.medi uni06A1.fina uni06A1.init uni06A1.medi uni066F.fina uni066F.init uni066F.medi uni06BA.init uni06BA.medi arabic_ring uni067C.fina uni067C.init uni067C.medi uni067D.fina uni067D.init uni067D.medi uni0681.fina uni0681.init uni0681.medi uni0682.fina uni0682.init uni0682.medi uni0685.fina uni0685.init uni0685.medi uni06BF.fina uni06BF.init uni06BF.mediarabic_gaf_barEng.altuni0268.dotlessuni029D.dotless uni03080304 uni03040308 uni03070304 uni03080301 uni03080300 uni03040301 uni03040300 uni03030304 uni0308030C uni03030308 uni030C0307 uni03030301 uni03020301 uni03020300 uni03020303 uni03060303 uni03060301 uni03060300 uni03060309 uni03020309 uni03010307 brailledotJ.alt uni0695.finauniFEAE.fina.longstart uni06B5.fina uni06B5.init uni06B5.medi uni06CE.fina uni06CE.init uni06CE.medi uni0692.final.alt uni06D5.finauni0478.monographuni0479.monographiogonek.dotlessuni2148.dotlessuni2149.dotlessuni1E2D.dotlessuni1ECB.dotlessdcoI.alt arrow.base uni0651064B uni0651064C uni064B0651 uni0651064E uni0651064F uni064E0651 uni0654064E uni0654064F uni07CA.fina uni07CA.medi uni07CA.init uni07CB.fina uni07CB.medi uni07CB.init uni07CC.fina uni07CC.medi uni07CC.init uni07CD.fina uni07CD.medi uni07CD.init uni07CE.fina uni07CE.medi uni07CE.init uni07CF.fina uni07CF.medi uni07CF.init uni07D0.fina uni07D0.medi uni07D0.init uni07D1.fina uni07D1.medi uni07D1.init uni07D2.fina uni07D2.medi uni07D2.init uni07D3.fina uni07D3.medi uni07D3.init uni07D4.fina uni07D4.medi uni07D4.init uni07D5.fina uni07D5.medi uni07D5.init uni07D6.fina uni07D6.medi uni07D6.init uni07D7.fina uni07D7.medi uni07D7.init uni07D8.fina uni07D8.medi uni07D8.init uni07D9.fina uni07D9.medi uni07D9.init uni07DA.fina uni07DA.medi uni07DA.init uni07DB.fina uni07DB.medi uni07DB.init uni07DC.fina uni07DC.medi uni07DC.init uni07DD.fina uni07DD.medi uni07DD.init uni07DE.fina uni07DE.medi uni07DE.init uni07DF.fina uni07DF.medi uni07DF.init uni07E0.fina uni07E0.medi uni07E0.init uni07E1.fina uni07E1.medi uni07E1.init uni07E2.fina uni07E2.medi uni07E2.init uni07E3.fina uni07E3.medi uni07E3.init uni07E4.fina uni07E4.medi uni07E4.init uni07E5.fina uni07E5.medi uni07E5.init uni07E6.fina uni07E6.medi uni07E6.init uni07E7.fina uni07E7.medi uni07E7.init Ringabove uni2630.alt uni2631.alt uni2632.alt uni2633.alt uni2634.alt uni2635.alt uni2636.alt uni2637.alt uni047E.diacuni048A.brevelessuni048B.brevelessy.alt uni02E5.5 uni02E6.5 uni02E7.5 uni02E8.5 uni02E9.5 uni02E5.4 uni02E6.4 uni02E7.4 uni02E8.4 uni02E9.4 uni02E5.3 uni02E6.3 uni02E7.3 uni02E8.3 uni02E9.3 uni02E5.2 uni02E6.2 uni02E7.2 uni02E8.2 uni02E9.2 uni02E5.1 uni02E6.1 uni02E7.1 uni02E8.1 uni02E9.1stem@%2%%A:B2SAS//2ݖ}ٻ֊A}G}G͖2ƅ%]%]@@%d%d%A2dA  d   A(]%]@%..%A  %d%@~}}~}}|d{T{%zyxw v utsrqponl!kjBjSih}gBfedcba:`^ ][ZYX YX WW2VUTUBTSSRQJQP ONMNMLKJKJIJI IH GFEDC-CBAK@?>=>=<=<; <@; :987876765 65 43 21 21 0/ 0 / .- .- ,2+*%+d*)*%)('%(A'%&% &% $#"!! d d BBBdB-B}d       -d@--d++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++mapproxy-1.11.0/mapproxy/image/fonts/DejaVuSansMono.ttf000066400000000000000000011624541320454472400231400ustar00rootroot00000000000000 FFTMQ|,GDEFnSywHGPOS\8GSUBSUT:OS/2i?`Vcmap_ωU?zcvt  U40fpgm[kWdgaspX glyf薥X$head-)@6hheax$hmtx ,rlocazX1hmaxp'G name`G!postЩhuprep:ɲ4rmrm $%kllmno|}       X arab&cyrl4dfltFgrekPlao \latnj SRB 4ISM 4KSM 4LSM 4MOL 4NSM 4ROM 4SKS 4SSM 4mark mark,mark4mkmk:rtbdB &.6>FPX`Rx | \447:" p#h&0  c"c" ]j]jbj $6HZl~ P|H 0 P|H <P ,  T`  P|H    #hhhn  "(.4:@FLRX^djpv| $*06<BHNTZ`flrx~ &,28>DJPV\bhntz "(.4:@FLRX^djpv| $*06<BHNTZ`flrx~ &,28>DJPV\Xd|dh0`tXtX <|00hL( Tplx`xDJPV]j]j]j]j]jbj]j]jh|  &,28>DJPV\bhntz "(.4:@FLRX^djpv| $*06<BHNTZ`flrx~ &,28>DJPV\bhntz "(.4:@FLRX^djpv| dt hd8|$|`HLD \t@4H(|XT(lt<0xx_` iFWW@TT[:B`LLL[Z[IWTL4K]l8@^  @bXXXXoM!TxxTT&~O[ XPXD,aDf\Ph< \\\0,0@, d `|tL`00@\  T`@L845D80H <D<x(LX$Tt0TLh0pLh<pl$d`0h8< PLdhx8TR m$v2z`M;)RDhp_)I R|o~4>>#AD$  K( N Qd T Wh g gl j mm p q M M R R T T V W  !"$ &,28>DJPV]j]j]j]j]jbj]j]jh&  &,28>DJPV\bhntz "(.4:@FLRX^djpv| $*06<BHNTZ`flrx~ &,28>DJPV\bhntz "(.4:@FLRX^djpv| $*06<BHNTZ`flrx~DhxzOhDh*hDhhhhDhhhvhhVhYDxhVVhxhhsDVhDDDDDDzzzzhhhh6hhhhhhhhhhh46vvvvhhhhhhhhhhhhhhhVVVDD6h6hzvzvzvzvhVhVhVhVhhhhhhhhYDxxxx9hVhVhhhhhhDDhhhhhhhxhxhhhhhhhhhhshVhhhhhhhhhhh 66hhhVVh"VhVhVhhhDDxVhhXVVjhhhhDDVh$=D]4:IWfoqr "#&36k  |4pb  "(.4:@FLRX^djpv| $*06<BHNTZ`flrx~      & , 2 8 > D J P V \ b h n t z     " ( . 4 : @ F L R X ^ d j p v |     $ * 0 6 < B H N T Z ` f l r x ~      & , 2 8 > D J P V \ b h n t z     " ( . 4 : @ F L R X ^ d j p v |  $*06<BHNTZ`flrx~ &,28>DJPV\bhntz "(.4:@FLRX^djpv| $*06<BHNTZ`flrx~ &,28>DJPV\bhntz "(.4:@FLRX^djpv| $*06<BHNTZ`flrx~ &,28>DJPV\bhntz "(.4:@FLRX^hhhh66zzhhhhhhhhhhhhhhhh66hhhhhhhhhhhhhhh`hhh`hhv`vhhh`hVhhhhV22h`hh`hh`hh`hVh`hVh`hh`hhhh`hh`hh`hh`hh`hVh`hhhhhhhzzzzzzhhhh66hhhhhhhhhhhhh6666hhhhhhh`h`vvvvhhhhhhhhhhhh`hhhhhhVhhVhVhhhhh`6h66hhzvzvzvzv`zvhVhVhVhVhhhhhhhhhhhhhhh`hhVh`2222222hhhh`hhhhVh`hVhhhhhhhhh`h6h6h`6h`hhhhhhh`hhhhhhhhhhhhhhhhhhhhhh`hhhhVhhhhhhhhhhhhhhhhhh  `6666hhhhh`hVVVhhVhhVhhVhhhhhhhh22hhhhhhVh`hVhh`hhh`hVh`hV66Vhhh`hhhhhh`hVhh66h`hh`hh`hh`h)`)`YYhhv`vv`v`v`vZ`Zr`rv`v`V>>Vh`hV`n`nVh`hh`hVooooVD`Dh`h88PP>>Vh`hh`hVh`hV`Vf&Vh`hh`hh`hh`hhhVh`hhh6`6Vh`hVh`hVc`ch`h``h`hVssVssVx`xVssV`VhhVh`hh`hh`hh`hh`hhhh`h`Vh`hh`hVh`hV$$$$h`h ` >`>h`hh`hVh`hhhV$$h`hh`h`h`v`h`h`h`h`h`h`h`h`h`h``h`h`hh`h`h`h`h`h`h`h`5`h`h``h`''}{}hhRREE^^rrhhhh??aaiihhhhhhGGhhhhhh6\aadd55g'ee``66LhViihhh`hh $=D]4Kjsxk 4;9=AaCGffnkpt   J J"+12 $*06<BHNTZ`flrx~h`h`h`h`h`h`h`h`h`h`h`h`h`h`h`h`h`h`h`h`h`hhhhhhhhhhhhhhhhhhhhhhhhhhh`h`h`/-3 ntz "(.4:@FLRX^djpv|Oillo|>DJPV\bhntztwwwwt`~~`~`/llo| Parab&cyrl:dfltVgrek`lao llatnv SRB 4ISM >KSM >LSM >MOL JNSM >ROM JSKS >SSM > ccmp>dligDfinaJinitPligaVlocl\loclbloclhmedinrligt   (08@HPX`hXjn  ~  ~  !$% > $$XLMLM&   gp5 i k m o q u w { } %    !  5 1 9 = C A ) E I O U M 4D# U U4T' s y  S '    #  7 3 ; ? + G K Q W 4=@D"T' r x ~ R &    "  6 2 : > * F J P V 4=@D"   u  u > $  o  k  i  o  k  i  O LI LM33f  "(PfEd@ m`,p, ~!AEM?CXauz~_s  :UZmt{ .<[ex{-KcwEMWY[]}  # & 7 : > I _ q !!!!!!!"!$!&!+!.!_" """" "-"="i"""""""####!#(#+#5#>#D#I#M#P#T#\#`#e#i#p#z#}#####$#&/&&&&'' '''K'M'R'V'^'u''''''))*/+,d,o,w,z,}..%..t $DLPCXatz~r  !@Z`ty~,0>bw{0Th| HPY[]_   & / 9 < E _ p t !!! !!!!"!$!&!*!.!S!"""""'"8"A"m""""""#####%#+#5#7#A#G#K#P#R#W#^#c#h#k#s#}#####$#%&8&&&''' ')'M'O'V'X'a''''''))*/+,d,n,u,y,|.."..RpvnfTPMHGFED6$xUnlcNM {kih]_YMH87530.%$"  BA?>=<986ށxsrqcccH jX  ~b!$ADELMP_fjlpy?CCXXaatuzz~~_&rs    !:@UZZ$`m%tt3y{4~7:<>?@ABCDEOQRTUVZadefhuw} ,.0<>[bewx{{ -!0K7TcShwc|s EHMPWYY[[]]_}1fu     # & & / 7 9 : < > E I _ _ p q t   !!!!! !!!!!!!!"!"!$!$!&!& !*!+!!.!.#!S!_$!" 1"""""""" "'"-"8"="A"i"m"""""""$""&""'""7##8##?##M##!O#%#(U#+#+Y#5#5Z#7#>[#A#Dc#G#Ig#K#Mj#P#Pm#R#Tn#W#\q#^#`w#c#ez#h#i}#k#p#s#z#}#}##########$#$#%&/&8& && 2&& ?&& A'' C'' G' '' K')'K g'M'M 'O'R 'V'V 'X'^ 'a'u '' '' '' '' '' '' )) )) */*/ ++ ,d,d ,n,o ,u,w ,y,z ,|,} .. .".% ....     R  @ L N R Tpt Xv ] p֣     !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`ardeixpkvjsgwl|cnTm}b x:   yfvqrstzwugf 7X!uu9!{Xm{o{RoZ!=fs +b#1N {T\q#w`j#fy```{{w`b{!{RNNfffHF?{L'oo5jo{-{3=foD7f}s, %Id@QX Y!-,%Id@QX Y!-,  P y PXY%%# P y PXY%-,KPX EDY!-,%E`D-,KSX%%EDY!!-,ED-,%%I%%I` ch #:e:-hh/10!%!!hsr) @ <2991/03#3#qeR@1<20###Ѯ++J@0     91/<<<<<<<2220333!3!###!5!!5!#3hiiThiih)T/hTaabbNZ /d@9($)%/%$(!$, ( 0<2<1/299990>54&'#.'5.546753.'n|phumdfbdcӿdOTUPDNtd]gp^Vd-.)>B+/Qš ! *9V@/7(" "7(.+  % 4  + :99991/9999032654&#"4632#"&'%32654&#"4632#"&iNMklLNi@s..2H#)iOMllMMk@u--1?NjkMMljO0./t?``OikMMkjN0--uA9*7@b  -,.+2345617B7 1 +"1"!% (! 7+!(!(! .899999991/9990KSX999Y" >54/3#'#"5467.54632.#"3267>7#'&JKNSj抋20ǭAF;}Eap:6\[ț*\,#1h F'XmFD ̉dHG'%[M;I\ 10#+u @  29910#&547u;:\' @  299103#654\<<J+N@,       <2<2991<22990 %#'-73%+f9s9f9PsPbybcyXqy '@    <<1<<0!!#!5!CDDD/@ 103#Śc/dm10!!d 11/03#1fB7@ 103#ymL # @  $!"!$10@////////// / / ?????????? ? ? OOOO O ____ _    F////////// / / __________ _ _  $]]4632#"&"32'2#"M68PO98K7PP78NL0670xx~F &@ ## 1/20%!5%3!!:P6ȪuLJժ#Q@)%%B   "$91/20KSX92Y"%!!567>54&#"5>32uu5dF[pga Yd8ժ.>zO}BC12`tA7(G@)  #)&" )9190#"&'532654&+532654&#"5>32ggfbYhyI'Ǖ&$54zms{(( ۵{fo B@   B    $<291/<290KSXY" !33##!5)!3d-=@"   "190!!>32#"&'532654&#"+W,wN\aQFժ 21%%L$=@#  %"& "%190.#">32# !2"32654&?M0n#J݁%'dkuzl75@%%B"991/0KSXY"!#!5V+N #/C@% '-'0 $*$ "!0991990"32654&%.54632#"$54632654&#"hʁ򑁖Myz{yŗT!Ѳ!!ȟɠbx~~xzF $;@" ""%"  &%1902654&#"532#"543 !"&T?M/nI%'!dk  os'@ <21/03#3#'9' %@  103#3#Śc /Xyw!@('29190 5yR!÷X`y@ <210!!!!X!! BXyw!@('<919055X!R^^="{@B  %%B !    ) #99991/9990KSX99Y"#546?>54&#"5>323#=TZ>/mNb^hC^XE&bRY;X1YnED98L\VBT=/s 4p@1(+$ 4 '$+1+5' ( + . !+ -.5<991999990@ ]4&#"326#5#"&5463254&#"!267# !2kkkk%RӡP$J6l90?{:]x<!o?DF=?z% @A%%%% % % %  % B   / 91/<90KSXY"]@    ]]!3#!#hnl#+{q =@#   21 0!29991/9032654&#32654&#%!2)qﰖ뒃JF{f>p}qdƵϠ1.@  2 10210%# !2.#"32671M[?[MJVXI5))pn))=@@=R(@  2 1099991/0% 6&!# )`dVDѦHKw/N )@  13 21/0!!!!!!vTrwժFX $@14 21/0!!!!#o\eժH7fP<@!   6251990%# !2.#"3267#5!PQv@^PQ_ſCe){KMon56MI!H &@ 1 0 221/<203!3#!#)d+9 %@ 77 221/220!!!!5!!=99ժm,@    51990753265!5!#"&m[hqG`=QQD, @!% %B  0 291/<290KSXY"]@L&&6FUWX dzy{ ',+&:IGG[WXXWkzx]]33 ##wVhs@ 141/03!!dժVy @,  B    / 0 91/<290KSXY" ]@$  &)&) 6968  ]]! !###V+'F m@B10 991/<2990KSXY"]@&)&8Wdjuz &)FIWgh]]!3!#3+3u\ #@ 2 62510#"32#"32IIz~u+@  2 8 3291/032654&#%!2+#ꌝL/ϔu\=@   2 625999919990"#"32#"32ȗyHdIj@8  %%B     21  0299991/<9990KSX9Y"#.+#!232654&#NnRٲM{cݑohy]ҔYJ'@=  %  %B %( &919"0(9999190KSX99Y"]@ ]].#"#"&'532654&/.54$32\^mjikshulм V;53#"&'.  yVWx! 9FBjiCE:= m];<<;\lh?;::;>9L@)%%%%B/091/290KSXY"%3#3h_KKѪ++ @D    %%% % B    /91/<<90KSXY" ]@^ //+ ??8 ZZ  &*%*(+ % & 5:5:;: 4 6 TTZXWV[[RW X ] gh yvy v #]]333# #ŏӬ߿ʿD"+w @K % % % %%%% % B   ;/; 0 91/<290KSXY"7]@8  '()& X xyw !%+% 5UYX es]]3 3 # #VHNAu3B}%Y@.%%%%B<< 9991/290KSXY"3 3#%lk!mb E@%%B/0 991/0KSXY"]]!!!5!" ՚ow@=210!#3!XfB7@ 10 #%mZ@=210!53#5XޏH@ 91290 # #Ȳu-m/10!5/mPPf%@ <1K TKT[X@8Y0 #fx#{ )n@*  ! $   D >*22991/99990@00 0!0" ]#"326757#5#"&546;5.#"5>32=zl;^[fX=& 3qpepӺ)Ld_y64''RR2X 0@  G F221/9904&#"326>32#"&'#3,fd./xRXWS%{/@   F210%# !2.#"3267%JR%QNI]`A9++88*,A:;>{0@G H221/9903#5#"3232654&#"Z.deCSW;7W {X{E@&    IH991990!3267# 32.#"X㿮Xmi[ ^Z89++9 @Ţ'4@     <<2991/22990#"!!#!5!5463'cM+Qgc/яN{H{ )H@' '  $(*' G!H*221999904&#"326#"&'5326=#"3253ZLSbC,ml/9.,}^\::VZ,@  J  F21/<990#4&#"#3>32jq1sJ`cD .@ L LK <<1/20!!!5!!3#mnm`/BCV 8@   <2991990!5!+53263#XZZӜ} @:  B  DE 291/<90KSXY"]@R546Ffuv ('(;;797JIYYkiiiyxyy]]33 ##Gb{ZFB?  &@   L 991/990;#"&5!5![Y饵|~mo{"@'  MNMNME#K TKT[X8Y<91/<<<299990@G000000 0 0 ????????? #]>32#4&#"#4&#"#3>32"iJo5FP;9JI9!c?LeHEws{p{``32jq1sJ``cH{ #@  D>10"32654&'2#"hڜ-.VT{3@ GF221990%#3>32#"&4&#"326w.df SWWRw 3@   G>22199032654&#"#"3253#L-ed.+SY7:WSj{O@   21/990@%  0030@@C@PPPP].#"#3>32;zI.Dv6y.*`w"${'u@@    B %( OI"E(99991990KSX99Y".#"#"&'532654/.54632OS}{\JSjgTzEZ9..QSKJ#}##55cY1!1@  <<2991/<2990!!;#"&5!5!f^^uϪ+|b`>^,@    JF21/2990332653#5#"&økp1qJyaddm`e@)BIE91/290KSXY"']@%]]3 3#dEFr`T` @E      B    /91/<<90KSXY" ]@      &&)&))#, 96993< EI F J VX W Y fifij e vzx| r -   ++>>< H Y jih {yz|  ]]333# #àö`wBfL` @H      B  IE 91/<290KSXY" ]@ fivy  :4 ZV ]] # # 3 ^oo)'`?HkhV`@E       B   IE9129990KSX9Y"8]@v  &&8IIY ]]+532673 3Z.Gc".\mQ\GOLGhu:NN^Nlb X@BIE 2991/0KSXY"8]@68EJWXejuz ]!!!5!-}bܖ%$f@5 %   !  % $  = %<<29999999199999990#"&=4&+5326=46;#"3@k>>j@FU[noZUtrݓWV10#$j@7%   #%#= %<2<9999999199999990326=467.=4&+532;#"+DVZon[VD>k@@k>XXrtݔXy &@  '1<2990#"'&'.#"5>32326yKOZq Mg3OINS5dJ t]F ;73 !;?<6 7= @ <2991/0533)eq%!N@*   " E"<<<2212<990.'>7#&5473%C??BI9gg9ބ5(,-("9="+` 츸X>@     <<1/2<2990.#"!!!!53#5354632D>Cs3A,,ُ/яLB /@ (-  * -'! @') -0)$ !'$* xyx( $02299999999912299999999904&#"3267'#"&''7.5467'7>32d|[Z}}Z[|Z^.[20`0\^.[3.^Z{{Z\}~t]1]02[-^Z3].2]-_%@D% % %%B  < e e<<2<299991/2<2<290KSXY"3 33!!!#!5!5'!53%lkVoqZmo#o o#o!<210##  = 2>j@<#$93 $*0?#54&S9akԂ[]=:̭IWW9fqր][<;ȧH>=><''PGZsweZ54m@''TLf{xf[1,pE-Z/L-Z/L?F@aa1<203#%3#?}N1ID@'  &>>2J\ ^,8 8YD/210.#"3267#"&54632'"3267>54&'.'2#"&'.5467>`:o:u8g24r=г=rjKKMMKLijLLKLKKkZZ\[[[~}[[[\ZZ/lhȬJKKjhKLLLLLijKKJgZZ[~}[[[[[[}~[ZZ %)d@6  (&&  #*& (' j kji*22999199990"326=7#5#"&546;54&#"5>32!!|WHi1Rwu3}CD?kdPDN@Ms=@pABtZ\#! {w# /@    v v<2991<2990 5 5L-+-+#RRXsy^@ '10!#!X!^?dm10!!d }N4L@I  ] ] B   A)5)M  \\ [G#X;#Y//29999129990KSX9Y"2#'.+##32654&2#"&'.5467>"3267>54&'.XXP:&rk1=-7ffZJJDZZ\[[[~}[[[\ZZ~jKKMMKLijLLKLKKLbeG]C;P*T6?>5VZZ[~}[[[[[[}~[ZZgJKKjhKLLLLLijKKJ=b10!!=V+u @ STS 102#"&546"32654&hAu,-/OomOPqp1.-rBoPPlnNOpXy.@    <2<21/<<07!!!!#!5!X!dCDLIB}a@WWBA     @9991990KSX9Y"!!57>54&#"5>32eQdR1q?Ay;Jwrnaz3=L$$}k9wuF(\A          @#) & )99190#"&'532654&+532654&#"5>32^c:r;Eq-evnmBJ]b`W,p;Eu2X`lP|yQDJLl?<8?yvcG]f%@ <1K TKT[X@8Y03#fT` L@*  !   JF!99912<9903326533267#"&'#"&'øxo ! &D">K .Y\,T H;8 OOPNLPj; #@WW1 9120!###.54$FfNݸ/`103#`u)X 9A      @  aW}a 12035733!j c)t'+n 3@   jkji9910"32654&'2#"&546!!hfssfeusgʫ˫\{u༻߻`{\# /@   vv <<991<2990 5 %5 +-:+-#^R^  ^R^  Z{'V'{ /Z{'{& /tVZ'V'u /!%@G  %%B! "$ $&# # )"#&999919990KSX99Y"33267#"&546?>54565#53%=TZ>/mNb^hC^XC&DbRY;X1YnED98L\V@T?%k&$ ,u@O ]1%k&$ *u@O ]1%m&$ -u  +@ /  ]1%^&$ +u# +@O#@]1%N&$ )u +@p0? /]1%m !@W % %%% %!%! %!! % !B     !  PPK/K!"2299999991/<9990KSXY"]@  ]]4&#"326!.54632#!#Y?@WX??Y:Arr@;nlZ?YWA?XXP!yIrrIv${g@7 % %%%B    c /<291/<20KSXY"!!!!!!#!3eex5ժFժu1&d&Nk&( ,uNk&( *uNm&( -uNN&( )uk&, ,uk&, *um&, -u  Ic:1N&, )u+1N ;@!    21 0 0<291/220 )#53 6&!#!!VD}}/`ŕ{HK+Fb&1 +y"+@O"@]1u\k&2 ,u@O]1u\k&2 *u@O]1u\m&2 -u +@ /]1u\^&2 +u 0!+@O0@!]1u\N&2 )u +@p0? /]1;T .@     <91<290 7   ^t^_t\t%\^u^uw^ +k@:+)&  *&&, #* #)+262#5,999999991/9999990324&' .#"#"&''7&5327sT sV)+y=g %s9d/NZIn-QUPeQzQQFIRPJ=k&8 ,u@O]1=k&8 *u@O]1=m&8 -u $+@ $/ $ ]1=N&8 )u$!+@p!$0!?$ !/$!$]1%k&< *u@ ]14 @  28  32299991/032654&#33 !##ꞝL!󄃃}/V@1-'!  **.  !' $'$-DF099991/9904632#"&'532654&/.5467.#"#7C:oEBL;lAxC\[yqyrq|d1M*%]taQG_J'8O#kr#f&DC#f&Dv#f&Df#7&Dv#&Dj#&Dt){ C@I=70 6 %C "76. 3@:("D%=/.M/u MCM6+sD299912<2<2<999990@ 05060708]5#"32654&#"!3267#"&'#"&546;54&#"5>32>321xYS\JMWWLepO27Gn 'aȿuc^8>M<[|%!YHZqYa4+#"33)+RNPPXx+'#!?@=Bu%{&hF{Xf&HC{Xf&Hv{Xf&Hf{X&Hj@@]1Df&CDf&vDf&f @@ 0 ]1D&j +1H)@O B $ *'! !'D! >*999999199990KSX9Y"#"32.''7'3%.#"32654&Ŷ"#!H&!!#R-:/(  (-Y,\bPȑ^b n7&QvHf&RCHf&RvHf&Rf+@]1H7&Rv. +@ 0 ?. /. .]1H&Rj +@ p_PO@]1Xyo '@ w <<103#3#!!j!/ +s@>+,&  )&  *&& &,+,* # )#D>,99999999199999990 32654&'.#".5327#"&''m1$eA H#cC')d<]*,g9\ //4o0.0tGGq.78MBz;32#"&4&#"326w.dfSWWhV&\j%0& 6$ +@ @O /]1#&D%m& 1$+@ _PO@/ ]1#H&D%u'u$ur{'uYD1k&& *Zu%f&FvZ1t' -~|&%f&fZF1P& 2K&%&KF1m&& .Zu%f&FgZRg&' .o{ ' (:G @8@]1N{$H@ "  @"   GH%<<1/<2990!5!533##5#"3232654&#"Z1.de5yySW;7W N0& 6({X&#HNm& 1({XH&HNP& 2({X&HuN'u1({uX{'uHNg&( .$o{Xa&Hg#fPm' -u*{Hf&fJfPm& 12*{HH&JfPP& 22*{H&JfP'*{HN'.JHm' -u+ +@ /]1m' -uKKQX@8Y@p`O]0?@!     1 0<22<221/<2<<2203!533##!##53!5*ʇʆ*9QF?@"   J  F<221/<<2990#4&#"##5353!!>32jq}}a1sJzz`c^' +u, +@ O@ ?0 / ]1D7&v0& 6,+@O@]1D&m& 1,+@O@]1DH&u&,uFuD&LuPP& 2,D` "@LLK 1/20!!!5!!mnm`/B =@!   "!221/2<220%532765!5!#"'&!#3!53#=Ga'&HHAA@-]@QQJKDuuêK I@&  ! <<<<1/22<220!5!+53263#!!!5!#3#ZZi,+뗗Ӝ}/BCmm' -0u-Vf&f&j.'N` @9  B  DE 291/<290KSXY"]@R546Ffuv ('(;;797JIYYkiiiyxyy]]33 ##Gb`/ZFB?sl' *v/@ ]1 l' *vOKQX@8Y@O]0s&f/ &Os' (m/' (Os'y`/'yOs 7@   1 4<2.9991/903%!!'7;NwdPo;jnL >@!    <<2999991/9990;#"&5'!5!%[Y饵P{;Pu|~$o/nFk' *!u1m&vQF&*1{&0QFm&1 .*uf&Qg&Q{`IV=2@  1021/90+5327654&#"#3>32=YZͧZ-,t|6~ij>>WotV{ 2@  JF!21/90+5327654&#"#367632YZ͹Z-,jqFE1TTsTU6ij>>~ʗ[\``21qpu\0& 62+@ O@/ ]1H&R+1u\m& 12 +@/ ]1HH&R#+@]1u\k& 32Hf&RH;@     -299991/220%! )!!!";(RH=MKF{ 8i@92/ & 8   #5/)#92& MuMCM,s9299912<229999904654&#"265&#"!3267#"&'#"32>32PVWMfRPhgPPcpP/;}Jb04TY/%W & +#T53+)CBDA88>A>Ak' *u5jm'vU&r5 {&Ug&5 .ojf&UgZJk' *u6m&vVJm' -u6f&fVuJ&6u{&VJm&6 .uf&Vg/u&7u&yW/m&7 .u +1~&W (/-@  : : <<1/2<20!!!!#!5!!/s-  +ժA@B@!    <<<<2991/<2<2990!!3#;#"&=#535!5!f^^uϪ+|b>=^' +u8/ +@O@ ]17&vX'+@/ ]1=0& 68+@ O@/ ]1&X+1=m& 18+@/ ]1H&X+@]1=U&8t O&Xt=k& 38f&Xe=&8uu^&Xut' -|:+1m&fZ+1%t' -|< +1hVm&f \%N&< )u +1k' *u=m&vV]P& 22=&]m&= .uf&]g',@   <991/990#!5!546;#"+cM=яNQFX%>32#"&'##53533!!#&#"32y,fd.{{aRXWSzzc)?@$   2 !$'+9291/9032654&#32654&#%!2)"#546xxtB>d{f>p}qdƵϠ/Wp1FqGX>32#"&'#!!&#"32y,fd.RXWS0 327654'&#'3)'KKOO{e5Fq>=DEdgh=< !&#"32>32#"'&'#'҈FDDFl,fttttdLL.zYmnnnRRX랝+,S|1/@21 <1@   0>3 !"&'532#"M[?[MIXVJ))gj))=@0230@=<g"%# !2676;".#"3267M[?ZO*ZT3,JVXI5))p*32j>5F=@@=^s!%# !2676;".#"3267JR%FC=ZZ-,I]`A9++8(8rGj>>~A:;>N3 !#"#546 6&!#FVD6<0c//&r1FHKwN#";5!! $5476%3羽EF5e{ɉ{+ˡd4 32654&#"5!#5#"32_.df,T}SW;7XR=G{ 7%2654&#"#"/532376?654'&'&'&'&32h(>vwf2BFKI I<' )iy|{L (=\RR $+.! -&N +@ 1 3 21@  /0!5!!5!!5NwrT+u\=@ 26 25991@  9905!54#"5>32#"327uVJM[׌~ S@=))yz~#7(>@  22&0)1@ )) #)90.54$32.#";#"3267#"$546IyhYbfgg"{ (({smz45$&Ε?V!!!!+532765| YZZ-,ժH#ij>>~V'$+"!!+532765!5!547676;'a'&QRF1i&&(W%6MI!RI ! 5 3 3325D`H&0tt?uo+EA&#767653#"'&54&#"#367632"&76/JI_BG}MKUv14IBe``  1:!5!!;#"'&5=,-Z٪\Y+z=>jf!!3!!#!!5!!5!!= 99ժi@n67632'&#"##3i~\/j!-<BmV c3r%3sh5476;#"33 ##YZ͹Z-,Gbij>>~ZFB? ;#"&=#53!5!;+[Y饵 |~ĎɎ1m% # #''3C\P"Pn@Jo |mo"%#"&33265332653#5#"&8"iJo5FP;9JI9!c?LerHE! s{ p{+`=R{#4&#"#3>32jq1sd``cu\  32'&#"32767\:DC; 9CD8 z~{{vu'y2 {'R-%63273# &#"327-N5>o毴o8=yy=Y+͠v~^VR{ 763273##"'&7327&#"VPeo~簵noVAA( /s%+n+͝u"mmB8!+#"#54763! 27654&+Vn]6<0``~'R@]M//&r1FRQE8DVT*3265'"#"67232#"&'#76;#"w" . [f,?N͹ /2FIWS.O#.+#33232654&#N76SٲM{cݑ77hy]ҔYJ'>323267#"$546?>54&#"iV luhskijm^\''Ƞ/ vp{DI--յ1#hcq<;{'>323267#"&546?>54&#"PZڒEzTgjS썉J\{}SO9!!1Yc55##}#JKSQ..xmvV[!&'&#"3;#"'&5# 54!238!n|wx'%dQW/R5-0A3=g)(V\`@oV !!;+53276=#"&5!5!f^^uYZ͹Z-,(Ϫ+|bij>>~`>/&#5463!!#ŃF1-/7&r1F+!!;#"&5!5!5476;#"f^^uϪ*YZ͹Z-,`|b`ij>>~/V!!;#"'&5!/s-,-ZߥZY+ժ~>>ji? '8v'q'|XdJ##"47#5! 54'5{no{x4xn8!L IL*!"'&533254'3\Z,,Zxznjf?~>> ILɸ#367632'&#"kSHcm.-PKG "(3t*b7m*9 Vm+5326?3 67632'&#"nQGJ|lLT3!;^2Q+31705+h:=HTN~) )!3!#!!5#5!!uP" "v՚i@5b!!!#!!5!5!!-8YbҖg 7654'&+5!5!2! $5dc{dd\^rhbVPKKKKIJG8+lh3! $54767635!!#" 76RUci r\\dc{dd݊hl+8GJIKKKK}LT` 5!!#"3267# $547676peje]\dcmTjdc^QVbܨ JKKK21%݊hm*8V$` 2767# 4%$54#0!!5! TMOQWPVa ejo0, 5%b|8d1a# 6323#!!5!5!67654'&#"п -"BP8u~i9Dc``JU?T<>< % 7654'&#!!!!2! &53h<= \^G/"icUPRz,ʞ[+3IJ I8+le[tG)}LT` 7654'&#!!!76!"'57?\]ȨgcUQԪ-5IJ,39+lhJc 4'&+#5333#!"'53276MJY>lunc9rO_}nw~FVrA}Vg{#36763254'&#"64QҸMNr98xܭz BR1pqWBA3#+9'6-3!!!!#!5!5!5!^^``l%m&$ .u#f&Dgm&, .uDf&gu\m&2 .uHf&Rg=m&8 .u#+@ /##]1f&Xg=' )&8i2&&Xq<=' )&8 *&&X<=' )&8 .&&X<=' )&8 ,'<zW{%' )'i$#2'q<%'i& 2$#2'q<0' 6)&fPm' .u*{Hf&gJm' .u.m' .uNue\&u2eH{&uRue\0& 6eH&m' .uy}LTf&g7Va&g#fPk' *Zu*{Hf&vJ=%2763#"'&5!#3!3m6!h9Hն+ROf'MSb9du'Fk' ,u1f&CQk' *u)f&vk' *u/f&v%k& 4$#f&D%m& 0$#H&DNk& 4({Xf&HNm& 0({XH&Hk& 4,Df&m& 0,DH&u\k& 42Hf&Ru\m& 02HH&Rk& 45hf'Um& 05jH'U=k& 48f&X=m& 08H&XJ&6{&V/&7&YW}RT. 56$>54&#"57>54.#"5632 4o1\}p_s54&#"57>54.#"5$32Fp>!BlJc(v];?"AW?-1CA#E ptgDZX%KlaF='.`[b[3XpVU 32=t|6~kWotl(1%7276"'676#"'#7&/'&'&3232654&"m B{ rhG0wD &8Md\]P{#looԐ>>t#O9 Y%Z5H7WSCTV!!3+53276=!5!"YZ͹Z-,՚o*ij>>~Vb!!#+53276=!5!-}YZ͹Z-,|bܖij>>~%%P& 2$#&DuN&2({uX{&2Hu\' )'i2H2'q<u\&2' +iH2'q<u\P& 22H&Ru\'i& 22H2'q<%0& 6<hV&\l %7276"#7&'&5!5!676#" B{wD3' rhF>O9aJwt#jlg{.%7276"'676"'#7&'&=4&#"#3>324 B{zgG1wD3'5ZI9!c?98>>r.O9aJs{``>t#O9aJ;>V` ,@   991990!5!+5326XZZӜ}xY12654&"&#"32>32#"&'#5#"323QSSQPQRRQP]=zz<\o\32#"&'#QSSZQPPSSPP]=yz<\o\GJR\D%QM((_]q"! ^`A7S]++ L8 rMq;> 3!!!!!5! d iA!#!5!7##'-.d'Jug[|BJ8jF{5.#"3#"'&/&/5632654/&'&54632OS}{\Jvh*L'TrGY3e2{zD>zEGIZ9..QSKJ#}^R ~$=&[5#`cY1!GJ!b!;#"'&/&#=!-j1 *L4[TrGY=Zb ~$=&[?%7"#5463!2##326&#v6<0~'ʌ//&r1F )533!33##"&'.=)3267>5~9FBjiCE:  yVWx! A?;::;>`m];<<;\l9 #3#i++!!2#.+##5332654&#LN76SٲM{cmˉҔ77hy]w{.#"!!##533>32;zI[M ܹ.Dv6y.*l\<Ĥw"$8{ )32654&#"3>32+3267#"&'.>zl<^\fX<& +qpepӺ)Ld_y64''RR2{{0@G H221/99053#5#"3232654&#"Z.deэSW;7W Wy 0@  G F221/9904&#"326>32#"&'#3.de,-xSWWS^X $9@  !G! F%22991/04&#"326>32#"&'#46;#",fd.̸ZZ/xRXWS~Ӝ}}{0@ <1@   0>3 !"&'532654&#"JR&PNH\`@%++*,A:;>s:{!)47&'&!2.#"63 !"'3254#" 90%QNI]cU-RG+>jiáS,+;7W{7 &32654&#"%5476;#"#5#"32;mjkookjDGH8H$#${PϳQ{#T ij>>~SW;7WSzW{432!"&'5326=!7!.#"z [imX^^"++98ȷzW{?@ I H991@  9905!54&#"5>3 #"73267zXmi[&Z89++"Ƣ{ )%654'2273;#"&5 '&'&'& 56BL+>w9P!1jcGF:“֊>8E#ZuaQ`vg("chV({09@ 22&%!*2 &.F110&'&54632&'&#";#"32767#"'&546wA@Q[\ihWVLHHZ[c[[MaZ[V_A@^  VJ=+,nQb54"[\m({(<@! #)) )& )190#"&'532654&+532654&#"5>32U`LdKhiP_m"#ibQnW=JV^8{B#"&'5327654'&+5327654'&#"56763273;#"'&55c88hh@H9DDKCE@?qv|f6688h8AANODE<[KO 0A=1_JJm\["45bQ77,+=J++  OAf10`ZȢA"y( !27654'&+532764&%632#"'$Sv<;;!!;yUkbbkˠU^A;;LL67ss.gg=XVq^!+53265!5!!5!qZZh(Ӝ}}ؤg {H4&05476;#"#"&'5326=#"43#"326HH1:hh>CO6xn#{XЮmssngn^ ij>>~.,}^\:<bH4^ $!"326#"&'5326=#"4763!|LSbC,muq9b.,}^\:ᥟzX %#5!#"!2&'&#"32BHXN,>hhxpVHdbbd">:KMrqfqrfQk^".5 3 3265+]ܞ^+ r.$a32#4&#"#5476;#"}2rjrZZκZX `cJij|~V(>32+5327654&#"#5476;#"}1rX[̸[-,jrZZκZX `c6ij>>~ʗij|~23#!!!!!5!!5!! bnnl~ ˏ5i ^!#"'&5!5!; ̦ZXXZjf;8~|2^ !!!!5!!nnl^BXy&;#"&=&#"5>32!5!3267#"'.H>ꤶ./OINS)&r\FJKOUi(? ;?<65=>;7-4 ;#"&=# 5432!5!3#'&#"3[Y襵>5*GN\|~ܽ󠠄K9V  ;#"&5!5![Y饵|~(L-;#"&5#5!!2#"'&'532654'&+5!HHTgOE@KOPUCWJJX|~A$8+lh%12KJhj{!%#"&3326532653#5#"&2"hJn4FP<88 d>LfHE!s{p{`LfHE!s{p{)32+532654&#"#4&#"#3>32"iJoSN5FP;9JI9!c?S,5HEcԜ|~s{p{^^@;#+VM{+532765367632#4&#"{5D1DCWVV\^oA@01re22wx\__V{ 4'&#"#3>32;#"'&./]p@A2XVV?4<>OO__^edwxH10``A{ !3!##{yyH{  #"%"!&'&!3276ߌH?7?H4HH4{-m__mOmmOE`!!!!!"'&763#";d~~~~ KKKK`hghi&{.4'&#"3276=332#"'&'#"'&57!29PHJ|")u) }07_CC_vD8@jxZqosO++Oz2ee2z| VH&/#5!#3!535&'&76767654'&_|{_hd{{dE,HH,O1HH1ouu{{BnmBHImnI^732653#5#"&';zI.Dv6.*+w"$732653#5#"&';zI.Dv6.*w"$fV^;#"&=#"&'532653YZNb.Dv6;zI}}w"$.*+jV{.#"#3>32;zI.Dv6y.*)w"$jV{;#"&53>32.#"#,,[.Dv6;zI[[}?>rw"$.*ll2{476;#"!!5!RRҼj&$pnhb`022{!!5!4&+532jnnHlдRRV``bzW^!#&'&+#!2327654'&#7545â?;;alkpw?@@?w 66^q$%'^NMi++ST**zW^!#!3327673327654'&+jpkl|a;;?î545(w?@@?wSiQP^)%$q^667**TS++V{8.#"#"';#"&=327654'&/&'&54632N[DF20@RRz|hj&"nfdbbFF24@LLf?((**T@%$!*MLZ[705-,QK((%$JK}VT+5326546;#"ӳZZcMӜ}}¸Qg}VT!+53265!5!!5!546;#"4ZZ=cMh(Ӝ}}ؤiPQg}VT^;#"&54'&+532'&cݳ--Zg() |@>vV[!#"327673## 54!3476;#"8tn!ʷ5RWQîd%'3A0Ǜo@`\V()g+`!5!4&+532!!H^^uϪ+>`|bW!!;#"&5!5!f^^uϪ+|b >`!533!33##5#"&=)3276:CYYu>>|WMĤ45wEioabdo?ܤdqnܑkmhAw` !+"'&5#5!?27654'&'&UBr86FRQS&(g3XXBO\Ldqn``;612abdw7,H`!# #3T`` !# # #33”­jj`jH >;#"# #4N|lLT2"zHTlfk}3 3#f%.]}8 V`!!;#"&=!5!;4z `ۧ10%*`!#47!5!5!332!'3254#ejL<FX3<;4% 6[}LT` 2!"'&'5327654'&+5!5!ajbVQ^cdjTmcd\]ej8*mh%12KKKJiLh`$- 76654'&+5!5!2#4'07&#"327* \^ejeidTQ'd( }ŃcL;*1JJ$8+lgqUeR8y*K/K327654'&#"56763 #?W::fPONNLQQUmlprLbAr+#}swt#&'&5476!2&'&#"3ʪplnUQQLNONPc9:Vws}#+rAbLr3!"'&'5327674'&#˪plmUQQLNNOPc9:Vtws}#+rAbLrJ#476!2&'&#"32767# '&5nUQQLNONPc99cPNONLQQUn>}#+rAAr+#}_-sB (47632 7 654'&#"47632"'&_ԚO̵eddede"!/."!B^!"5Ԝ0ٍccƍffff.""""./B!!} -@   F!21/9032654&#32654&#%!2#!]_Z^UTTVeb`sci?\dU?.Vi}u"y+";#"3$''"'&5467.5476322s9@< <@7uTxVddjbbjdd6=76NJ@6sx>WVggWV6l0%#5!#"'&76325476;#"#&'&#"32t:{}|3H >ws}#+rAbLr #26&"3!!!+5#"32deen a^!;8NN8:j+^Lۓa31DD10ML C3276'&#"%#5#"'&76323!2#"'&'5327654'&+5!22XW3223WX2o#55JzLMMLzJ55#o ?M;319;<@3xAr;<78chqjtssttss_3d0110d^L$8*mh%12KKKJ6 ,326&"!5!332+#47'#5#"3233254#dees ai$\\#jKyyKj#n0 h*5j 3.#"#"'&'#"&5#5333#;532654'&/.54632/e6RR;Y%w026:8>qaQQo-Ek>v;NTj&g[{=l?((TT@I!,KL!&`>NM55YQK($)$V4%.!5476;#"+53276=#"'&5#53!3A &'p.6@3632&'&#"632#"'47&'#"&5#533254#"&57#3uZ310.///0l;;;2v1+\!raQQ$-WO=MT-E‚#+qrfr9DhT"2`>9KiNVH3+5327654&#"####53546;#";67632G11l?KJYhonjjhqij;/m(56Ft<;H``01/яNPhce22wx%7&'&#"#"'&'#367632327654'&/&'&;>ABgf$&n/<>][ALODKTLDCKEJIa54%"092?)TT?&$!,KL\[&:MV3-+RK(#*$JA 3!!!+ۊqvLۓB 333# #333# #ttttU=B!#!#!#!#kkUXrXJ 4&+53232653#9O%5zpcęaBþybV "32653;#"'&'5#"4'&+532Zgo0>*m/2O?*mbþyf\gę10A @ 32tNN^luu)qJy}wYYk\g88A3>32#4&#"#5476;#")qJy}tNN^lu43rB98wYXj\1Sw66WU 3+532653#wtgr,B0ttxlX6Vr8.#"#3>327.bjtt%uT  qksa97832653#5#"&'.bjtt%uT  qkJa97Q32653;#"'&=#5#"&'Q.biuB-r33$vS  qkJH VX66x a970!+33276?3327654'&+CGDDuj=%%(f{n!!!|K((((K|N;[--s?5/.B 333# #tt+5326?331]O\D05 {{bpEW(K/itf--452654DŽ@XX@rPPPP=>X@?X=>POPP"'&4763"3tPNNPt@XX@PPOP>=X?@X>^s327654'&#"567632#(y6$$>q31210336WEDGkM@*7K$@ ` XFh_@C^s#&'&547632&'&#"3kGDFV63301213q>$$6yMAmC@_hFX ` @$K7*@)f7@  91290K TKT[X@878Y3#'#f)f7@  91<90K TKT[X@878Y373x$@1@0#+=b$@1@0%#+=/#!!heJ'#!heJ#b#c>U 533##5#5uJu!5!J>ߖ/)HDV{ W @  P{P10K TK T[X@878YK TX@878Y#"&546324&#"326{tsst{X@@WW@@Xssss?XW@AWXu"  @  |1/90!33267#"&546w-+76 >&Dzs5=X.. W]0i7@!   PP99991<<99990K TK T[X@878YKTX@878Y@?       ]'.#"#>3232673#"&d9!&$|f['@%9! '$}f['@Z7JQ!7JQXfs%3;!"'&5k&&iWRd10`ZȢ% '&73733256/MMV| ;#"&5#5!88hr.EGWwl:Q[v/).#"#"&'532654'&/.54632P1j8WV>](}248{D@}=RX o)k`@q a//$)*+MWfk2-*SIXa #'#37 ͉H+^s#&'&547632&'&#"3kGDFV63301213q>$$6y[AmC@_hFX ` @$K7*@,X!!5!yЈ,X!!5!34,X3#!5hh,X3#!54,X%3!5 DfCfv)ff7v=b10!!=V /)H @ PP1<0332673#"&/w `WU`w HLJJLD@ a103#?Fj82#567654#"56J24C1xZ@Vƪ@$C!Xl05^ V{tXf%@991<203#3# fx)fg"#DVv'4f#!#͇fxx/)'sr/)H >32#.#"/ w`UW` )LJJL" #3ﻒY#55#53pp{53#3"pp{f3# qfyX0CbyX0v53#5#5L:#33T걈s^p!5!#.q532654&'3#"&=X.. W]0ihw-+76 >&Dzs5  "&463"]]3GGlbbG23GbL3!5353:^H#5!##걈c_I #53533##sc^5!tcV %+53276=YZ͹Z-,ij>>~V 73;#"'&5,-ZͥZY~>>jic/@a103#?d.@ aa1<203#%3#@ D  1  0#"&546324&#"326D]\\]bG33FF33G\\\\2GF34FG;@103#ﻒu)8  @ |1/90@ IYiy]!#"&'532654&'85xv-W,"K/:=,,>i0Y[ 0.W=u#u $s/#DU|/#5!#|J9X#"4533273273"h;tv gfv ifvtR)@  91<90373x)@  912903#'#m/8 @ PP1<0332673#"&/w `WU`w LJJL/: #.#"#>32w `WU`w LJJL5@!   PP99991<<99990@?       ]'.#"#>3232673#"&d9!&$|f['@%9! '$}f['@X7JQ!7JQ=/10!!>VєmB]Xy a&h!5&hhh5!Ĥ/'\ ]`LM'ogDdFFJ  26544#3GG3]]lG32GbU|3!53UJU |/!!|&b9X632#&#"#&'"#72h54'&' RJ 6"RH 0PQn +0PQn  &Ds7"#4%62#&%n~vv<<tf3AntVH%#AnHV #"=3;X3Vh'fv?F&jrf&>/`yNf&DHf&f&D\f&pf&f&6&%$q%sI%1/01/0@%%%%% !3yyN(=H+u\6@ 26 25IJ]1@ 0!5!#"32#"320qYǪIIz~,.%0/01/<0@%%%%3#3#+#Vy0F1H (1  <<1@   /0!5!5!!5HA)Aժ9u\2HUu3xm < 1 <21@ /220@% %%% !!5 5!!9" :A@/7%<uZ&/M@"2  &,2(0<<2<<21@&( '   /<<2<<203!535&'&547675#5!67654'&'Ͱa|{aa{{bL+CC+LL*DD*+v[ssZttZss[v*DD*7*DD*;uZ9@  <<1@   /22<<2067633!535&'&3I.K{bb{L-I["WDWx ֪ WW"J@@qqro prol 991@ /<20353&5323!5654#"J{n !o{1xx 7oȼ߅LI LN' )u%N' )uFf&(f&Vf&6f&3i& Fz *'&'&3273;#"'&''&'&767,-b=MJMUHi;c( #) Xn^T).\-rv~ oik*%1)0T*XmY*)Va#%#54'$QQ 0kEb6=q'0  Vm`&+532 3#-^1FAF[D~S]VH" 4"32654&%&'&54632&'&#"76hY(>f2BFUR I<' )iy|{L (=\ $+.! -&({R&%#457654'&# !5!OTJPE* :Lf.,KOxsPWKL,#%5,*3eaZiV{.@ J  F21@    /90#4&#"#3>32jq1s```cH9@ D >221@ @]0&'&#"!32762#"?HH?5@HH@<⇙8wyvs6`@ 1@ /0;#"'&'#5"$lYoRQ`+.0`b;`D # #'&#5~J/1Fe<2T`wtB`!367676'&'31!xdLjE.*{`T|p5dwY|rNįtR8&%#457654'&# 4%$47#5! OTJPE* 9MKOxsPWKL,#%5,*p$Rݿ &H{RP`!#3267#"&5!##P117,#J%q\T`PH? XVT|.@ G  F21@  906#"&'#764&#"326ttf,n䇅{WS<R%{$%#457654'&# !2.#"OTJPE* :%QNI]]^KOxsPWKL,#%5,*8(8*,A:nok` 2@  D>ij991@ 20"32654'&7'"763!aFH<Ηr{sPSے-- 2^!@  1@ /20%;#"'&5!5!!$lYoRR0`b3i`%27676'&'31%"'&5#5!tZ;jF-*RR"#vfwZ{s`b;+.0LVh )O@ '#* KQXYԴ0'']<<Դ0]1@' *2<220"27654'&'2##"'&7673A\VMMG*w|~~hA1LLNeˑRh]c[斘n,mKseg.YVx`#&+53 3;# t/1FC /1Fz ~,~VN`%67653#&'&533?>TyyT>?@WxؑoW@F`&#"&'#"'&37676376' 2KUXK2~)@V""V@)~`{gLHk{A>oRyRo>6&j3i& jHf&3if& Ff&C$ # 76'&%$'&763 '7676] NI5|utf M2C6R6WpA{z Ʋ irS$ $6'&'&'&7!2#"'&32765JgNn-R Nr^ydPpw{A K~}Sj~"#4''&5676'&qO**d\txLJso@z8 vVOv~*+40r51_Tpf&"N& )umVd'#&'&7673567654'&'ĸh]i^V5RR*aW4QQ(VyvaxxGnռFCImֹD9` !32376&7%&# 67#5!'TQ0'( 0A_+T3 3[g/&'&7'&7676'&#"56776327'5!`ȍ=`[+9[R~!*`ȍ=`[+9[&͘7 cl|YDT|˩hl="pl |YDT|˩hlfMZuV\ #"32&'&32_{|^"y|•"jVH{ "32654&#&'& h1`{{_ mw.vRL#"32#457654'&#"'&76)MbzYTJPE* 9{2e+wTOxsPWKL,#%5,*˞nͱR)`#!"#457654'&#"76))I]]_bNTJPE* 9ۓ eUlnoJOxsPWKL,#%5,*8(X)V#!47632.#"!!#"&'53276`1c3$R,x:KAb9f.1d0W@Rd>Qoɏ?s!K_`7"'&76'&52n 'BQ_'BQ_[~,`*l#FR`*l#FRM #!3M&pM]!V!#56#'0#0?&'&bTB9[@[`7"7>9[@[|"O z:6hl0%[Ml |"Oz:6hl0%?[MVT| 7636#"&')! $&#"32nttf,hՇ<WSs)%{FVMu\&'&#"!3276 32 5DC6 >BCD@qopުՈOz~ {!&'&#"!!32?# '&76!2 %%cjf_[_fMJOhk en(' c\\c(  {"056763 !"/532767!5!&'&#"'(ne khOJMf_[_fjc% ؜c\\c VT1&Vy ! !###V{+'`VO` !!###`{`UVT|##5#537636#"&'!&#"32wiinttf,;pp>WS17532#"5>3 !"&IXVJM[?[5=@0230@=))gj)1&/y1'yc3Nk' ,uKNN' )8uK*o/32654&#"##5!!676767632#"'4=N qjqjW.E~C%0"@X UkiS*\o-* % T pFwusk' *HuI1L@!21 0221IIPX@8Y0327# !2&#"!^?s}RooR}J6,N&< )u+1m-%326&+32+##526!Z}y^ϬvH+W"%32654&+!#3!332#!Z}x_uwg)9dqco"676767632#4&#"##5!%0"@X UjqjW.E~-* % T pΗ*\o-k' *uPFk' ,uNhm& 1YH 33!3!#)v++B%$q2@   21 0291/032654&#!3)!qﰖE{e5F{f>dghq%s141/03!!/ժ!0@  1@ /2220%3#!#32!!3!7yŪM0'TB /ѪBL ҪN(x@   <<91@ B /<<2290KSX@ % %Y@ I:I:I:I:I:I: <<<<33 ##'# 3 0YY0S0кv{Z7F <@  B 10 991/<2990KSXY"33!#3+3Fm& 1N.F$@  1@  /<0#526!#v.+W++Vy0H+u\2H@ 101/<0!#!#++u31&/7h?@ B120KSX@%%%%Y+532767673 3;E,LE\mQ.-"X74oJ+'/.M *5>Bg@2  2 <<<<1@ /<2<20 KTKT[KT[KT[X!  @868Y3#5&%>54&'II Ǹ ˥II<{z WS ;P $@   1/22033!33#P)ˆ+BD @   021/20332673##"&nmuz[v~PEx+:r` &@   1@ /2<<0)33333`++</@   1@   /22<<03333333#<ຆ++B u*@ 2 2/1@  /0%32654&+!5!32#ϊ)+An ,@  22/1@  /<20!3%327654&+332#[fN+1@"#( +0! 3 632#"6/&4767676%67䐌x$[3#F#3bJ/P{3-wRIUA   +t` -@   F!21/9032654&#32654&#%!2#!_eUkUTTVe_cmcpPO^UCCVpou`@ 1/0#!6`ih`0@  1@  /2220%3#!#325!!3y-C7 "Ld64d!{X{H;`x@  <<91@ B /<<2290KSX@  Y@ I:I:I:I:I:I: <<<<33##'#3hh`Pl4_P({` =@ F 221@B /<2990KSX@ Y #33#b縸)`)H&n``"@ 1@ /<0!#!+53265 _7#U^`v=` N@B      221/<290KSX@  Y3 3###=ww`M` $@   F 221/<20!#3!3#b縸`9H{R`@F1/<0!#!#bW6`VT{S%{F`@ 1/<0#!5!и&6ʖhV`\cVec@    <<<54&xjjx޸ܸxjj ++ gsL`[|^` $@   1/220%#!3!3^渖L`66b!@   F21  /20332673##"&øknXrE5od]'+}U` $@    1/2<0)33333U(`66P`-@   1@  /2<0)333333#".𨐖`666L`)@  2/1@  /0%32654&+#5!!2#|y֜XZZZʖ;hi` +@   2/1@  /<20!3%32654&+332#S|yS[`Y[[[`;8`*@    F291/0%32654&+3!2#{֙YZ^X`;%{K@  <21@   IIPX @8Y07532767!5!&'&#"5>3 !"&A`^S E^]INQ%R9>;qdRp:A,*윜+N{ ?@#    221/904&#"726332#"'##3pLLqjUUe9ҝ暙?``B@  291@ B  /<90KSX  Y;#".5463!##r78r5ܖaUmV9{Xm&kC{X&kj##VT533!!>32564&#"##@1|yj{яLs`c. m&ivQ%{L@    F 221IIPX@8Y0%# '&!2.#"%!3267%JR%QNI]]E S^`A9++8*,A:pSdq;>{VDLD&j +1VM `%2+##+53265!327 RC'#U^5iрiv;A`%254+32+!#3!3 褽l`9#9533!!>32#4&#"##@1sjqяLs`cBm&pv0m&nC@hVH&y` 33!3!#ø`6u\aH{s@141/03!3!/2$@ 1/0#!38X`:Us 3!!!!##U/#˂>` !#53!!!!`¸ fs#!!!2+5327654&#/7qohfL>87||9ժFwr|zKK"VR`#!!3 +5327654'&#HRRQn!&&1`GQ``07 )3 3333###'0:YY{ZSS/B0кv;`33333###';M4hhPPlL_u7&Mu({&m%3###33VrwBh`%3###33,bG*B?`/Z%3##!#3!3)˪B9db|`%3##!#3!3ø縸*`Cu1&dWu%{&hw/ %3##!5!!+s-B+` %3##!5!!ø&ɸ*%<\Vt`3 3#\IIT`lD%3 3!!#!5!5%lk! mP\P\Vt`3 33##5#535\IIT`l55%3## # 3 3XfuPHNAB}3BL`%3## # 3 3o)'o*?HkG#4&#"#36?6?2GjqjW.E#20@0X U ڗ*\cU'% T pK,m& 1L;H&lf32+5327654&+#33stohfL>87||wwqwr|zKK"hVm`3 +5327654'&+#33j:HRRQn!&&1'wGQ``07 )&?`/fH%+532765!#3!3HhgL>87)hzzKK_9dV`+532765!#3!3RQn!&``07 `CG %"'&'&5332767653##|#3/@0X TjqjW.Fժ$'% T pI*\z)b%6#"&53326=3##c<1Tsjqø~3$2 #%m& 1F+@ _PO@/ ]1#H&f%N&F )u +@p0? /]1#&jf){Nm& 1K{XH&ku\QzW{u\N' )uzW&jN' )uL;&jl7N' )uM(&jmy}LT`7F0& 6N&nFN' )uN&jnu\N&T )u +@p0? /]1H&tj+@ pO@]1u\*H{u\N' )uH&j1N' )uc%&jh0& 6YhV&yhN' )uYhV&jyhk& 3YhVf&yDN' )u]&j}s %3##!!/Bժ` %3##!!øȸ*`AnN' )uahi&j7R({u\4RwT:`Z1"$/#4'&'3767653653#"''##53 ra{ .r &q,bS !/#12j{@E#$]|0q!<"5DAb1)*5"32767#"'&54767&'&'&76'##53|LAV:218UG&/=8O6-N@?_F?D(7-"Gq/#)^ %$ \*$@.!* n F?\KH* #TH#5DAbZw %3#%3#3#%3#ô ^ %3#%3#%3#3#%3#̠&!#53ӤR@dm 327654'+53367657M593pQf$h?FA@6b !eI(R[2* #53 3#ӤR%@-$%#5754&'./.54632.#"'/XZH߸g^aOl39ZZ8{4<5/VVL89CFnY1^5YVeU-"%56767&'&54767632&767/SD435gcbnZdF31`9:H:ZU!LOTAKv?=0ps2# &#\&"r 3# E' <, K' =qE ' = KE' > KX g' < X g$$27'&5767&X$JԖ`e_'@5 ^vbĘe4)X ' <j%67654'&'3#"'532T! D' <U !?%#"'&7673327676'4/37653323#"'&' &!UNBAE3I0<^yM\dsቬ+;H2zm^\꜑#P}g£x&R" C~m8(!' >c  =%327654'&#"67632+"'&5#"'&5473767654'&'3HIj($@GgLK1ZX%5,0.3cM[|dh<2=B%A !  .DF-%!mNH7(M' <O  %327654'&#"!#53367632,Ij($?GhKL1[W~.DF-%!mNظ\wLR! Xn*' X &/.Q&+8O\79LK5:,]-#4BK%63#"'&&733276u2lecw@A(IiTcI9(jzG1H*V\ss~B")T.327654'&'&#"&#4763&547632#XzL,5;(.;Dn2KxAZM\MObxX'*9:X DD(NOf7*(?$S-8AP6`' < U"327654'&'2#"'&5476B!799[]KB{ƶ`Q%T*WE{R,,9.UMAx|KU#JNL 3 &"4'&!5 767&'&'&547632?,3/V%.-js1v-3t9>YH9!$7+(;ڮ.TVLh+bZ3[f5%#"'$47332767654'&'&767632#4'&y]H?BKSxlkA;"b^M`72'#}[7 0&huqc-##NG".*3:,=2IB="9), g:^M ' =r D5%5%DHHnnnnD&567&'&54763233"/#"'&5332767654&#"t$!lD?I'8 .4LT^s7Z $08 " ,d$* 9^W4'6O'&n=NV)qaK" %D5%%5%DHHnnnnnD5%DHnnD/&'&54763233"'&'#5276767654'&#" lD?I'8" +EɓV  , 8_W4'6O -n=*{nmp" %D5%DHnn0('&54737676537654'3'&x!9EO)"a 2=`KG g&ZGM'DA2omb}8T"RY$6s9It6X !Vz 4&"2>#"&4632XXXXztrrt?XW@AWX栠732767#"&'gC*6:*kWZZB6"D6{S )L}@"Fx3 3\v4373ŠF3#< !#'3<&1yI !n8#'337673#" &1CRz6 *boajr!nUPymL%#'37676537653#"' &1/(0H/<(F!34.5WY9!nr|> @2%,*;l>3  *"2767#"'&54767&'&'&76`yg\NNYp0' >p  S ]*%267654/&54767#"'$473&64_>.VhG hRcpl?AOXj<9U9iDGTOA7.?#ou\N/ b \^xH a8' GA&6F('&5473327&'&5476&'5#"'767654'&#"%327654'&Cv-(;G--0M,Q;(J$"':AGb 41~!$@K5:,+  iEN@TSZ 'C49g=ql@H=.%4-+#%v%'r.C!0B7,g`o6oU%m`m!3/AbM3))I~R,~R-0.>d{*>@ #% +BA?>pпQQ9l,"2"54767$32632&#"# '4%7654DҼJPi?3k]KM?oabu;\SfaƎ:F78UyP8327&'"'&#"%47&76$#&67 #"'632Y60I616*, !*:9u`0'"/6OAK %[9.ȵ!463 #"&'7325#'&&7'6et !yCBBquЍ h! ACBB|U )"32654&24''&5432#5476n$ % *|{e6Lj` %"%:yx~)RhKK>a 9"32654&&5456767$ 3276320! 54-6546$ % #vdz]#x.>r>>$o %"%}@~Y9peDQcFlWNn-7Vr\ ,2654&'&! 3%$4567&7'7$!% /@l~.ZA $! $?=Qm.uG4 {V|,@ //1@ <0%"32544$#"54$76f@@@)@@@@Pyxo *%"32654&"#" #"5323272#4#"$ % zw6^"$J7f %"%&PW0>|#$"2"22#"5#&567663 #4#"&퀀!7y{^܀ݹIedm%jh|'>@ $/<@ )1@")<@ & )<0&7'64323254#4%$7"6CBCbRBCACKؼJjeh0KT 24#"53265$54767653!"'#@>z]U]xTrs@0egu/ss|T}"247&76% 3%$Զ%< &Ѩ'LB|T"247&76! 3%$Զ%< &Ѩ'hBc J"32654&"32654&+&'#"'&5432'3253765&7465&'7$ % $ & cge~PLfrtгJwT\U &"$ %"%IJTObo;4ˋP6A^g[oPIcb$0+&'#"'&5432'32537653"32654&b\U{cge~PLfrt$ & ,PIIJTObo;4ˋ %"%el~ ,"32654&2537653%&'# 47&76b$ $ 0nuhigeޘ %"%4ˮ/IJ=%ۏel ,"32654&2537653%&'# 47&76b$ $ 0nuhigeޘ %"%4˯IJ=%ۏ``3323!"'#"543225cиx)3Ɯ)`,88{r\ ,2654&'&! 3%$4567&7'7$!% /@l~.ZA $! $&=Qm.uG4 dm +"32654&&! ! &%$&7676=$ $ W6tm2JX $ $8${{N&`p .%"32654&%&'&'&7!2765!"'676F% $ WD NbfP'G!$ $TrJco<ob{"326! %&$'423 54! P@@<)"FTY=Ia# 2:%"32654&%!$76! 6'&'7%&'&'&7!276 54! H& $ JF r cXD NbfPt!$ $=1TvTEmMd 6"327$"327$7&76365+&7632676#4#"A?A?`ƾ5@VC?/@@@L@@@E>/} cbWZwj %-"32654&7$%&'&763 54\$ %  ʪ=A8)SUB<S %"$36H9G)#$67T 6"32654&$'&%$76!232'&#"%$'&762$ % T\oEh@MqKUPn^ %"%+DYl0yP^d6Qqt^}]F0&$"2''&'$! '&5"32?6*ʁ+=x #esZh N)=mdZe'llu ! &7623$54'74"m#!AVB8?kP$U.F~>=!{ ##"2#"5324#j=;C>{jVnn!r&m|/S~i! ! !5 74! $&>%~?>~Si! ! 3!= 74! %&>%~?>wJ~S~i! ! !5 74! #5$&>%~?>~NSi! ! 3!= 74! #5%&>%~?>wJ~T3"36654'#"5432AA\(DeN[̼o[$N[ux"325"547&5423253,r>Jm,Ws> [yu?{EBXFD '656%"'&76! 4"3VA!. {x9f>.`h>4A?~= h\$kb8:;-F_Zkf2)I 53533##5J؎؎؎vP;r 432#"324ЄLT3z! 473! 4'$331=PU~iibcWOJf34! %56'&53!! 4<1~k  !TxY9vwsknWb!%! %674#"&5! % %a lշ._z-FH+,S.+RLo ۤTnB7W !,6752363 ! 54+"&$54+"32(TX[P<fI+yhZtԕGw, bb+]mLVW3! 473! 5 &5 3I%$&>8 wjs oeKrdW2'! 673! =4+5374# #&5! 24:ip II-<-]mxRbXxR~X{\+2 ! 75&7!  4'"6258TW\F ;IJ:QU-\p!7 %#65+"! 5!2363 6#&32Ԅ!S}GJtAFWHCBuccyi3#! #&! 2Ɣ#cZY4! 473! =+53254!5 4C(pbAAZZwfq211W2 #&$'6?! &65l_$^M>p v\Y1xOh_[ )euG4! !234!#5!  ! 4E%D˼  }>>&T3! )!"363! %22:M#@͹$[ڀ g7 ##654#"#4+"#&=!2363 K@BM{hIF`fhh&4!! 473! !#53274%$534D'e[?RܬW&Z~IJȕ!6G))=iY32! 3! 5 '%5%30>'Mhko 4>>HS2>+3|'! %!5!$! 3#3%! ! 5)54!  I8<rrr OfkQؔc7X!,!"'#!52'4#&3$5!23634+"32~ Mas1D>3LIvjt| ٔxukYf!! %$54#"'! ! 4'7fGD `U6I@bYsrg8A:ԃM){6\lY4(3! 4%7%#"'#% 3! #>U&;3̿0?7YpmWc$!6=3! 47$$5! nڞòd?;kHuLL8TWJ&)*y354&#"'675&%'% t_CCty`^q|ytJfI8=\ ۣb*#2 3#3#3##Ѻ/㰽<2"4;%"4#"32lѹF|pux$LRQ´){ B32654&#"26=%!>54&#"5>32>32+3267#"&'#"&1xYS\JMLepO27Gn 'aȿuc^8>M<[|%!YHZqYaq4+#"33)+RNPPXx+'#!?@=B2({0#"'&'532654'&+532654'&#"567632wA@Q[\ihWVLHHZ[c[[MaZ[VA@^  VJ=+,nQb54"[\mPDd %!!5!!!#53)Ḹя{ 6326="326&!54&#"5>32>32#"&'#"&PVWMZfRPhgPPTcpP/;}Jb04TY/%W & +ݮyT53+)CBDA>A>A2/H{  #4&#"Ð/.G/  33265G.Ð/.+[%!5!2654&#!5!#J^adlp2r?W75353!5!2654&#!5!#?idxEvDFzlp2r+")5!2654&#!5!2654&#!5!#HEws{p{``pU?32654&#%!2+#XcbYSJKR]#'.+#!232654&#1E4p1M>ze\Y_[' ?]Z4D|vShPIKHM!!#!ڀ_A33267>53#"&'.A L67K $,*kCBk*,$=4!!!!4<8l$! !#n 333# #|ZkmZ|xyY>E )#"326757#5#"&546;5.#"5>32&ffMD_ntt&pQlT];y:Av7X|&??8?vh+]85l[hmKDg..RE )32654&#"3>32+3267#"&'.&&ffMD_ntt&pQlT];y:Av7X|&??8?vh+f]85lZimKDg..RG53#5#"&546323264&#"tt`??aVTSXXSTNO/00z{{ B32654&#"26=%!4654&#"5>32>32+3267#"&'#"&5kK84:/0n06@F2Q #S-E^T=bg~yI>;#T'1S&9NT8m\)3?26JUJLXP ZQ`.+-,`\`d1CH^#$"%G4&"2>32#"&'#3VWWaA?`tt]z{{.10/OgG3#5#"&546323264&#"tt`??aVTSXXSTDO/00z{{1!3267#"&54632.#" xn7yEB{9t\UTm 2gp fnZ_cW15!54&#"5>32#"&732671xn7yEB{9t\UTm 2gp fnZ_cWO(.54632.#";#"3267#"&546KQ3sCBm0W][Uhd^jrm>s0=r6]H5KX ]0*"0Q>-7;af]=SO(#"&'532654&+532654&#"5>32KQ3sCBm0W][Uhd^jrm>s0=r6]H5KX ]0*"0Q>-7;af]=SG '4&#"32#"&'5326=#"&5463253UQUZZVPɖ0i4>d+]V_E||D^tgxxz)f[bF5302QI !#5!#3#53W?浵ss#PP-8 33##8x0BVxyDI%">32#4&#"#4&#"#3>32B/UFj",2%j$/.#jj?'0@)&ug@Eg?Es6"#'[v+5327654&#"#367632v89hu9CGR,+tt54Ilj!pm;32#"&'53264&#"X.c43a1.\;muvl=_)ʮl$!~~!#: 46 #4&#":&{[YX[ՠxzzx:  &533265ڜ{[YX[ՠxzzxG#3>32#"&$4&"2uu`?@`8UWWbP/00z{{M!!;#"&5#535};JkPF7R]rTP[v332653#5#"&[tCGRWttkGlj{TPg`b^68~}!5!2654&#!5!#K`Yslqj=?g32#"&'#3LSbC,ml/#.,}^\VZ:5`!!!5!!5!!5!!5nnlphˏ5iV ;+53276=#"&5!5![YYZ͹Z-,0|~ij>>~G#3>32#"&$4&#"32tt`??a9VTSXXSTNqO/00z{{Xy#"&632.#"3267y.c43a1.\;muvl=_)6l$!}ut~!#QI+325&#"47&'&547632.#"632#"d&/\R@5a$^`^63302b3q>>>5|4 * &:/ZXX `@?@bj:)#"&54632.''7'37.#"32654&|s .sPm4\a^UV^%wp237,pQ57vonwwn=rO(#"&'532654&+532654&#"5>32T]5sQ0"*0] XK5HWz#"3###535463z>1tkqU.98P#P,gabo53#5!3#+53276=Ι<98h9\P\ m;d+]V_E|~4wuxzzf[bF53[v332653##"&[tCGRWttkGlj{TPg`bO68~C3#!3#3!535#535#4tt)r\PP\ap #"&5#5!;phq)99uun@PpFFI !#3!53#I?P-PPG#3!535#535#5!#?\PP\PPdm3#"54;33#'0#"3276ttdytrx !3rJMB ,|ssW?#5$ U| ;#"&5#5!98dv.FFXtp(QU| ;+53276=#"&5#5!9889ht9hr.EGbm;32+53276=14&#"#4&#"#3>32B.V"#23_uj3",2%j$/.$ii>(0@)&:;Sm;32#4&#"MU!1$XT8[]V;;FQxlX6V~a88wYYk\U|$54'&#"#367632;#"'&5:G()WW*+7[//$1!U&'H/Y,-56\sa8BDH V6X66x? 33##?-{{~: #"'&547"!&'&!3276&NNNMMNNX-(e(-W !-XY-!TUTTTTU=5cc5=J,==,:&/#5!#3!535&'&5476767654'&3fwx=%; )=xw>)[v<.#"#"/;#"'&=32654'&/.547632P1j8W*,]({44MN8> 0Br34@>?=RX!k)k`FG@98b/$+*MW33 V6X66x"192-*TIX00xY46;#"+5326 j{mo>1gr,B0]MecU-:JxlX6M!!!;+53276=#"'&5#535}J88hu956PF]m;T_^s!!#;#"'&=!5!jG$2!V&'G^=R V6X66x ^M^#47#5!5!3632#'03254#a\'Ln& m,8!!^R^=jR332#"&'532654&+5!5!dCP>i;}C5~Dx~uhn\' xM|mTPJS]R^: .#"!326 #"&54UYXUcVXYV&l~~g~]% &$ #{&DqP& 2%X&2Ecq&%cX&Eq&%X&Eu1k' *Zu&d&u%f&vZ&hFRP& 2'{&GcR&'{c&GR&'{&G}uR''{u&GR&'{&GN&({X{&HN&({X{&HuNm& 1&(2{uXH&&H2XP& 26)'P& 2IfP0& 62*{H&JHP& 2+P& 2KcH&+c&KH5' )\+X'jHKuH'+7u'KH&+&K&,D&Lk' *u.k' *%uNc&.c&2N&.&2Ncs&2/c &Ocs0& 6=c 0& 6>s&2/ &Os&2/ &OVyk' *u0mof&vPVyP& 20mo&PVcy&0mco{&PFP& 21&QcF&1c{&QF&1{&QF&1{&Qur' *w|3VTf&SuP& 23VT&SP& 25j&Uc&5jc{&Uc0& 6Y=c&Z&5={&UJP& 26&VcJ&6c{&VcJP& 2&6c&&V/P& 27P& 2W/c&7c&W/&7&W/&7&Wd=&8d^&X=&8^&X=&8^&X9E' +\9dm&vY9c&9dcm`&Yr' ,|:m&CZr' *|:m&v@Z4'j$:&jZP& 2:&Zc&:c`&ZP& 2;L&[5' )\;L&j[%P& 2<hV&\t' -.|=m&f]c&2=cb&]&2=b&]&K'jW&tZhV&t \'P& 2AH"%c&$c#{&D%ct' -|c#m&f%& 1&$ ,#'<%cm& 1c#&rcN&({cX{&HN^' +*u({X7&vHcNt' -|{cXm&f"c&,cD&Luc\&2cH{&Ruc\t' -|cHm&fk' *ub f&vck' ,ub f&Cc^' +ub 7&vcc&b c{&cc=&8c^&X k' *vuq'f'vdr k' ,vuq'f'Cdr ^' +vuq'7'vdr c'vq'cq'dr%r' ,|<hVm&C\%c&<hV`'\%^' +u<hV7&v\Fr&oFr&Fr&|Fr&Fr&}Fr&F&~F&%r&o%r&pkr&|vkr&vr&}r&&~&p(r&o(r&(r&|(r&(r&}(r&~Nr&o~Nr&Nr&|Nr&Nr&}Nr&Vr&oVr&Vr&|Vr&Vr&}Vr&V&~V&LHr&o]LHr&]?Hr&|J?Hr&JHr&}|Hr&|cH&~DcH&D6r&o6r&r&|r&'r&}r&&~&~r&o~r&r&|r&r&}r&&~&Hr&oHr&Hr&|Hr&Hr&}Hr&\r&o~\r&\r&|\r&\r&}v\r&v3ir& o3ir& 3ir& |3ir& 3ir& }3ir& 3i& ~3i& r&?r&JDr&1&Fr&oFr&Fr&|Fr&Fr&}Fr&F&~F&r&oer&vr&|r&r&}r&&~&Ff&CFf(f&C(fVf&CVff&C6fHf&CHf3if& C3ifFf&CFfFVr&ʜFVr&ʜFVr&ʜFVr&ʜFVr&ʜFVr&ʜFV&ʜFV&ʜ%Vr&n%Vr&nkVr&nkVr&nVr&nVr&nV&nV&nVr&Vr&Vr&Vr&Vr'Vr'V&V&LVHr&nLVHr&n?VHr&n?VHr&nVHr&nVHr&ncVH&ncVH&nFVr&FVr&FVr&FVr&FVr&FVr&FV&FV&Vr&neVr&nVr&nVr&nVr&nVr&n V&!nV&"nFH&F&FVf&#ʜFVz&ʜFVf&ʜF7&pFV7&fʜ%m& 1%0& 6f&pf%V&nroVr#525#53d7vF&jpTVf&'V{&Vf&V7&pV7&uNf&vNf[Hf&DHfVH&nr'o'r'o8d&op/H&6&&67&p&qm& 10& 6f&fr'r'$d&p3iH& 3i& 3i& 3iVTr&oVTr&3i7& p3i& q%m& 1%0& 6[f&Dpf~ur&F&jr?FfCFVf&/FV`&FVf&F7&pFV7&\f&\ff&fJV&nfvr53#3"ïddm10!!d dmy/10!!/yy/10!!/yy/10!!/yy/10!!/y]&BB-@ 10#53Ěb~-@ 103#1řb/103#Śc/-#5b %@   1<20#53#53Ěb5Ǚb~~ '@   1<203#%3#řb5Ěb/ * @  1<203#%3#řb5Ěb/ #5!#5bb;/ '@  RQ R <<1<203!!#!5!nn\];/<@  R Q R <<2<<212<220%!#!5!!5!3!!!/nnnn\\?!   V 104632#"&?}|}||{|?q?P1 #@   1/<<2203#3#3#P3f111'3?Kt@%1= 1%+C@&7IF:4(:PFz4P@ PzP"P.zP@(/99991/<22299990'32654&#"4632#"&32654&#"4632#"&32654&#"4632#"&H%'H_EDbcCE_yxxwyLaEEacCEayyxxy aEF`bDEayyxxy7a`JGacECcaEyxyEaaECcaExxy"GaaGCcaExxy DP\h4632#"&62654&#"'4626763267632#"'&'#"'&'#"&732654&#"32654&#"32654&#"yxxyyaacCE%'E FedE  FeddeF  EdeF FceeO:8RR8:OxQ::PR8:QzQ::PR8:QyxxyaaECca`JyS  SS SxyT TT  T{GacECcaEGaaGCcaEGaaGCca`$3`u`'j`P','`$#3$V`u`'j`P&',Z/#@ v29190 5/-+#Ry#@ v<9190 5 +-#^R^  '4%#56763253767654'& Yb^`_hon"!^XE&->B% #D9``LAB\VBT=BR-;,,1Y7 Bw !#3#3!XEFZ !53#53#5Xޏ!' 5 5!' 54' 5= ,47632""327654'&'2#"'&5476"%F$W+,,+WX+,,+XLLLLJKKL @ !UUUUUUUUYnmnmmnmnH !3!53#3#z(洴ttPPD  5   @  W <291<29033##5!5 !wtt}oyc?}!!!>32#"&'532654&#"f6TTXYJz04?9= 25DIJLL...P\\PS****hQQ;JJKJ hh2112=!#!=HCD0;.="327654'&'&'&547632#"'&54767327654&#"hT-../RU-../P--KKKK--P]12PPPP210'(KL('NMK(')+*++*+NM*+/23Gc;::;cG3288Yq?@?@pZ88C#$$#CDH$$0.27654'&#"532765#"'&547632#"'&SP-..-PS+***(X/x==jDHIKLKKZ[-..44]\4421ab21hQP854&e_]]_eTSS}~A @ 32tNN^luu)qJy}wYYk\sa88=TdXC{dB}TtdFTud Cd?}CdITd=Cd;Rd0Td?d8d difdifdEd1d:ds|d1d ##"32.#"3267!!!!!!;JܾL:9II9^o78?*?77IG8GI`{c9'.473&'3267#"'#7&'#7&'&76?3&',;8+$"5:lUXn;4";τPqJ8=0;i<)^_HH?WgjιKp(_Y,%6767# !2.#"3>32.#"YQbUYoHqWUnrV,e#7!v'/_HGghGG_^ٜu]\YC!!!!3###5ZpP~WHE9El#!!53#535#535632.#"!!!?-쿿=OL=tyB_))HmBo)632#4&#"#5#4&#"#3>323 0?o5FP;]i9JI9!c?L3!Bjbws{Ep{``N#55YQKP%$((TT@I!*##` E326&##.+#!232654&/.54632.#"#"'&]``]z/YM$TP*N(:?>?>SZAm)naAt02k:WX?^)}j9>/b؍$~3YQKP%$((TT@I!* *-037#!3!73!733#3#####53'#53'33ٹpg1 2CYȿYD2FIn$uumuuwugu* %2#4&#!#)"33!3*ԕ|aԕ~V*$oN{&kz%%3p@< 1& (# #43('1)-&- 2'-4229999999999122<2032.#"!!!!3267#"#73&'&54767#70TJBN1Fi1OCHU,1u1!(*=Dl-.&nC>*( n -/ l*33!!###5<~rTws1s/!5!!77#'%5'+s-PPMMo؈onوn9-bw'67>32#"'&'"326767654'&'&67'>7632#"'.'&/#"'&54632326767654'&'&&#"32fbU!O3'A"+0.!. !  _ \5#?\k2,,#2!$(2( 4" )1>((E8&^ ,9Q F 9)ЗiRm:3Xwdg7? 2j7#=5(6$ 629T/ (2M !:5S}$@{mbq~Es/4 -& "TAB`]|@8nRkcd]aC".)5'632327&547632#527654'#"'&#"%654'&#"o|@X"07PYtaTk~j[IwmqJ2530D#24!`NkBX``S㫣†qJ`R{{{{{A667654&#"5>323#!!3267#"$547#536767!5? 7^\iV ^':,hski HE 4cq<;''K={[/ {9b{DI--N@{ O/{O!,&'&#2767#&'&576757O[TUeeUT[Y\Y[dsye]Y\[CvlCi----iH$"u9Bt"#BuflC1 %3267# !2."_|dT ȄE}=[~oi 7@,L]r4*:0N̾ (2.#"3267#"&54632%3#"326.2"&54:F#KVVK#F:-Q.~*Pʇ=II=323]W!{/tKb J G'QWab ^TH632#64&#"#'?3%Ǘŋ]W!{ 1$ÑEmJHWEbOYbcJ %# !3!# GHMZMd q+  #  "32!!3463"##526eb223b WU&WU&  1Q~>;\>}N*3>"32>54.'2#".5467>32654&#%!2+#hjMMKLijKLkZZ\[~}ڶ[\ZZ&RXXRuJjhKLLLijJgZZ[~}ڶ[[}~[ZZICBISqmopB 33!27&#%!2+!67654'&` `s1:+YX*q jdZ)VV) (%#'#  %27&"676'&\ӿ,F E]]]][{ab[ 2222jT%%5$c$% &.2&'&+3!.+!!2!27&#676'&%3A::f&AVy-`5?vfAd)7%LK$201/O~hbb)j)V>U)- fh@6    B     ` `_`_/91<<2<<90KSXY"###5!3###r}r7q^^-B0 %#!!!5!bJZCJ]d qddd J.%m -)7 7673 $54$32!"53!25&'&#"6Ky {U>ZLtࠢ""38M{{M7M3TT<`xGZAEIpP3RQ4Oe{'uV& /{e'uV& /tZ{'V& /{Z'V& /tZ'V& /u Z{'V& /j{'V& /{j{'V& /_{'V& /{_'V& /u_{'V& /_{'V& /Z{& /{B} 5!!B#ZpZR#ZZM '#'"ZZ$MZpZ#B} '7!5!'7ZpZ#ZZM !737@ZZ#ZpZB}!5!'7'mZ#ZZ#ZߠZ#R#ZZRZM%7#7'3'ZRZZ$R"ZݠZ#ZZ#Za 7!##:nt':tna #5'#5!tn'dtna )53753dtnntda 733!ntd:ntB}3!'7!5!7ѓc}Z#Z㔎RZ#R#ZRB}#5!7!'7'7!'/cZ#ZߤRZRZRYxa532767676767632&'&'&#"#"'&/#7!$f ! +!3-68+2",j!!!3 .6+85.0$m: w '07)(6;C+ : ,:'+:Yxa5!5!#5#"'&'&'.'&#"'6767632327676:m$0.58+6. 3!!!j,"2+86-3!+ ! f:d+':, : +C;6()70' wB}!!'#537i&ڠZZ#ZZZZ#R#ZZM'75'3''#ZZ$R"ZZ&ZZ#ZZB}'73'7'7#'7!5hZZ#ZZZZRZZM77#75'73ZZRZZ'ZZ#ZZ&B}'!5!7ZZ#ZZ1ZZ#R#ZZB}'7!'7'7!'4ZZ#ZZ1ZZRZZB} 53#5!5뤤4Z#ZhZ#R#ZM %'3'3!5Z$R"Zh̠Z#Z4B} !'7'7!#3̠Z#Z4ZRZM 7#7#5!ZRZ4Z#Z̤M%'7'3'73!5ZZ$R"ZZhZZ#ZZB#(276767654'&'&'4#!5d >b-*,%:0Z#Z  *+(54<852.&Z#R#ZB#)!'7'7!"'&'&'&547676763"mEZ#Z0:%,*-11> ZRZ&.258<45(+  B#$>2+#5!5!54767676"3276767654'&'&'&l>b-*,%:0ΠZ#Z2)-019 o #*+(54<852.&ՠZ#R#Z};47(+ }  A#%?!'7'7!#5#"'&'&'&54767676";54'&'&'&e910-)2Z#ZΤ0:%,*-11> o #+(74;}ZRZ&.258<45(+  } B}X3267676767632267676?'7'7#&"'&'&'&'&'&""'&'&'&#5! ! Z#Z  > >  Z#Z" *!#$' * ZRZ %  '%  %' " Z#R#ZB!'7#5!3'7'<2Z#Z<2Z#Z Z#R#Z ZRZq` %7'7]JQgz=Zӄh PJV}e 5!#Z"ZǠZ#R#Ze !#!'7'< Z$Z9kZRZe !3!5zZ"ZZ#R#Ze '7'7!354'&/#7!J%%%'HD_SlhX[HJ%%%%Jw422-A8;>112-!:zJZ[ghX\HC+%%'GKY[eg[WMs2=>FD{2,/2{DF>H':Xy6#5!#52767>54'&'7#"'&'&'&54767<:!-211>;8A-224wJ%%%%JH[XhlS_DH'&&&Iz:d'H>FD{2/,2{DF>=2sMW[ge[YKG'%%+CH\Xhg[[IB}5!B#Zp{#ZB!!BMZZ#M3'#|"ZMZM#'Z$MpZ#B}!5!'7pZ#ߤZB'7!5Z{ZM!37ZMZGM!#73{Z#ZpB|  '7!5!'7 5!!ZpZ##ZpZZZR#ZZ*M !737 3'#'2ZZR"ZZ#ZpZMZpZB| '7!5!'7%!!ZpZ#ZpZuRZZ#ZZ#B|'5!!!!5 #ZppZ>R#ZZ#R*M73'#'#'3hR"ZZ$RZppZ#B|'7!5!'7!5!'7ZppZ#>RZZR*M%#73737#hRZZR#ZppZBA! '7!=!Z#Zp{Z{#ZBA! !! !5!'7BMZMpZ#ߤZ#ZB}!73!!!'7#5!!qVa6ZEV`6NZ#Z">RRjը;mRR:lNZ#R#ZRRB!!373'7'7#'7#537!7'!RRȚNZ#ZN|NZ#ZN.9#!RRRRNZRZN ~NZ#R#ZN RRB}!'7#5!7!5!73'7'%!7'!`]Va6.ZxV`6NZ#ZRR;mRR:lNZRZRRB}!!5!RRpNZ#ZNRRRNZ#R#ZNRM#'3'#'RNZ$R"ZNRSpNZ#ZNpRB}!5!'7'7!5!7NZ#ZNpRRNZRZNRRM%37#73RNZRZNRRpNZ#ZNRB}!!7/7'7!5mRRRNZ#ZNNZ#ZNRRRNZRZNNZ#R#ZNM'77#7'3SRRSQNZRZNNZ$R"ZpRRmRRANZ#ZNNZ#Z6a##7!#tn::n3:t:5p::6a '#5!#5'5C:3n::n:4:dp:nt6%753!5373:4:dp:ntn:nd:4:6%3!'3n:nd:4:n::p5:tB}5!!!!!Z#Zwgw"?Z#R#ZRwRwRB}!5!7!5!'!5!70"wgwZ#?RwRwRZRB}37773'''#5:;!\[`Z#ZCCjjZ#R#ZB}'7'7#'''53777Z#Z`[\!;:ZRZjjCCM%#5#535#535'3'3#3Z$R"ZtZ#ZtM533#3#7#75#535#5ZRZtZ#ZtB} !553353!Z#Z{Z#R#ZM '3'#7#7Z$R"ZnZ#Z}ʻB} !'7'7!+53#53Z#Z}ʻZRZM 7#77'3'3ZRZZ#Z}6B} !!#3#Z4ZݤZ#ZZ#B} 3#'7!5!'7뤤Z4̠ZZ#h#ZZ 5!5! !!? Ou]%uuv 333'#!#\^vtP uB !!75!!5 t]]Xv ###3!3,^\X& v 3'335%!!# #^\XtvpFguv %3'3#!5%# #3!^\^$tv~Fuv #3#!5#3/# #3!J\^^|HGetvJ~{GGMuv 3#!!5#3# #3!F\ F ^tvW~uv 3'333'37# ##!#^\fd^tv ^u9v #!5#3'%3'37#7# ##3!3^^ fd^tvJ^uB '#35!7'!!!5 5~t]]EF 7!##!#*:ntaI':tnIFEF %!53753!5!ldtn~ntd&Iv #7#3'# #3 3\^^tvP*OutuB}'0#"'&'#53676323'7'7%&'&#"!32764RvxN1kk2Ow9g' Z#Z 0GD2 & +JD5@3PO2BB4R,( : ZRZ11/0*M !#737'#'RZZ"ZZ$#ZpZ*ZpZ#Ba7!5!'7!5!'7'7!5!ppZ#Zp?ZRRRZB}#5!5!53!Z#Z[qZ#R#ZB}!5!53!'7'7!#p\Z#ZߤZRZB}#53533'7'7##Z#ZZ#ZߠZ#R#ZZRZB}#5##5#53533533ҤtZ#ZtZ#R#ZB}#53533533'7'7##5##tZ#ZtߤZRZB}53533533'7'7##5##5Z#Z8Z#Z8ߠZ#R#ZZRZ !! ?OuuuB 7% !5uzR##7 ! ?S:uuzRuu##% %!3!3hV[7l n7R{+u\ #&'&#"327673 u B!OO!B ocI7͙7IcL 0"'&547632654'&#"563 3276767&#" \m`cu\6% GGnth r5?,/H@3H5,Y:$UeI+HQ\N,tqzSd69->eSY׮l 7!!5!!5!!LLk+5!#7#53!5!!5!733!ZD2/+^^``kIb!0?"'&''7&'&54767>2"&'2767>54'&&cv-'''OO_@8vcu-'''OO_A:GE:;9($(#&GFF:;9cv8@_pm__ONP(-vcu:A_mp__OOP(-9;SPF($(9;PSF'O@*iiiiB91/90KSXY"#3 !q!#7!hqqP3!!"&63!!"!0",Z(膆(\JN*"f_QQĪKM_fOPi%+%3!!"''7&'&6;73#!!#"!#L(0,:CyEB航6'|>v\JK-".4"$: 1cQı2#KK_ff_lFO]B/ 3 3ް2ް2201!3!!".>3!!"N=c(憆(c=֪I9[[9IP&'.#!5!2#!5!276767!5 ,Z(؈膆(\JL, 1f_rĪKM_fOPi%+&#!5!27+'7#53!5!3276767!73&'&'(/-9CyDD舫6'{rx\JJ. 4 %:  1crı2ݪyKK_ff_lFO]5!&'&#!5!2#!5!2767>b(؈憆(؆b>,I9[[9IL9@ 120!#!L^L= 7   @  <91990!!5 5!!LR%# Չ\P_X-y10!!X!תXy!5!!5!3!!y!DCmILfB7+U e+Gr?; /@     99190'%3##d)#Ӕ/}b%9;v'ue;e'e %.#"326"&'#"&54632>3"38\32#"&'#"&546329[=G[TFBi8\=G[SDCj~/[w~SNAU}^sdlkutcjmvu۠d|k֥s}T!3!T*,}T!3!T*p,33# NM^T,3 3#T^,$476767632#4'&'&'&#"#;9_UijB9 KGLV32326yKOZq Mg3OINS5dK t]F;73 ";@<7  6<Xy32767>32.#"#"&'XJF]t Kd5SNIO3gM qZOK?<6  7<@;" 37;XyG&'&#"5>323267#"''43OINS61-NSXIFJKOQdSP  ;@<7 W"323326X!!KOZq!Sc1NJOR`!t]D;83$777=X`y!!#"'&'.#"5>32326X!!KOZq Mg3OINS5dK t]F c;73 ";@<7  6<Xbz'767#"'!!'7#5!7&'&567676ǧfYUE5kIQ%\n*xrYQMoIF\<[ETFR q$"B2(d%(9L5XXy$!!!!#"'&'.#"5>32326X!!!KOZq Mg3OINS52'V t]Fجϯ;73 ";@<7 " 6<X1y0%#5!7!5!73!!!'#"'&'.#"5>32326Qu{hq,gqTKOZq Mg3OINS52'V t]FR=R ;73 ";@<7 " 6<Xy.1%!5!7!5!7&'.#"5>3273267#"'!!!!'hMEnK Mg3OINS523J:VQ FJKO!8!E$F";@<7 832326#"'&'.#"5>323326yKOZq Mg3OINS5dK t]FJKOZq Sc1NJOR` t]Dï;73 ";@<7  6<а;83 $77 7=X0y8&#"5>327&'&#"5>323267#"'3267#"/'00NJOR:G67'43OINS520N]a91FJKO?J4r[DKKOdgb 7 ;@<7 !7)32326#"'&'.#"5>323326!!yKOZq Mg3OINS5dK t]FJKOZq Sc1NJOR` t]D*!;73 ";@<7  6<а;83 $77 7=Xy7S#"'&'.#"5>323326#"'&'.#"5>32326#"'&'.#"5>323326yKOZq Sc1NJOR` t]DKKOZq Mg3OINS5dK t]FJKOZq Sc1NJOR` t]D;83 $77 7=;73 ";@<7  6<а;83 $77 7=Xy$!5!53276767632.#"#"&'y!JF]t V'25SNIO3gM qZOKج#?<6 " 7<@;" 37;Wy' %52% $'"51pZV(IٜXDz;%76767!##"'&'&'#5!!5367676323!&'&'&i1*+V WJRNMR  W,!::!,\HSLPM% +*%'H:^2:A<336G84^:H'@'H?Y L=@33/N0<^:H'%X`z!!5367676323!&'&'&!!i:!,\HSLPM% +*!#'H?Y L=@33/N0<^:H'%X`y& $Xy& '$'$$Xy'$& $oWz'$& $nJ. 3#3#!5!5J=>𹬬J. ##!!!!>7BX`y 365&'!!5!&547!5!!%43448>!0IG00GG2?8>;_8X`y !!!!"264&'2#"&546X!!IdddeH:l'**z{ BbFE``bq+((d:svvX`yK!!!!2&'56X!! BS X`yD!!!!73# X!!鏫 BZVX`yD!!!!33#X!!֕ BLVX`y!!!!!!'X!!߰ TU UT BX`y !!!!!3!X!!-e Bz(iE`07GO!!!!#"3###535463!3267#"&54632.#"'53#5#"&4632264&"X!!4@#mmC???DJB&G$$K&aqk[Q_B;18BCC?-I\\I-?p`ctiF6A?9i=$#tu#gSSSX`y*!!!!>32#4&#"#4&#"#3>32X!!."]?T\Y88EQY7:DQYYU;;R B=:xoHOM]QHPL^P%U20=X`y ,!!!!3#7#546?>54&#"5>32X!!ffc`--A6(Y1/a3\p$-, BiN2A+,/-7#!^aO&E++ X%y<@     <291<2<<990!3!!!'7#5!7!X}y}J;fժhӬXyB !!!!!!X!!!جX y%#5!7!5!7!5!73!!!!!'G=XkXU7Y Z:wSAw@Xy 7!!!!!!!!X!!!!߬Xy? (@( ' <2291/905!5y!!LK Xy? (@ (' <<291/90-5!!X#!!VVTw 3!!5!5V!!!!߬¶LK VTw 3!!-5!5V!!!!߬VVw#5!7!5!73!!!'5 p[5m{*[y~!߬`u,`vLKVw#5!7!5!73!!!'-5 p[5m{*[y!!߬`u,`vWy&%5767$'5674[šzآb|۠M)Ig#M(Jh#Xy %5%%%'w2rK/dtm0x0oVXy '75%%5%'rKnd.t'o0xEu0#oVX y!5!%5%%%!!'XC_^?sMN#N+PJ>`5Yd|5X y!!'7#5375%7%57'NEO>:fLNtt5\h}a5H<Vw?#%#"'&'.#"5>323265wKOZq Mg3OINS52'V t]FJ!;73 ";@<7 " 6<LKVw?!(%#"'&'&'&#"5676323276-5wKHGOZq M343OFGINIIS52'V t]FDE)!!;3 " @< " 6V w+.%"5>327%5%%%3267#"'&'&''}QINSE^AsMP#Bt]FJKOZq _4O;@<7փ_5Xc|6V w27'732767#"'&'&''5676?5%7%53;L t]FDEJGLGOZq P32326&%&%5$7$7wKOZq Mg3OINS5dK t]FJl#a;73 ";@<7  6<RO]ɗ9=}Vw*%#"'&'.#"5>3232655%$wKOZq Mg3OINS5dK t]F)a#l;73 ";@<7  6<R˖}=9"]OV[w67&%'&'5$774hmU֠Gc _eT2wnw2"O0Bj%V[w'567&'567&hmU*c _eT2Vwnw 2O0BDj%Xy_%!"'&54763!!"3!yɊD_`Dƍ^`Xy_75!27654&#!5!2#XD`_DȊɣ`^ȋXy> #"&'&5476;7!!!!"#'J_+30TD~K9# K^+#Eƍ5p5Xy> 32654'&'7+'7!5!!5!237RJ_+30TD~K9FC K9^+#Eƍ55Xy%!5%!"'&54763!!"3!y!ɊD_`Dƍ^`Xy%!=!27654&#!5!2#yD`_DȊɪ`^ȋX,y&%!!'7#5!7&'&5476;73!!!#"$UrG6:qYȲG5^_=R5 Yƍ5p&`=X,y!++!!'7#5!7!5!&#!5!27327654'&'92D4VqF53 D&#I`__ 2ȋ559`^`X0y!%!'7!5!7#"'&54763!!"3!!yR|ɊD_aDAQjfƍ^`5eX0y"%!'7!5!7#!5!27654&#!5!2yR|Da_DȊ]zTQjf`^nj^DeXwy1/3ް2/301!!!!X!w@Xwy1/3ް2/301!5!!5ywXy H/3 ް 2 ް2/33 3017!!!!!!X!!w߸Xy J/3ް2 ް 2 / 301%!5!5!!5y!w54&'&'3!!#!5!ݾOO''''OOݾOO''''OO~GE:;99;:EGFF:;99;:FތPOO__pm__ONPPNO__mp__OO=9;SP;99;PS;9Oi372"&'&'&547676"2767>54&'&'!5ݾOO''''OOݾOO''''OO~GE:;99;:EGFF:;99;:FMPOO__pm__ONPPNO__mp__OO=9;SP;99;PS;9ՌOi3?2"&'&'&547676"2767>54&'&'77''7ݾOO''''OOݾOO''''OO~GE:;99;:EGFF:;99;:FBccccPOO__pm__ONPPNO__mp__OO=9;SP;99;PS;9ccccOi372"&'&'&547676"2767>54&'&''ݾOO''''OOݾOO''''OO~GE:;99;:EGFF:;99;:F,ccPOO__pm__ONPPNO__mp__OO=9;SP;99;PS;9KccOi73#2"&'&'&547676"2767>54&'&'ݾOO''''OOݾOO''''OO~GE:;99;:EGFF:;99;:FPOO__pm__ONPPNO__mp__OO=9;SP;99;PS;9Oi2L2#"&546"326542"&'&'&547676"2767>54&'&'h7b%&'qqnNL88OݾOO''''OOݾOO''''OO~GE:;99;:EGFF:;99;:F)'%`8nqqMpLM77POO__pm__ONPPNO__mp__OO=9;SP;99;PS;9Oi!'/7=E2"&'&'&547676%&'&'& 654'67676-ݾOO''''OOݾOO''''OOf:F-T1-F::E.S1.E:POO__pm__ONPPNO__mp__OOAϚ9FPQ9.9떖EQPD19Oi!;!!!!2"&'&'&547676"2767>54&'&'+{{ݾOO''''OOݾOO''''OO~GE:;99;:EGFF:;99;:F;gZfPOO__pm__ONPPNO__mp__OO=9;SP;99;PS;9Oi372"&'&'&547676"2767>54&'&'!5!ݾOO''''OOݾOO''''OO~GE:;99;:EGFF:;99;:F2mPOO__pm__ONPPNO__mp__OO=9;SP;99;PS;9IPi%!!!3!!#!5!3Ҍ8Ȍ7nj6Pi %!!!!53rM_Pi%!!!7   '3ͬc  ccc #c ccc Pi 3#!!! 3/`103#`7 !!'  TS TS8X`y!532767>32.#"#"&'yJF]t Kd5SNIO3gM qZOK ?<6  7<@;" 37;XAy 755%5!5X!#!!ʶLK XAy % 5 -5!!y#!!!!KL VVw?  55!5!w!!!KLVXy? 55%5!X!#!Vw $75$&%&%5$7$7"nWlܜ86s˖}=9]OVw $'$'5%$5)n˱#lݷW680O]"ɗ9=}Vw)%*67&'&%&''&'57&%5$?7dMjTVʥ3˱!3a4m"cjX)3S][e﹏3N@%HZ-=}k$Vw)$(6%'56?56%7$'57&%D>WwZN(۷+/m")33 +Si063hiyje˖X[y3!!!'7#! !PYBzrYh?ݪ@?@X[y3!'7#5!!5!!PYzrY(s??ݪ@X>y!!!!!!'7!5!7!X!w R`RgfjfX>y%!'7!5!7!5!!5!!yRgw! RjfhDfVw?%%&'&#"5>327%5 %3267#"''43OINS:Z0!!x2XIFJKOQd>3  ;@<7 ҧK{"327V!!?E>XIFJKOQd>C43OINS:Z0"323267#"''&%&%5$7$743OINS61-NSXIFJKOQdSl#a  ;@<7 W"323267#"''55%$43OINS61-NSXIFJKOQdSa#l  ;@<7 W" # #h֣ͣG9> 3 3h*338> !!# #g֣ͣrcG9> !!!!# #gg֣ͣrrcG9w!##mZ##5w33ϸ"mZ!533X%C!!3#CrrCr[  C!!3# CrrCr[ %~!!3#CrrCr  ~!!3# CrrCr Xsy^!#y^ap$%%$~  %6 %!&'&"112*zz`XXroGGnY  67" ,J5PP5JX*77*#L8P"2642#"''7&546Ċnji56؝]QBɉLJo3NEQ\|G+-7AJT35#"&546;5#"&46235462+32"&=54&#"3#"2653264&"2654&#ςYxxYςZxxZE1/EE0uu0EE`Ev/EDaEEaDE/wZ\ZЂZwwZЂZ\Zwu0EE`E`E/1EE0E`EE0u0EE1/EXsy^!3!yߨys+~!#!r ~5!#r rS;+;!!3vrr;)3!rv;SL4732#"'&'.#"0 Pd@7* h$TA6?&H|( #"&546323250 Pd@7* h(V$DTA6?&Hk- hi !!!#%!!h\roa`޾"(I  !! #37!#3'Q''Ho99Ƀo!p=⻻}(TI #!!7!#3'l)okkɃ=r!r⻻+2" #/;GS_kw+7CO[gs!2#!"543!254#!"+"=4;2+"=4;27+"=4;2+"=4;2+"=4;2+"=4;2+"=4;2+"=4;2+"=4;2+"=4;2+"=4;2%+"=4;2+"=4;2+"=4;2+"=4;2+"=4;2+"=4;2+"=4;2+"=4;2'+"=4;2+"=4;2+"=4;2+"=4;2+"=4;2+"=4;2+"=4;2+"=4;2'+"=4;2+"=4;2+"=4;2+"=4;2+"=4;2+"=4;2+"=4;2+"=4;2+"=4;2+"=4;22+"=4"=43+"=4;2+"=4;2"=43!2#\\K\=Kl]\\\\]ii\][]\\\\\\\\]\\]\\\\\\x::f>]Y"\I\\\\I`LLMK\y>>(I !! 3#)%3!'-''96=ûHJ7 hHH--JJ4!!!rrZm4 !%!!5!5!5!!mr4 3#3#!%!!5!!omrX4  ! !! !ZS;Z$m4& :4 '3276'&#"!#"'&'!!67632!zzzzzzzzIwwHS;HwwI΍GG2GG$_EXXE^m_DXXE_3#%673#&'%676'&1rsrs2srsrTTTT@TTT|B B@B)B)΍11@4121tD& : y4 !%!!Ybm5A4 !!!!;rbmY5A4 !%!5!mrXae4 !%! !2mrx1x4 !%!!5!!ZZmrϠZnZ4 !!'7!!!'7;rZZmZ1X1Z'327#"'$%'3632# 6'&#"zz=>lWWsPYX2灾vzz>GjX`OXG%3 33!#!!3/ .^+^kk4 7!!#xr;rc/Krmu4 % !%!50!53!ymrZ[z4 !!%!!''!;GZZHrZEZw%3 !#!3!###.77/^+kk4! !! 3!xS;ZK{mu4 ! !%!#5!5!"~~+mrz[Z4 !7!)!7ZbZmEZwZ4& 9 4& 84?& 8 u4& 9 :4D& 8 y4& Z^' %4&' ;%?H' ;8 :t' ; yXyH' ;8aXyH' ;8!% *!%#567!676&'&'&'&|z*(2J E<iKH#&GIJE E|-2 M3 +O  ! r; ?9y?p  ! Xy*= 67 '&5677%"632327'&32767#"'&'.#"y{h p{"zi piE5 5dJ t]BFZCEF4 Zq Mg3ħĨ1P$s 7%ےs3 !Xy3#"'#&'&#"5>323326yKOGU43OINS52^NFz;7 (  ;?<6 '='3#3!!#7#537 `?+0'7ϺrSSr4!!!!3!!!'7#5!7!xrddeZmn;fժhӬ4&"6`VT|F`4z& 845{& 7+4`& 94`& 8Fz. ! //1 /<20#5!!!#oo.ڭ%ۭ&H:07 %#"326=7#5#"&546;54&#"5>32hHH#[]E>Vcii!fHatLT5m4:j2O85%--JJ??8?vh+]85l[hmKDg/R: 935#35#35#!5!#"326=7#5#"&546;54&#"5>32assssss;#[]E>Vcii!fHatLT5m4:j2Op"r??8?vh+]85l[hmKDg..R:1=[%5!!5!#"326=7#5#"&546;54&#"5>32#"326757#5#"&546;5.#"5>320;0#[]E>Vcii!fHatLT5m4:j2Op""[]E>Vbii"eIasLS5m4:j2Op"rrrr??8?vh+]85l[hmKDg..R}??8?vh+]85l[hmKDg..Rk*.26:>B#5#"&546;54&#"5>32#"326=%=!!%-!!%qi!fHatLT5m4:j2O85%#[]E>Vc,,,,,]85l[hmKDg/R}??8?vhyyrDyyyYrJyR8~4<DLTZ`%#5&'&'&''7&5475'7676767537'5676767'7&'&'&'5'7'%654d3/D9229D/3d4.E9229E.42*  *22*  *2*88fWfEOPDfWg88fWeDPOEeWf8g * apa *  * aa * ba.43ab-45F^'04>2".33&'."#67>76#FVʓVVʓ"vv"Z83Pv"Υ"vP3ʓVVʓVVnh<8PvDDvP8"vP9~?9Pv"F^&0!4>2".7!&'."67>4'&hVʓVVʓDvP479;;74PvD"MʓVVʓVVv"i c;DD;g"vP F^ %7'32>4.#52".VFoDvvDDvYoʓVVʓVSwVF_YvDDvvDoVʓVV4!!!xrZm #53àZ03#s #5ˠАWeE%3 53Zz i#0s  3#àР˓[m#!!# q3#s!!`N um!5!#z3#zz3!5!`z m #4763!!"ƺoyeD9uߑfW#'&%'53 763:*enK==Mne( =C _AEc H<  3!!"'&59De{oVfd #3ƺ m 4'&#!5!2 9Deyo}Wf &'&3!3#76<(enM==Kne*!<McEA_I= 3#!5!2765 o{eD9ᏞfV(3(v% !!!!55!#uX ̼uu]]e! !!;bc;$<.: 1/<03!3T.%y5!!X3 2!@ 2 5!!5!!5!4)4𬬬 !!!!!4)4XXX 333 Nf  !!!@@@ Nf  53353353353𬬬 3333333XXXX 333322s's' !!!!@@@@22s's'!!!!\!!#!!#\!5!Z!!X!5!$Z!!$X3!-Ԭ3!-.*!!@Ԭ!!@.*5!3,,(!3,X5!!@,(!!@X3!!- 2Ԭ3!!- 2* #!!!P@ZԬ 33!!P-#,Ԭ!!!@# 2Ԭ #!!!P@.* 33!!P-#\*!!!@# 2*!5!3,Z,!!3,X !5!!#@PZ,( !5!33$,PZ,!5!!$@Z, !!!#@PX !!33$,PX*!!!$@X!5!!Z !!!!-XV !5!5!!,ZV!!!X!5!!$#Z !!!!$#XV !5!5!!$#ZV!!!$#X5!3!,-,Ԭ !3!!,-XԬV 5!3!!5,-3,*V!3!,-X*5!!!@,Ԭ !!!!@#XԬV 5!!!!5@,*V!!!@X* #!5!3!,-Z,Ԭ !!3!!,-XԬ !5!3!!,-Z,* !!3!!,-X* !5!!!!@Z,Ԭ !5!3!!$,-#Z,Ԭ !5!!!!$@#Z,Ԭ !!!!!#@#PXԬV #5!5!!!!P$@V,* !!33!!$,P#X*V !5!533!!$P-#ZV* !!!!!@X* !!3!!$,-#X* !!!!!$@#XԬ !5!!!!$@#Z,* !!!!!$@#X*5!35!,-𬬬!!!-,XX33*!!@@*DH5!5!xX333x 2 2H !!!!-Rx !!##xmsZxH !!3!!xm3-sZRH !5!5!5!,NX 5!###lZZXH !5!!!5!4l t,ND 3!!!--Dx 333!x,ԬxD 3!3!,(D 5!5!5!3,,D|X 5!333,,(DX 5!35!3̠| 3!!!!-- 2Rx 333!!xs 2 2Ԭx 3!33!!-s, 2ZR !5!5!5!3,,X !5!333xtZ, 2X 5!3!5!33t, 2H !5!!5!4R 5!!###sZZH 5!!5!3!!t,-sZRD 5!5!3!,-DX 5!333!,,ԬD 5!5!333!DX,!5!5!5!3!!!!,,--R5!333!!###s,,ԬZZ !!!!5!5!333!-s t,ZR, 4763!!"Q[yY[`~| 4'&#!5!2.-Yx[Q`~=?x 5!2653#xY[Q[~|2Ψx !"'&533![Q[Yyx2|~>3m 2>#3> 2> # # 3 3>ݲ}#$cc|5!F3F~|5!|iF3P|!XF!@F~|!|iXF!@P5!5!!5iVV333PP~P!!!iXVV#!#P@P~P( 2! ! !!!!#!#(!(F(!Z((!((!(h(!|((!(*(!>((3(i( :} F( #'+/3!33!33!33!33!33!3䟟䟟䟟mnmnm( '/7?GOW_gow3##3#3#3#;##;##;##;##;##;##;##;##;##;##;##;##>>>(!%)-13#3#3!3!##!#3#3#3#3#3#3#ޟޟ#|ŸŸ|Ÿm#( /#E( =Zh!|i D}h( D(& D& E F(& E F(& D& F K(& E& F Ki( D}(& D K(& D& E Kw!N<w7!!!xr$<w 3!254#!") ) xrVVVw& X Ow !%!5!5!5!5!5!5!5!5!5!N?:IILII޸[["[[w !!!!!!IIN< w !%!!5!!!I) N"-?33 #&'&+"'&#"/573;2?"#'57#&'#"#567635a)8)kOkaKA-'= //G),Y=  !H$ /+HDH)+) $., fYYx !=Z Lx73&'37&'67&'67&'67'32654'&'7654&#"3672 $54767&'&47'&27632#"/#"?#"54?'&5432'&327632#"/#"?#"54?'&5432'&327632#"/#"?#"54?'&5430'&327632#"/#"?#"54?'&5432&5432&56327&5432'&327632#"/#"?#"54?'&5432'&327632#"/#"?#"54?'&5432PO~ )*+')+(@&'$||e?/A}]\B-71SLoWj\vLLr%%,* #$ )*n$ % +) $ #*+    ? '+&()&(+&p   % % +) $ $*+*EC*Z*,)-)-*,%&%&fБfU 5HhfeefhH2pu^QFs棥sKQG4 4  22044 22 9       L%('*%)(*%)(*t     144 22 0r!2CTev+&'&54?6?6/&2#"/547672#"/547672#"'=47672#"/54762#"/54762#"'=4762#"/547672#"'=47672#"/54762#"/547672#"/547672#"/547672#"/5476l=.%G\&#- Lj.N 0d&K4i    }    g    s            &                            H5-V"*2-.T<:U'EGE'DN-ֈU]\`CDcbF]WWZA@ZZ@AZZA@[[@AZKPrqqrPGeޝdMP䠠P }2ٛk A4&#"26%4&#"326#"547&'&4632 $54'&'&4632XP79NNqO.N97OO79N']EacDC_\n\U>DbcEXFDbbDEaaEEaaEDaa+G詄UUSj멏i LVV6 "32654&7#"32?ɏǾ/`TcȐɎ;P12Y.1"264&"3264#"54327&5432#"'&'@KjjjiOiiLKirqrtPssrqQܩZTdIU )5AMYdp{3/&76'!'47653!476=332654&#"#"&54632'#"&54632#"&54632&'&676&'&676'.7>'.76$6&'&%6&'&6>'.>'.f<;.=+,>/;Kyz~LZ|WX{{XX{IE11EE11ET    m       ;   R       s@dd@s}>}=/NnN/=}>@MllMNkk& % I% % "!$# "! "!! & % % & %-5AMYdp|5#!4'&'5#2#"&546"264&"264"2647>'.7>'.%676&'&>&'&7>'.%7>'.676&'&676&'&753!476=3''676%27/&76'77&'&/#?6'&7liilYz{XW|{bEEbEd      8    @     .HxttxH%?%5E$6  6$D5%?%-5!!1(~(1 5,4t4(4N4(4t4;hhh%%#%% $ %_ $ $!"!$!/!!!" $ $ $ %:-,GtG,-: XLRqqRLX ![$n[ii[n$[!ob !!'!tKZGkcn "!!'!##&+572367676hNn_5, S Grj3#-EmDJ~o.(*!4\tR~UL !!'!  ##' CI3Z  > << 5DCX << ; YD36273 ##'5&< +Z@\\DC ZY\5#,5>~3+&=4%3+&=4%3+&=43+&=4%3+&=43+&=43+&=4%33 #&'&+"'&#"/573;2?"#'57#&'#"#567635@)A({@(@){A)A(@A(^)4 'iOj_J@,&< //F(0'&&'ܐ'J&(lN5  >! )&V?<?$&$ '& ZN N />Eqw!674#!!6?676'4#'323276767654#3#&'&'&6%67!672!&=75$/563&43!32+'!67#>54&53 *,  3)="(&)09$) L&TE` MPA[MH Y $ ;&&e=O%/ N ,8(.7L1Rf~H8SQ,zH%9D6 )jGP@4Rjd_*KfsDIR 9! O  -]&C+/3#"'43727&'#"$472776725676&5&U8)$ tJ .; d3f,"3' VD ( GL/7;;,g t^F$< LD&?>X4R !/# I ? P?D!)Mv>/z2!"&54676737#&'&54>;7#"&546767!7!"&54>3!6763!2h!.)g$'30!/&j ! /:(/  )/ 9)/  9)0:*/z2463!2!2#!!+32#3#i9/ ! j&/!03'$g).!*:0)9  /)9 /)  /(:!!C4&#!"!&3!!"3!#";#"3&'6737#&'6737!"'67!7!"'63!67!2e;'+pCCo CCCC2CCKK<LLKK%JJ60"2=2).=<==<@=:>=;TT USUT UT83$QE!D72654'6#"'4#"'54#"'54#"'675674767#%$4:JILLHOKHLKIhghgighgD>-sJ1 b6'SS cRR SS?SS\\K\\;\\]]!A*>K!!D!254+'3254+'!254#!'!254!&#!"0!463!!2#!!#!3#3lCC2CCCC oCCp+'q=2"06JJ%KKLL:=@<==<=.)g$38TU TUSU TTE!C32=732=7325732'654&#'%2&'&5&'5&'IKLHKOHLLIJ:4$N->DghgighghSS=SS SSb SS'6a!0J)K>*B \\]]:]]J]] O!%)-1523656;2#'7+"/#"'+"5&54775'"'5476;25'7&567635&56;374765'75'76=4'&+ '"'4!#"'&36365&5&#%#754'&5&&547'5367&7+&'&'735&2?"5775537'7'3533553535'32767&5%2?&#%55'575775775uo,Mz"060D/5I:2'5:6&" *:D:S46$.e QN5  u4MDa 6bUP+ ,H;`I23N5( (#I0M '^5%#!:X+ "*  6W}W:uW4 5vT & /H3V XD9\SL+&31.d+%X!Q $2``KPPPG[6%# Qy- 6[[3GK[O`_A[-)$t7 L-$ L6=" (CJ#R"0 :~GB{~Eoj<4S[Za LC5 ) .U%+Z&)͢ 7e<ILAaMoK33K@G6 $$(& (''&1/----2)( (-((d.'-T?OK8T$ !T3(-<((')))())( &2%2#"'&=477654'#"'5473t\*e O@UCXq P S. P ӍMOb>YaYƮ58l7P P@ $0<FX + &=%6&#"3 6=%&#"';27!54767%!&'&'2+"'&=476^7\Pg㑵Hr'.)%sM M#fC-7!%A.; ӎw:kKqz +H*G;M tu/&((AA&:+C;."/ 8Pi>'67&&&'6.7#"'&'#"'676'773.#'6'5676&&5476'&'67&&07 ^< 1x,B5@2 JVMv!#uA+UBDX[f*;-10)..C,sB#HKU P]12<0VQ }%'H6-T}^$k7 R2'7f!A\;y?1!50BEt"!zkQ;0qu0\oi:5oPZjsXFaPJGl;4ejN^1F[q7&&'7'6&'$#&7'&#"'5&767#&''5$'67'6'6'5$'67'656&'67&'6'&'''5$7676'&&'6'63&7"7&'7&'7&'7&'6'6%676767&77&77&''5&"'6%35&'.54>23#67#&8 p +WDTc'H @XO`= ;*)8 kDv/Pk-J KDhGa D`gBD6DDD =3dTDW, :g j)Yi#'WtI-9w18$^8;./7-I)jS)'#i\-IM91D;8%a7/.D=uRNBR&'%QBNRq d2 D s98C ["|44&3, '2^3R T(B?#'9C- !y ~#Z10>N?$%Y4 )%FN? ({ usis< 3(&^T05<>7;,#4[:O(vAfGEtYB z^~4j #,;b:['~Av@~EQ Bak4~_H#T2 $$$$ 2T"`q$&'6&'67327&#!65#&3jjdnh wWVݱqZre[c7 7 cyX ,35'533#3!'#'5!5!5#53!5!5#!!ʶ~~ blvF F A<<3ffX苜qXGccGap 3264&#!2+73 #'#5# 3m`hh`2`Ĉѳh|;vvʷ}f33#!!#'!'57!5#'5735 64pzp7d+!#!573#'5!3!'573!#'73!#'5IxOOTxSVVdY\yvVPPvIyY',32#' 37+ &5%6323'#57'53mJl{~m@+ݼh4144'0>,_ vNknmmnObs32732753"'#"'432364'5;+"'#"'53275'&'&54?5572'#&'&547634%476='4&#68$$B )Z>&A_;i88u-o1bFGfQ_M5mwLbkjI,K=''8 0##Rm4 ڹ+ܴ5!PP"4\=ѻ"8Qý32#"&546324&"26%#"5432itvxsq1"00" 0/B//B/#a`ir|H!//!"00""00"!/0 _b 9>DJPV\bhn27654'&#"&7367'67675673#''5&'&'7&'%67'7&'67'%7&'&'%6767%&'&$h%$%%34$&1++XSA N@`==k>P CRX++XYC P>k==l?L ?Q oL+ Nn;P?;@  nMNn3%%%%34%&&%s==`?J >PW,,WW? K?_==f?H?PW,,WU?H?^<=Ke+cL mCP`k<<!4(0847632#"'&7327654#"&#%#&7&'67&'67!󫪪vӤ=6 5N'V[S.U[R󫬬񫪪񿉊 ʯX[V[X[V[!4(0847632#"'&7327654#"73$3&'67&'67!󫪪vѦ=63QNV[S.U[R󫬬񫪪񿉊w  'X[V[X[V[!4!)47632#"'&%#$''&'6%&'6!󫪪4>;D@KDzcngk?dnhk󫬬񫪪I kpinipi !4 "*2:AIX3#''%#&'52#"'&5476!!'5%!!'53'5%3'5%3#'32765'&#"M==,/0#H 8&O6 |7iY06./==e6a&i1r4z012+KN2HQ>>>>f^2"/1]8`1"Y 4f2y5+ +"'5$76%&'547327676=&#; hz0/O{[(*TQ~`NO =tR[\ 8d<+% &56;2'5$%75#"3vh0.P~N^(8P,VRZycOpO >S\^ f`1B7#5#53'&'&54767&'&=33676=3#327654'&O&"}|fzg}}"&&"}UQn$mQU}"$nQUVV{xVVUQ<"{u^^\ _u{"#| zUOOUz |#YOT{zQPPQz{TO@>)4'&#"3276&5476327#'#53'&`____`oŠqk]^^]YYňÁhgf@> '"3276'&'7#5373'#"'&5476j___``_ߓqŊqYX]]XYfhhĈÁj0 '&'&376&+"'&5'476%7!Z{z[ZZ[~\YWmpN#ZX[[YZ[PQmp#TG*52764'&#"#463233#!5sPQPPtrQPyzg֏LQQQPPQr{{t|g*#"#53533#632#47654&#"#ddiqqCBigIIugzyUr}ppDtPQs_CS 7"27654'&7#"&54767##53#533333#3##h. @\ ! 2(>>?ZW~>'3|}}! -/@ /- !^'?XY??~YX?(F}R}hh}}hLS<#5#535&'&'5'73'3#'73'676=35'73'13|e{vw}wwUATwx|xxS@Wwx}vv|d|re{Eus~~suE|VAKtrrt@X{Ius~~suI{dr|*! #!!!'!27674'&#_82V)3{D#MHZW{s{?zK8! %#"#&5463 67!2#6#";z\)MaBuh __ itBaM(]y tt[+##+tt\5."264&'67>3"#"&54767&'&#52hq៝rd:BJ|^d#!p⡠q $c]7A;{26XY "zz" YX62 &'5 %$ 56?6'.j拈|*xIIz'&|JJx, F42$8"3264,'5'&54632264&" &$#"&547>ȜmmNMm} lyzU<Mnnnm+}7 lyzU<|||,&(uO#eaHG||||Q'(sO#e‹`IH=! <>'.463227#"&5454&#"&'&5476766&D9BB8Ğv?W:pbW~tp) "-ff)-gtpQ@3AA:ACj›GmN?ijbvr56WGe((Wi0154d)-?/6?2>32>32#&'567'6'#4&&#4'3>64&"-S5,9"\0+Fgv!4u|W")^,k ikdS!eb[_[H|NYC:RHB=G`SnU|#!!!53&54632!!5#67654&"U't00Z =yy= :]ZssZ JjkkjJ 2f4%353'5#"'&''#&&#4'3>32>32YE;<<-!&Y*dx cf_Oz.*O2)7Ze``b<`WuALh`8!5!1##'!5!_drrPk^K{U_W{'/27632#"'#576&#"4'5267>327&'"2XCZd}uud$gq~dV)40tlx!&%"dLk}:Uwma4 sOHK{wY@x A63276327632&"'&#"'&#'6327627632&#"'&#"'&#'YR #{=('%{XNCEz>O&z>'(#&R #{=O&{YNCEz>'(%{=('#&ee22ee$l66kd23dEPdd33dd$l76kd34eE^s#!5!37!!'  L34((C $Td67&'&"!3!67>54.#"!5&'.54>325467675#53533#63232>54.#"3'8xpA?9l9>@q<;9'D} 5RTP=: SSPSS ;r>>p  p>>r> !A% )RSQ1 )6BB6) 1QSR) p  ""V{zHNRh|&'4>32"'4>32&'4>32&54>32&54>32#!5!'!567>54.#"32367>4.#"323732>4.#"327>54.#"732>54.#"I )),(?)(#!3()3$))BG!(( K{mg,;h IXI L$  P   H''1|G''#s%'')7$ ''A  ''HTݬ9.%~~ rF)~ wpa!'-23353#3!53573#'5#5335!75!!5'57!ePPeeQQeDpH>H@A~}}~00mrTTreppe-!7CQ^&54767&'&'5676767&'&54>32!535#5##3654."!2>4.#" 1""#@%@#!@% ?$##0 ܍a1%?E?%4,/--+D,/1+ 4;AB<>"  "#>"">#"  ">#10$ITNnVB, n ?%#Naji-/4^t&AYcgb3%' + ((NV8OQĿ>:<uyg**5 k5h P[32>4.#"732>54.#"!5&546767&'&546767&'&4>32'&'.#"+L)+L*+M)(LH     > |n @: !:;! 8An} E04`a30TL**LTM((     ++x: 8>>q ?9 9? q>>8 :x++c^UZbbZU^jg% $Tdhy47&'&";67>54.#"!5&'.54>325467675#53533#63232>54.#"!57#&'.54>3234'67632#7$5oh<:5d4:;i865%1MNJ96 MMJMM 68JNM0v    +0 +/0U-,,+,.T1/, 9j9:h  h:9j9a &LMK- '2==2' -KML& 1  V//X//X//V6HLP&'4>32"'4>32&'4>32&54>32&54>32#!5!5!M ,,.*C,+%#7+,7%,+ FK#++ PDNAM**4d;K))$'**,dY&"**E #**L:ƥ??@@=%)5!5!3353#3!53573#'5#5335!mD^JJ^W^KK^׋LLZZ,}}uz%yuu{{u}--4@4767&'&'5676767&'&54>32!&7535#5##3 1!!#?%?#!?% >$""/ _1+ 4:AA<="  !#=""=#!  "=![1=%T e >6.HC'L"'G 12h[FH`[$%ok+*8d .Ncv[.7&546767&'&546767&'&4>32 w "E> #@!!?% =E!w ./@ =CDz" E>"">E "zDC= @/.QO##"'##565'##"/547?kM ,4N"DF &Fi?JO/FB!O {|Im<&=M2227632#&547636=4'&#"#4'&#"=` ]d2 cBU;/G;SXMB:@B ս;7hf% #>|\@9@O &&5 iC n:^O G  %2O7236;2"'##'65##"'&5476;235&'&=476jS c1=EO ;SCFRʝT6*F@E1;O+.`162V Yi8/D ;8[B VRP"<B+"'##565#+"'&575477;2732;276=4'&3&'"ih;F(wQ"DG".FWCNfBy" bODUq5u4  Pro@ |S`64 '4'&'~ v '  w (  w ' $k=F F>jG3~Pjb^*IerN{̑?qJAe}Ωv6\~x(ONPPNO(!8?|EE|?8!r!_3#"/4?23D-!]UF+}{<!/3#'654'&'#"547326Rs9W5[S%3;B[/OBC'*|<j_g#"=4?2%#"=4?23ɧ%QM?ˠ)TK7(w7џ5s ?|O"'4723!#"5472!5YA>RHIOq 1 ӫg 4D% 3363'$6'"I+4 puoS^*  3%#'#3%#';&2 IʗHj7*(,377#'#'547#5773%%,ppsr,'zzxz'984?/99e5>:_`qE#&#"'5654'5673;54'56732733273+&##&"#&'565*G1 VV2Is3'{'3sI1VV 0Gs3'{'3sP3+1='3sH1WW1Hs3'=1+3PH2WW2H. ;G7567&'&'3#6737'#&'7#&'6735'67#3335#5*)SR))&*&';((:'&)'ȶkkn\\[[nȶ kk n[[\\n`ff/ee.((&(;((:(&((@))SS**n\][[o jj |o[\\\n jj e(P( /N#.6CMhw!2732!'5675&'&=32#&'567637&/7&+"+&'532?4/%32#'#&'&=4?#'57335'3!273+#&='#"/547354;2?!&=35-,;K> #WU* y "њHV ηz/;@"q=o )we)$IY'L ALaXwH >X%CII$PC/DN6g+  b% #  jnN :3 O+5{bQ< ,d-  X] f '^ JJA!< 8 2E35733!&54?'7'7!!"'&%#'73676'77'7'&'676}]} =--HW(7*! >y*1c{F=.,H-.'d(#Y+GC8957jN})%%tGl5nm3(,H:0/(_kiN}!N920K 1DW3!5>7>54&#"5>32&54?'7'7!!"'&%#'73676/77'7'&'676@.#5*"I?6O"[m" c<,+GU 5) <|w)/ayD<,+G,,&a(!>B<#q'%NG91 M7835hL{'$$qEh3kj2'+G8/.&HghL{ L8*/D *(=Pc#"&'532654&+532654&#"5>32&54?'7'7!!"'&%#'73676/77'7'&'676D|q%N24H'CB=9PS3464E>6O#]o 32EX!#2632#"&'532654&#"&54?'7'7!!"'&%#'73676'77'7'&'676Hevyn$L27C'y*1c{F=.,H-.&c)"ERUHHS S /(*. 8956jN~(%%tGk5nm3(,H:00'\jiN}!N91/K "7J]"3264&7.#"632#"&54632&54?'7'7!!"'&%#'73676/77'7'&'676]'00'*//l+2>AB(S`dT^dyg7<,+GU 5) <|w)/ayD<,+G,,&a(!.T--T.H D&RECSukf{7835hL{(#$qEi4lj2'+G8/.&HhgLz L8*.- X.A!#!&54?'7'7!!"'&%#'73676'77'7'&'676n!?/.JY08+"(@},2fH?/-J.0'f*#&K:;68nP*%'wIn7rp5).J<21(vmmQ"P;:1-K':7&54?'7'7!!"'&%#'73676'77'7'&'676N!?/.JY08+"(@},2fH?/-J.0'f*#:<68mP*&&wHn7qp5 ).J;11)wnlP!P;;199'9HR!273!567&#2&'676+&'67'#'6765'533!273+#/#"/47$,7Jv I MO $p%|I ^ [T<K"(~GW$?8?])( EAs#L, T 0 ` +WVۄ`$$a.|%2<J\e3 + &=762367#&'&#367&#&#"3274/"34?3'35732?5#+'535^-J|@h'\-e@<r2&H); uZJM =9jl:jgb.Qi2Q|酝:*}( dpR!h j `]_i$x:-(^%,3"ؿEa HMP E7g /:BR`j # &5%6; 65%&# 327#57&/#2#&'676+'%3#'#&/47'3327##'%3#"/6j1M{ǮG&z v$ExݨE(+=R:n:D!s Y!gQKum;} uA;>e=g¯Cy??ԢB|*>w4I ' 5@` bC$ j$H?iM!%.|7H27&' # &5%6367&#'.7&67263'#%; 65%&# mJB|e6O}°I+o|BJn^jaygwaaygxaj^w$FyتFG퇢D{C?` B]ww]B JХC}.?yP%.232#!7&!"4#".54767267p   {u*_ Jcllm8*#I%<($|ʀX#{Nwt7mnld4)5:IIIB,<_4767632#"'&'&!%!!  >W$`4 Z|b<_/374767632#"'&'&4767632#"'&'&!%!!    UW$`H    Z|b<_/GKO4767632#"'&'&4767632#"'&'&4767632#"'&'&!%!!      UW$`H      Z|b<[/G_cg4767632#"'&'&4767632#"'&'&%4767632#"'&'&4767632#"'&'&!%!!    /    UW$`  K     Z|b<_/G_w{4767632#"'&'&4767632#"'&'&%4767632#"'&'&4767632#"'&'&4767632#"'&'&!%!!    /      >W$`  L      Z|b<V/G_w4767632#"'&'&%4767632#"'&'&4767632#"'&'&4767632#"'&'&%4767632#"'&'&4767632#"'&'&!%!!  0      /    UW$`+    .      @  Z|b.t )2 $$ >54.#"4>32#"&h..--t*Ƅ2..2/.y )62 $$ >54.#"4>32#"&$2#".46h..--1.-.y*Ƅ2..2//2..2/.t 2 $$2>4.#"h-..-t*f/2..2/.j '2 $$2>4.#"$32>4."h-..-q.-.1-j*f/2..2/y2..2/R7!!R-ӖR7!!%!!RMzM; 67'&/#'3#67$#%ׯP==Ͱ̼bN+#!f"K++!|o554.#"##"'5##"&'&'0!5!5&'.4>32!!676767'7' :!9!"9 :! F GF;kY_1278e56d:81)RLk<GG E~^ : : ; ;NG 5 e4G( Li) enf77fne )i (G4e5 G( Pm 9Y%&'%67&673&/'67'&'"&'4?&'37' '7 &/7&'#>7$%88EFu/- 6uNDL22LENu/80uFD8jU45B%y\A@Yy$F 0=/0 ,-X70 ;~*2 %% 2*~697X-,oo  +F9d1 ) ( 1d9C1*CT'&#"'5&767#&$'&%'6'&'''$'676'&5$'6%'.54>32D$ "@F,NNNvF8p^Lb2  N**+ B@0"AR/0?wA%od/D&3.YaQ/5#3$"uI' @3/u= =#n- .... w3% % 32+#".7!"&'&'#&=4;73737D*$#GFHH%#Ι+(&aa'm99m9 3.055_4i4_550.3k  #tttk"632&'.'#####֊v)%8 _^>:k{ZG_?g@`H,>|:=+,j,,<6O/233<bbJ 132>4.#"367#&7&$735&'.4>2,P*+P,.N+)PƗd"/%(MM~95DLMNMD2)WN,,NWP**g!ʇw֜s~  &JJ&?GO277''"/&'&'7&'&'7&47'6767'676?  6"&462EG#96\>42(p __ p(24>\69#G#:5\>42(p __ p(24>\5:'NmNNm U%4m+3 EJ5:6JE 3+m4%T  T%4m+3 EJ6:5JE 3,l4%T '\nMMnM* ? !&+05:?DP3&7"7&'7&'7&'7&'6'6%676767&77&77&'"32654&'5&'.4>323#67#&#"'5&'&547&"'6%6761a$O` "NiB*4l,4"U47),3($aM#"aT*BF 4,=44#Y3,)0BB0/CBO"!-$F$FJF1.#- -#-2MJF$G# 8<g7*!2U6J%n=_CBnT> rYw0d "*7]6U$u=n;wBLz >\e0wZ3C.1BB1.C(N "%""%" M#p.PA.$;QW$.AP-{ "R &.FR2#".54>&'767&%76'&''67&'&'&'67676547676'&7>3263'##"'&'&'&54767&'&547676&'&#"6&%6767&'&'&676&5467&'&6732767&h@9h),)RP|  |PR-*g:>/**Y&()((')&&)')(()% @9f+.TR"`33`\_ .np, 00441/ ,pn, ]]&&()&&EEEJ032WyQT.,d9@.**..1230IDE%&**%&F+.SEFE.IMMI."#FES. !  ";-0.--.0IM+.REF$$1.%2S_`Q2%-1OQQO2-$3Q`_R3&.>GIIG"" 7447#.$$FER/+L"  !75/57%"IJJI* )p~67&'67&'4&6%67.'4'6&&'6767&54?67&'&#&'#&'5&'"'67&'&47632>4.#"72#".4>"0'-, )*#'05%"*%%, ),,"GNYI'+""$(JYNO21, 9,4=SM:7,: -12-[[Z[]WXIOMKLMN2 Y{\ bCWDJgABcp7L^BML0b \u]! @R%KlhhO+ww+O hhlK$PZX'@D 0:)ww*;0 EA&XZw[[[[GJMMJ"( %3!'#!52#"62#".54>o:5(67%'$(n H0L*I"33'554#$/* PR 6h"&>I > >A>!!ua!&5476'#5!+{h_a66mHHm.rZy'#"'&#"'&'&'&547676763232767676'&'&'&/&'&'&547676762!2!%3276767654'&'&'&#"&#"3276767654'&'&˗Pz  ,D@   7;+  23  M98G ):               r         0   L:5U        .\ r26767654'&'."#"'%"'&'&'&54767676;27>764'.'&+"'&'&'&547676762%632$"26767654'&'&#  #  @!R763276;%326767654'&'&'&#"6767654'&'&'&#"32ɓ E79E"  21 +96  >B+  # zOo              49D   /    "    :           =JZx-4H67&'&'&+"'&'&'&4767676327632 #"/#"'&'&'&54767676;276276767654'&'&'&"276767654'&'&'&""'&'&'&547676762"'&'&'&547676762'&'&'&547654'&'&'&";276-&#"+"276767654'&5476%327%&"'&'&4767628?.  !  !a=?^'_)\?=a! !# "!.8?""  "  "  "   f  2 .?E S@6f G=. 2  ŕ6@  B   )_>9 9>_)  % I        ? *        ;d.      ?P   !-  @( ,#%>  NpNM&_*# (! &) ,,f&  ! (K_  Z0-  Yi D   cp-)L &gK1 [N3$ n/ "!0{I"H#fmt2>,7HBI.;/8[, Q[z)  .)S9L *E   '+(4%(4  *X >  7A) 0'-570+I;-% *#%(0  ]'5.  U-9Lp{7654'"'&#"+"'7&54?67676763276323273#5%6767'&#"6%"/67#"27632327654'73654'676547& t!M#l5G;@\ 2BX-0%-m * '?,N ?'!&R;-> <\-R5-6E!"$b$6$!q",; t@P"#C  *FS "DX@! %z$(`]jMP   &O/+@ p_u<  3  DMKZRdYL6D_YBI5.!!''kGWz")3SZ67654/##3276?7%754'654'36767632#"'&54767632'0,,; (| w| ki5.U,\\    %g .  ;,-0j{w {w3V. T, \[^     -5& '-EL4'&'&/767675'7! !'7!654'!4'!!$4767>2"&'&'!654'$$CCC||]V|V#u    9Z(f(Y7%$66%"'&'&'&47676762%'b&I    )^tN/  /dIW?    @ViDV /  V%&%$64'%%&'&'&"27676@))< "  " ]NO]    9|23277632#"'&'&5476"# 6v>? (-=%P8j?  #j<  y"$"JrB23277632#"'&'&5476"" YTo k%,02?=V8jiA{C {u+'qP?  ' 7 sssssstsXrsrtsssr@Q  ' 7 5NB2632#"'&'#"'&547677&'&54763267632676   Bt  ah>) c!  ,Hs *ܡ   },"2A "  {3+Q26#"'#"'&'#'&'#"'&547&'&54767&'&54763267632676  ΂    NjM  rkW* & \ *3 #ﳎ*3 Tv! ( 5+" , @V #!!!!!%!!!!!!!!#!5!3 ;E;JEJJJ<;E;EJK!IKV{ !!!!!!||uv9f !!#!5!335#*+մ*ִw0r!!%!!!!!!/0``1/`1) !!#!5!3^^^~S3!!'#'!!#!!3!5LDʃDMA #5!#3!3'3#!#35!3###5353;9nj#5AI##0vQ#"#3;54'&'&'!"3276767653#4'&'&'&+3!52767>5/]LED73!&&54GBO]63H>SkS>H388]OBG45&&!35FEL]63H>SS>H38882I32367675&'&'.5467676236767>32#"&'&'&'#"'&'.546767675&% >#"? ?"#>    G   >#"? ?"#>  G     F  >##> >##>   F    ?#"> >"#?4'&'&'&'.54767676322767676767632#"'&'&'&'&'&#"'&'&'&5476767676765"#"'&'&'&5476767632B ,#,+%) 3!, &&*-#''#-*&& &$0 )$W$) 0$' L+,$&&$,/"&&$b3") M*,%&&%,."'%%0 )$W#) 4!, &&+,$''$,+&& &$1 ,#,+$)0267632#"'&'&'3&'&'&54676763267632#"'&'#"'&'&'&5476767#6767632#"'&'"'&'&'&54767#"'&'&'&54767676325##"'&'&'&54767#"'&'&'&476767632&'&547676763235#"'.'&5476767632&'&54767676h             -  (                  '    *             .         +j276767653"4'&'&'&+sidUS+*+'WPihtthiPW'+*+SUdi),)URhexuhbXR,,,,RYaitwfgSU),%t?247676763"'&'&'&5!276767653"4'&'&'&LEA86:4DDMMDD4:68AEtjdVT,*+(XQjhvvhjQX(+*,TVdj-76DCOME@:66:?FLOCC67-*UShgyvjbYS,-,-RZbjvyghTU*,(8 %%! !)ttJHcdecH]F]~]^C5 )!%%!2#"'&'&'&54767676hzt@z@Az@t{ne_RP)((&SNcdome_RP)((&SMdd0x}*(QObbrle]TP)**(QObbooe]TN+*(.'"276767654'&'&'! !_)(""""()_)(""""(Y$(*/.*(#  #(*./*($]^#< '1%%2"'&'&'&5476767! !#xxa)(#""#()a)(#""#(YDgghgD^I^W $(*0.+($  $(+.0*($ YZ(8 3'7'3!%%!! !hE۱CCDe g  g f ҁссi:]^= 3'7'3!%%!7!7'7!hTDEDDTNPPIQ2P11P2#mm(? -5%7'%!! !] P  gfeer­696ƌ]^^. /'%!!%!77!yrryyqm"_^^l%%tu%ߴ߳!63% %#'-7:|:||9|kֵֵkֶWz`37'%7% %#'ZZZZZ]^Z^ZZ˛ʜm˜˜mʜ0o #'!5!73!P6M6P$6PMP66R#6QLR6$Q6L$z     - h<_K <; L_zK <; J`;<_  '!'/7'?!7% % -[9^[[ZG^ZZz'}*}zy}*}'q^\\ZG^ZZ:\O}zy}*}'yz(}2 % %  h_y(_^(zFGs% % -hVHzVUzHrVU{HUVH% % -hhhႁhhhႂhhh$h7% %' 7-'hX5 5XV6 6g5VW6 6WV5 0t/37%!!%'#''7'%!5!%7'77;[TA:#T8#AT[TA#9T#8AT T8#AT[U@#7S#9@U[TA8#154'&5476276767632#"#"#"327232#"'&'&/"'&5476=&'&'#"'&'&54767632332?&547'&#"#"#"'&'&54767632676?>$,.c.,$> ]5 71+: H3> kR  Sk >3H :+17 7Z  >$,.c.,$? Z7 71+: H3> lR  Rk >3H :+17 9X  ib9@R'))'R@9dg  8d< +$;)01):$* \570+9 F3= kQ  Sj  =3F 9+077Y  >$,.a.,$? Y770+9 G3= kR  Qk =3G 9+079W     > h`9@Q'(('Q@9bf  7c<+$:)/0(:$+HH#:.'W4,CEH@,4W'*>&DL:Z##KGW,f ',;[;;+*Q--}KOW*AA*WSGu5-U&+;;[;,)  '+;[<>**Q--}KNW+@@-USFu5-S(+;>Y;+* !67654'&"327632#"'&'&/#"'&5476=#"'&'&5476763232?'&#"#"'&'&5476763254'&5476276767632#"'&#"#"'&#"327676%32767654'&'&#"#"i/)F)/,UK:M $\/8E(5>H6-EFJA-5H;8)D7.\# L;KU,*UK;K #\.7F'5>H5-DE-6H<7*C8/\$ M:K U+:6-21 4 $:<;$ 4 22-6 O;(A7##7A(; !*:#.#;&Rm!CcJMU)??,RMJcCoS%9#.#;)!  );#-$:'Qn!DcIMU*??*UMIcD oS%;#.$:* f /D;;D/ $i"276767654'&'&'767632#"'#"'&'&'&'#"'&'&'&5476767#"'&'&'&5476767632&'&5476767632o00'))'00o00'))'0]0+)*+%# #+%0%##&&.0%+%   #%'.0$,#0%-# #%'.0$.  #%'-1$,#$%*/0961/*%%*/1690/*%) "*&0-(%$$$)-0&*!&"*!$$)-0&-#%(-0&*"" (-0&*"$$(./& n%#"'&'&'&5476767#"'&'&'&5476767632&'&54767676267632#"'#"'&'&'&27654'&'&'&"67&'&'&'276767&54767'&'&#"276767654'&/?676767654'&'&'&#"h &,&1/(&#!$&1%-$!&$/'.)2$-%c%-$2-*++&$!$-%1&$!#&(/1&,& =s0 9 55%R 9 !_  , 9 R%5s  _!#'"+'0/)&$%%).2'+$ * '1.*%%%%*.1' * "+'2.)%%$&)/0'+"'#L% %L %#M L:2(&6  _ M#%   6&(2: -[3b &'#"'&'&'&547676763267'&#"327%327676764'&'.#"7632#"'%&'&54767676324676762676322##"'&'"'&'.5#"'&'&'&54767"'&'&'&54767676&'&'&'&'&'67676?&'32767677676765&'&'.#"7676767&'&'&/326767674'&'&'67'&'&'&#"67'&'&'&'67676767"276767654'&'&'"'&'&'&54?&'276767654'7654'&'&'&"67'&547676762    (  b  (       #!"G"!# * " ' ## G!""  '  Y m    ( y   ( O k  w  m Q (  O (     ? ?   + / L* / *   +. M+ .*   !!!! '? ?' "#& #'"!!  '? ?'  !"!  $&  m P    O        m     y    O k         b       %j<\l"276767654'&'&/2#"'&'&'&47676762#"'&'&'&54767676% %-[''!  !''[&( !! (TB39)+,+76?A3:(+,+76>tjeVT,++(XRiiuskdVT,**(XQijtuz"z!uv!z"z#&(,-''""''-,(&#e)*:6?;97,+)*97z88,+,*UThgyricYT+,,*USigtvjbZR-,zvvz"z vv!z29"327632#"'&'&/#"'&5476=#"'&'&5476763232?'&#"#"'&'&5476763254'&5476276767632#"'&#"27654'&%&'&#"327676%327632 654'&'&#"#"i"(-+S I9K #Y.6C&4<F4,CDH?,4F96(B5-Y" K8IS*)RI8J "Y-5D&3<F4,CC,4F:6(A6.Y# K9I R*"(-62 #9~3 #9; 01+56 00,5`%;G,A $.?'!3&@!*Yx$ ImPT]-EE0ZTPmI "zZ)!?&3!'@-$ #,A'!2'?!*Yx$ImQT\.EE.\TQmI#yZ)!A&2"'?.#~&41%%14>3t-3>41%%14>3f^CC^B%@#@@%@#-4>41%%14>4-3>41%%14>3+  V  ++  V  !r +?Sg"&46277''"'&'&476762"'&'&4767622"'&'&4767$2"'&'&4767eeeBABA#U##U##U##U#V%**%V&**KV&**&V%**~ffeAA$AAV%**%V&**V&**&V%**#U##U##U##U#  &3@MYam+%5%32476;#"'&'?632&54?#"632/&54#"/72#547"&462"'&=3?_?6 6  6] 6'?&M&C_CC_?&M&< 'L&&L'!6 6^6!6 >_CC_D<>"l267632%632#"'%3#"'&'"'&547#"'&54727%#"'&47632%&'&54763&5476h!#;'&1'h 9##8)'1!, ;#A#; '&1')8##9 h'12;# 4%.&! 6 = 6%".% 3 3 G%.56 = 6 %".G 4 $8 ! 54."#"54$32632#"_ ɀ~~a>E  %!#!3!p EE?p9E=V %!%!35!cE:d FF8 %!!![:F:\;[0q %!!7!N]<N;)G+t  ,o9; #q !rQk!k`!733}b>v!#7#)iC~'  #'  #g]jOS2#"327676765#"'&546;57!##"'&'&'&54767676%#     42;%-n`Ԯrr#26A@:V7:$)&7.Yq   % $.277g[(dVDQ49%*,04?()-#52&'&547676762"'&'&'&5476767hc"$njln(Lfe*+$$$$+*e*+$%%$+! #'(*dRjjSc*('!"%*,20,+%""%+,02,*%"%C&'&547676762476767622"'&'&'&5476767hcn(%X%&&&W%(nؖe*+$$$$+*e*+$%%$+,Dj*('(&,,&('(*kC"&*,11,*%##%*,11,*&".i%%&%&54767676247676762hhÔ*(42u24)(()42u24(*i\=97,*+*96@@69*+*,79=Zr_'#"'&'&'&547676763"'&'&'&5476767632_dA=;0-/.=:DD:=./-0;=Abx1.=8DC9() 1F="%".4"tNa5&$4! /.r<@6B2L_0>Q#kI|"rz7&)?),%=^K=.C26F@13.!9+cM313N676 547&'&327#"'#536767&'&'&5432&5476323254'&543253%5@26`', =NR6#!vWR>4 2:O t51"".1&X.RO A5ȏ )T/186,FAS :#(=:tA0 9SD 'A#5}11BO9 "'&'&'&547676763"3ᗊpm8884qlYTN! !C@RP]e:6pltm9:'62~~jf77"05276767654'.'4]PR@C! !NTYlq4888mpe'67fj~~27&:9mtlo7:fkR !&547jljjlyyxzQqpnc$0!!676n wu;;vi43f$lcC}U# 3tD}U 3 DutV.! !JV. ! JA! !m^\GHB ! ^^HHv!'7DWWWW|'7'7WWbW>W^$#"&=4&+5326=46;#"3xMe,,fLx1d=AOOA=dƂ׈ihDŽOߍOi(326=467&'&=4&+532;#"+5nCFVU$#Cn5BB*)p//oTBB¥P⎁AAPDBۇ45iDCS/~ #!5!3}t]} 7%d^=]d>S~5 /%0~##t] '-f\=]d]>-!'7!. (``I)=2"&'&'&5476?!".'&47>3!'&'&54767>2 '!  `!!  !' ,&   &   S~&!5! F78-x!5!5 V(Mr6u #3#3#3!!5 鴴ZZ---I(,,,,S~  55!#3#3#3F9UU**b]^bUUUS~!!5 F7.`tq!%  qR{V$%! S%@{V t%226=3!5 5!"'&'&'&6  $hI$  h$   6<47676763!5 5!"6  $hI$  $   $O!! e 6n55!lMlTwccwekl!5!!53 ' !_[y"kd""e/l5!!53 ' !_["/d""5 !73#57!%!6UcGjbzbdǩ""ap 5!'53#'!!!7%acߎA[؁(ZqZ{{{ĒҒ}TM %'!'!53 !;qKRnKa26wwIw22wT}> 3#5!7!!! ZQtZQ0L>ssjLK2Nu '!53#'5!'7! !pSn%R&%Ua2wKJ,Lw22w)1 '7!573#5!7! !r&j&St&SpWl6qM,LLyy77y@!6767632#"'&'&'!  6IYZgb^UMI%&&"LF\Zfc^UM3!t:6I&&&#LHZZhc\UMH'&&#L2<tt XNy "&*.37#37#37#37#5'!!55!!3'#3'#3'#3'#r+qr*rr+rr+rV{{*q+*r*+r++r+9Ɔ\]t] 7&#"7'7 #%5#t69.wZY96t".*X/S~k 55!5!!7'!nnUVGG8:ȏu\j '327'' #395t".Y/Y"u69.xXXN2%&#"6767&'&"67632&'&547676767}:"  s %*&*(&"!#!"O>>;*E/4767!"!47676763"'&'&'&5!3!&'&5v  5 $ %% $  H vgMME %!#"!% EMu\2&'&'&'&54767#"'&'276?&'&'32\":  #'$'$#Y@I:86s6::I  #&'#'" X  :5*+B67"'&'&'&547676$47676762"'&'&'%&'&'&547676762$[ /  H =a=   / ZI=X  q> d(*c     XJn.676767632#"'&'&'&%&'&54767&'&54765 #&+*1)F-Y)) .EOO/3S>>S&/ #$))%#]]#%))$#&e"'&'.54?654'&'&'&+"#!".4?64/&4676763!2;276767654/&54676762 I ]]I    Q      Q  %eg"'&'.54?654'&'&'&+"#!".4?64/&4676763!2;276767654/&54676762 GKa u~iKG E     2 +#76767&'&/3#6767!5!!5!&'&'g?j7R=y66y=R6k?VO S+ +Sd _8=eyu'&utj>jiVVy ##! !+532765YZͷZ-,'ij>>% %!#3!3iVO7n lѲR{H3!!# 9`#3!{`9Lh '"27654'&'2#"'&7673A\VMMG*w|~~hA1LLNeˑRh]c[斘,mKseg.!#5#"&'532654&+532.Dv6;zIMcݳw"$.*gQH{"264   676326^\`s-!\[^[]-=P@7'"]_$5hWd3#3h݄D%36767#"'&5476?>5#5% Yb^`_hon"!^XE&->B%DS #D9``LAB\VBT=;-;,,1Y7:w!##Z:#5!#J&w3!3XZ!533XK,X3#3,X0X,X3#3,$dX,X3#3,X,X3#3,X,X%3#3#,񈈈X,X3##3X0X,X3##3$dX,X3##3X,X3##3X,X%3#!#3X,X!#!!yX,X!#3!!Ẍ,X!#3!!X,X!#3!!Xd,X%!!3XT| 3'#'L:LjLCUCT| #'737:LjLCUC( 3#3#'( f*D( #53#73 fRo(D d'IJ !5!5!5!J>>I3#qe10#+H %"32654&'6#"467.5463%!"h{1PAž(rڜ-*/1|I5#7N@*        JEE<2<2991/<299903#'#"!#!##535463wcM%ɩQge/яN#7B@#    JE E<<991/<29990#!"!!##5354637cM%۸ɩ{Qge/яNE & K EE & ? E X& R E  X& S E E & K DE & ? Dx X& R D X& S DE & K FE & ? Fr  X& R Fz X& S FE& K E7'E& ? E-x& R Em& S EE& K FE' F ?y& R F|& S FEW& KLfEH'4W ?& RK& S]r& S >tj' >v C?' >,~ @' > A~& S Fj ' Ft C?' F,x @ ' F AX g& E{X g&  E/}>\/' Ea8 >/' E 8 X g& =X g&  =&8\/' =8 8/' =8 X g& D*X g&  DB\/' D >/' D8 X g& F2X g&  F^\/' F >/' F8 a7& >` D' >\ 'U` ' ]A2%#"'$4733267654'&54767;#"'&'0Qcpl?AOL64)>.VhFd((&*=#>2(I=/ b \^xHj<9"1B,f%TOA7.N?+  cG&A GJ' GT E+9' H F9' H G6 U  &#"'&47332767654'3;#"'G{579T?:!e#"#V4^Wt<;?xC3^w3UT8@0C&6D%3!"''&5473327&'&5476&'5%67654'&#"%3276'&'&bJDv-(0g:-0M,QG$"':AG 5' 343%@K5:,+ CfN@TSZ 'AO@H=.%4-+#%v_U[1C "&_ ,:%&'&5476&'53!"'+532767654'&#"%3276'&'&\:-0M,QG JF$"':AG 5' 3CfN@TSZ 'AO844U@H=.%4-+#%v_U[1C "&_X %!#53 76=3`H՞,1VV,1jٻX%#!5!276=33!!"^L,02,*VV,1jj1,{ X' = RX' = SD&D"k ;#"'&=31,cK\WL71,\W+DD&D&D&0&Vz & U-&!'! u :&":'"' uL 3&"`>Z '"2>   &# &#V uW&"Y{`'" '"p R'"p S L 3;!"'&L2,PXskj1,\eE& K <E' < ?X& R <X& S <& =j' = E & K =E ' = ?#' =R R' =R SE& K >E' > ?,& R >R& S >,X g& <X g' <p \/' <  /' <  X gX g.%3#"'&'&'27'&5767&5$(1{R=IrbJԖ`e_$m3HZdP]vbĘe4)@5 [_w\/&'&'&5672+5327676SSgURHKLXJKݣdht^#4b4bBPH:jV/);#"'&'+53276767&'&'&5672~AI2h6:H~D& <] ` ' <  !F"'&''#"'&7673327676'&/37653323;#"'4A@E2J/1%=Gq_evh+&%*TdOvG#%,9?#@*8m~C "RVxgt7323;#"'#"'&'#"'&'+53276=32765!5d*,""@e;6:HO7* F35C%&F:DF*7@`*%Rx&),`X I6[m8*6759,%#GvOdTtg!& >6 ' >  ' >  ' >R   G%327654'&#"%;#"/+"'&5#"'&5473767654'&'367632Ij($?GhK=.';4f&4-//3fJZ}eh<2H7(28`@%(jm=vbw $A7. *727&'&5763"327%+5=K4X}ں>SF8I \Y];d}M4F!Ť$/%+532767&'&5476762;#""654'hkB;(aD hYYf MXD=pʨ4/gg/($'UZ'-)74--38)-'bM,(U __ z F&  <w L' < F' <w L' < & S <*jL' < C?' <~ @~' <" A|t& L =]~' =Nk B?& @ =,~~& A =!D#"'5327654'&'&7676##"'&547#76767;5#"'&5J&P DfXRNB8D-<9_h$$EB|=Q&v+6(  %z|qe))5!27654'&54767;#"'&/P/62 hF F&,@XB:f"``h$$EB|=Q&v+6(  %?+)x.BK $##"'&547#3276767;5#"'&5EcTiI(A@wc}^h3N8K+,19)"B~ss\V*"/~?]3,1j )5!27653WP,1se\,1j%)5327653;! hL,02,VV,1jkj1,TGt4%327654'&'&#"#"'&#4763&547632;#" zL,5;(.;D Kp#IxAIM\HT(RfV*9:X DD(PNKOmf7*(?$G@^m*%'+53276767632%327654'&'&#"d`p@ht4,+^]EE>/4:''5)24ed0$#1P8O$*ME5EX !a%m/%#"'+53276767632;#"%327654'&'&#"|an@h4,+^]HB3&id>/4:''5).4fb0$#1P8S1>/E5EX d%6& U < ' <w M& R <R& S <R $&'&'&'3;#"'&'#"'&5476 xRot$8pqZI-&8:m*12e CY>)2'+eO,3;I0D-=67654'&#"27&'&5476&'5#"'+5327654'&$"':A4N--0M,Q;(Jxb 41~! @H=.%4-+#%v iEN@TSZ 'C49g=ql)D%'r.C!v-3j  ;AWE L9P)8K6(S/VL_+Y9K1\S{7%&'&'&54767632;#"'&##"'&'&73327676 ,)MW,.@"##C@"*5NhLy $Eq:<3, $.=N/[ EW3235huFX^"!ݺh^bNm/>ZUb=*(DV\BAL89DEnY1X;YTBEb%$q%1&R'N(X)fP*H+,m-.s/Vy0F1u\2u3u\45J6/7=899:;%<=#{DXE%{F{G{X{H'I{H{JKDLVMN Omo{P{QH{RVT{SRwTj{U{VW^Xdm`Y`ZL`[hV`\b]LF#7fo-L7NFf@103# qf?Q@ aa1<20KTX@878YKTK T[X@878Y3#%3#?Zk10K TX@878YKTX@878Y@&  //// //]]3#@   99991<<990KTKT[X@878Y@t        !      ]]'.#"#4632326=3#"&d9 #(}gU$=19#(}fT"<9! 2-ev 3)dwyi10K TX@878YKTX@878YKTX@878Y@ //]#1Ś7]@ 91<90K TX@878YKTX@878Y@ //, ]3#'# ӌ7i@ 91290KTX@878YK TX@878Y@ //*//]373 ӌ Zj@ 9910'3$ll/{m > #.#"/  waSRd {xz{w8796/{m @ PP120332673#"&/w dRSaw m6978w{zPa103#PXck@ 1<203#3#\zkcyk#!#uŚŚkf$(#5476?>54'&'&56763#7:)*%>8'9@B9Q[~CG=9. bEBTY;X1I*%#D>"Y`LAB\VE'*==010!!=V045!5!5gr4!5!;r4!5!/r^s 2"&46"264hrL&'◚vURyVP%`8n|TyRTv?F3#%3#?53#73#'3# 3#3#'3#}}dE&"'&547332767654'3;#"'&')+ubEW-7!K4/+X`t8A>|0l7%5A8>7KZd3(,*OQ/9?0654'&323276767'&54767632#!V)B,4((7(*HTO<?aNbNLZB`.NJ|m+M;3*)3P& ]027EW4,E$2Hf3Џ,' 3;!"'#!5327&'&5476762"67654'&'ȷ$&ň($28 D@$ 8P2*I1C299(M.L,0W 5+5DE2.4!.@%&'&'&54767632;+'$'&547332766'&'&#"B.y9()Wp8c2X]0Lh^E>>:lu{/"'"5 9Ld/  #+m=E2X:SmJN}`kI="'&4762<R8R8z?@?@@?@(8)*8@@@@@??V<D@!B  10291/<2990KSXY33+532765#5YZ͹Z-,˹iij>>iE'&5473327654'ߚ)+ubj{G{yo9;>|0lALj@G|t8654'&32676#"'&54767632'&'&5473)B,4((7(*H[b?zKbNLc9g'!.9ΊMRVV+M;3*)3P&;f4KCW-3E$2Zwfj}ػH(E  ' I & K =E' D  KX '"8X ' EX ' >T&$>x9654'&"32#"'&&733276767#"'&54767632)B,4P7&,Hir$$xZT0A&?zKbNLZB`.,L95T2R$T&()X\^-"2(hLDV,4D$2Hf3B'$6#"'&47332767654'3dG{579T?:"FHt<;?xC3b`L 3H'$p'$(J=s_<LLUmhR!9\XdffXXX%fmVuu/9%fZH{{{mjdLhX%?wXd=+XBFjX%%%%%%uuuuu%){{{{X/hh%%%{{{{{{{f{f{f{f{FmLuuuHj j///%h%F0<<^u?4An1mu -V8xv// 'J}}}9%uz%%)f{uu}f{=)/%%{{uuhj/}%{uuuu%hjxx/!79{{{zz8{zffX(hhgEfjjzz}}}v_AHHf}i_6w#6AAQ0^^))$=$=>/VX%V[^,,,,,)=/?VX)"V//yysb^_s?$U))//=X/UU?p6%%u%Vux/%uuJ%F63F 6DtP3LYF633Fr"p"m93u`!Y4uVUm"h%!Vu/hBPr< A<L}i{;=hcL|}PhN{{# A#huU;b/%\%\L;%%){uzuz;}uuuhhhhhhAhu11ZdL EEEXXX"" zzB6LDDDDDD0V\~~6EEEEEEXXXX ~~>ly|ar{jK||cceerd`oawT0ZSSSSxDV_In7777-7777Vv7km#)+?+77LL,:d^E<<.?AEEGGG11OOGI8%[:X::GM[%#H[#{:GXQ:OWbG[CaIGdUU~%%UU?::[xM Fa^M3:%{{{}{{{{{f{7VmVmVmjj==////9d9dLL%hh%%%%{{{uu    ' ' ' '%h%h%hFFFFFFFF%%llMM@@cc66'33333333@EFFFFFFFFfFF633FFFFFFFFFF%%llMM@@ccFFFFFFFFfFFFFFFF%%%['/66333333%%[p?FFFFFJdd??PZZ!!=H ?I=;0A=XBF ?I=;0E1:1 {_m *{%*/.j5':TTJ B%0J%  BBBBBYYBBBBBBBBBABBq?QQ2BXXBBBBGB*BB*B*BBBBBBBBBBBBBBBBBBEEB*BBBBBBBB%uIXXf+?;;;)}}?5XJWXXXXXXXXXXXXXXXWXXXXXWJJXXXXXXXEXXXXXXXXVVVVWXXXXVVVVVVVVVXVVVVVVXXXXXXXXXXXXXXOOOOOOOOOPPPPXXXVXVVVVXXXXVVVVPIrZZ% % XaGX++|h}2H%%?XX%XX6FFHRFFF    xxxxxxx||iEiiDDu777777?aa"2"Y::;+6.ocN8/&/// "N`ya7g!!!!GCL54=/U28Y^CVpj6gv2 1.rjD7`./<KD$>K--9.7.P<<<<<<....RRs'J*R*"..:=4%=?D-W&W;%J@3@V90~0G+%(C(#(=(.6W0$2$017+!$%2 02)!"$E=80+Qg.rA1fnCDStSt-IS-6SS`{{66O6ee5a}T2)XtSuN*u5&%277uuXXV%LZZ,,,,,,,,,,,,,,,TT(((##EEEEEEEEEEEEjjXXXXXXXX`` 6|DD"DDDD0VLZ| LEEEEEEXXXXXX``""  zzjB G6LZ||@S__R%fmVuu/9%{{{mjdLhf?y77//Xf=^?EjEEEXXXB6LLLLL,x 8d x ( d  4  ,(PD  !!T!!""@"#p$$%%&8''p'(P)L)*+ +,,,-P.<./ /0123X4$4T5$55567L8d989::<<=`==?,?X?@<@ABBCC@CPCDHDDEE FF8F\FFFH$HHI I$I<ITIlIIIJlJJJKK<KtKLMM4MdMMN@O$O<OTOlOOOQQQ4QLQdQQQQQS S$S<STS|SST8U<UTUlUUUVpVVVVWW,WDW\WtWWWWWXXXDXTYY(Y@YXYpYYYYYZZZ0ZHZ`ZxZZZZ[$[\D\x\\\\]](]@]X]^P___0_H_``\`````aa,aDabHb`bxbbbbbcddDdddddde|fffffg g$g<gTglgggggghh8hPhiLixiiijj<jTjljjjjjkk4kLkpkkkkklldlmmn ndno`op@pPpqqlrrtsst txtuXuvv\vw wTwxxLxxyy,yyzlz{\{|L|\|},}h}~~ ~<~~TH d0 dt4Ll ,DTt $<Tl, 8Ph(@Xp0H`xLTD,Ld|``\XxDX(H|pLt<,txP@8h<p`H < 4p< |0x d<Hxp0(HlH(pP D$t@X$\œ¬¼Tð h4DlňŜŰƼ 8HȄ Ɉ<`ʈʰ 0\lHX̸̨4l͈Ͱ<`΀<hτϴPмdtѐѴ<x ,<XpԌԨ(pդ,@րּ֠4D\t׌פ׼ |،؜جL\lDTdtܜD\t݌ݤݼ|,H`h@D8p( $4Th4(dtx4< 8P(`H`pHXhxHh\x(8pPhxd ,pXh t L  8   ( @ X       @0`p<p,T $<l \L\l`XH`p0H`pDTd| $<Tl ,<L\l@`\  , D ` x    !D!\!!" "$""##$,$D$$$% %$%&<&&'H'(l(()P)|)*(*D*+ +P+`+t+++,@,-@--.D.//H///080P0h00000011(1@1X1p1222333 3034444445667D78x9 9::;H;<h>?D?@pA A|B<BCD<DDE8EEEF,F|FGGlGH$H@HI<IhI|IJJdJKlKLDLM8MMNPNOOOPPPQ Q|RRSSSTlTUTUV$VWWtWXYYYZ|[[x[\L\\]4]^^_`__` `h`aaLaab$bTbbccPcccdd@dde,eTeeflfg<ghLhii\ijDjjk(kkl<lllm@mmnnpnnnnnodotoppdpq8qr,rlrs sdsst tHttu(uHuv8vvw(wx,xy yyzHzzz{@{{|P|h||||||}}0}L}d}|}}}}}~ ~$~<~T~l~~~~~4Ld| $<Tl,D\t4Ld| $<Tl,D\t $<Tl,D\t4Ld| $<Tl $<Tt4Ld| $<Tl,D\t4Ld| $<Tl,D\t4Ld| $<Tl,D\t4Ld| $<Tl,D\t4Ld| 4D\l $<Tl,D\t4Ld| $<Tl,D\t,<Lp(8P`x 4Ld| 8H`x0@Ph0@dddddddddddd H`8P`d$Dh|00`Hp((\0DXl  4H\p8x\0X4DX@Ĉ<|ŌŜŬ4TtƔƴ4TtǔǴ0dȔ$TɄɰ$l˼L̔\͘LΘ Ϭp8PҰ,dӜ D`՜֬Txנ@d؈0ل\ڤ ۄdܤ$d\ޜ\ߜH0|L@|$<Dx$hT @tX TX|hp$0@h|\tX l|l`$Tl\$l8dt,(  h  4   D    H 8|d`XL$xD8TPHD , d  !,!|!""<"# #$8$$%(%t%&t''''((@(h(())$)D)d))))*$*D***+0+,p,,,,--p-..d.3h334 4d455(5686P6677T778899h9::P::;; ;8;P;h;;;;;;<<,<==>8>>>>>???0?H?X?@@@ABCDE<EEF F<FtFFGG G<G`GGGGHLHHHI0IdIIIJ<JXJtJJJKKPKKLLLLLLMM(MLMpMMMMN NDNdNNNNOODOpOOOPPDPlPPPQQHQpQQQRR@RlRRRSS<SdSSST T<TpTTU U@UtUUVVTVVVW$WXW|WWXX(XPX|XXYY4YhYYYZ ZLZZZ[([X[[\\0\p\\]]\]]^0^h^^^__H_d_____` `(`L`t````a a(aDa`a|aaaabb$b@b\bxbc@depeeeeeeff,fHf\ftfffg,gDggi,ij<kkl$l@lllllmm mHmdmmmmn n<ndnnnno o8oTo|oooppXpq8qrsHst4ttu4uuv@vhvvwDwwxxDxpxxy$y@y\ytyyzz0zXzz{{4{\{||<|||}0}~~~~ 8T 0h0\,L<Hx<x h8\(TT(0@t8H(hLh|ŌȐTʬ˼̤hͼ 0ЌPѠ҄D( <(8X4(thp( `@`0D8l(DDP HTd 8   PLxX@,hLPX  !!"`"#T#$4$$%D%&L&&'0'(D)+-8/h139;(=x>8>?A4BDETFGGDGxGGH0HLHhHHHHIIJK|KLxMN|NOTOOOP PHPpPPPQQtQRR@RlRRSlSST T\TTTUU\UUVVLVVW,WxWXXlXY|YZZt[[\X]4]^`0`a(aaabb4bxbbcc4c\cccddDdedeeff<fg gggh8hXhxhhhhii@ihiiijj0jTjtjjjkk,kXkkkkkl lPlmhmnn n8nPnhnnnnnnoo(o@oXopoooooppp0pHp`pxpppppqq q8qPqhqqqqqqrr(r@rXrprrrrrs|sssssst ttudutv(v\vvvvvwww(w\wlw|wwwwwwx xx4xDx\xtxxxxxyyy4yLydytyyyyzz z8zPzhzzzzzz{{({@{X{h||h|}}}4}L}\}}}~~\~t~~pd0H`pL $48(@Xp0<th|d Td 8PhLDTdt$4DTdt$4DTdt$4DTdt$PP|LDp <Xt P`x$D t$D\tl$ Y +kW_B]  @  4     S  b     " :R & hhCopyright (c) 2003 by Bitstream, Inc. All Rights Reserved. DejaVu changes are in public domain Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. DejaVu changes are in public domain DejaVu Sans MonoDejaVu Sans MonoBookBookDejaVu Sans MonoDejaVu Sans MonoDejaVu Sans MonoDejaVu Sans MonoVersion 2.29Version 2.29DejaVuSansMonoDejaVuSansMonoDejaVu fonts teamDejaVu fonts teamhttp://dejavu.sourceforge.nethttp://dejavu.sourceforge.netFonts are (c) Bitstream (see below). DejaVu changes are in public domain. Bitstream Vera Fonts Copyright ------------------------------ Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is a trademark of Bitstream, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces. The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Bitstream" or the word "Vera". This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Bitstream Vera" names. The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. Except as contained in this notice, the names of Gnome, the Gnome Foundation, and Bitstream Inc., shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from the Gnome Foundation or Bitstream Inc., respectively. For further information, contact: fonts at gnome dot org. Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. Bitstream Vera Fonts Copyright ------------------------------ Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is a trademark of Bitstream, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces. The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Bitstream" or the word "Vera". This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Bitstream Vera" names. The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. Except as contained in this notice, the names of Gnome, the Gnome Foundation, and Bitstream Inc., shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from the Gnome Foundation or Bitstream Inc., respectively. For further information, contact: fonts at gnome dot org. http://dejavu.sourceforge.net/wiki/index.php/Licensehttp://dejavu.sourceforge.net/wiki/index.php/License~Z Y  !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghjikmlnoqprsutvwxzy{}|~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~                           ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~                            ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~                            ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~                            ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z sfthyphenAmacronamacronAbreveabreveAogonekaogonek Ccircumflex ccircumflex Cdotaccent cdotaccentDcarondcaronDcroatEmacronemacronEbreveebreve Edotaccent edotaccentEogonekeogonekEcaronecaron Gcircumflex gcircumflex Gdotaccent gdotaccent Gcommaaccent gcommaaccent Hcircumflex hcircumflexHbarhbarItildeitildeImacronimacronIbreveibreveIogonekiogonekIJij Jcircumflex jcircumflex Kcommaaccent kcommaaccent kgreenlandicLacutelacute Lcommaaccent lcommaaccentLcaronlcaronLdotldotNacutenacute Ncommaaccent ncommaaccentNcaronncaron napostropheEngengOmacronomacronObreveobreve Ohungarumlaut ohungarumlautRacuteracute Rcommaaccent rcommaaccentRcaronrcaronSacutesacute Scircumflex scircumflex Tcommaaccent tcommaaccentTcarontcaronTbartbarUtildeutildeUmacronumacronUbreveubreveUringuring Uhungarumlaut uhungarumlautUogonekuogonek Wcircumflex wcircumflex Ycircumflex ycircumflexZacutezacute Zdotaccent zdotaccentlongsuni0180uni0181uni0182uni0183uni0184uni0185uni0186uni0187uni0188uni0189uni018Auni018Buni018Cuni018Duni018Euni018Funi0190uni0191uni0193uni0194uni0195uni0196uni0197uni0198uni0199uni019Auni019Buni019Cuni019Duni019Euni019FOhornohornuni01A2uni01A3uni01A4uni01A5uni01A6uni01A7uni01A8uni01A9uni01AAuni01ABuni01ACuni01ADuni01AEUhornuhornuni01B1uni01B2uni01B3uni01B4uni01B5uni01B6uni01B7uni01B8uni01B9uni01BAuni01BBuni01BCuni01BDuni01BEuni01BFuni01C0uni01C1uni01C2uni01C3uni01CDuni01CEuni01CFuni01D0uni01D1uni01D2uni01D3uni01D4uni01D5uni01D6uni01D7uni01D8uni01D9uni01DAuni01DBuni01DCuni01DDuni01DEuni01DFuni01E0uni01E1uni01E2uni01E3Gcarongcaronuni01E8uni01E9uni01EAuni01EBuni01ECuni01EDuni01EEuni01EFuni01F0uni01F4uni01F5uni01F6uni01F8uni01F9AEacuteaeacute Oslashacute oslashacuteuni0200uni0201uni0202uni0203uni0204uni0205uni0206uni0207uni0208uni0209uni020Auni020Buni020Cuni020Duni020Euni020Funi0210uni0211uni0212uni0213uni0214uni0215uni0216uni0217 Scommaaccent scommaaccentuni021Auni021Buni021Cuni021Duni021Euni021Funi0220uni0221uni0224uni0225uni0226uni0227uni0228uni0229uni022Auni022Buni022Cuni022Duni022Euni022Funi0230uni0231uni0232uni0233uni0234uni0235uni0236dotlessjuni0238uni0239uni023Auni023Buni023Cuni023Duni023Euni023Funi0240uni0241uni0244uni0245uni024Cuni024Duni0250uni0251uni0252uni0253uni0254uni0255uni0256uni0257uni0258uni0259uni025Auni025Buni025Cuni025Duni025Euni025Funi0260uni0261uni0262uni0263uni0264uni0265uni0266uni0267uni0268uni0269uni026Auni026Buni026Cuni026Duni026Euni026Funi0270uni0271uni0272uni0273uni0274uni0275uni0276uni0277uni0278uni0279uni027Auni027Buni027Cuni027Duni027Euni027Funi0280uni0281uni0282uni0283uni0284uni0285uni0286uni0287uni0288uni0289uni028Auni028Buni028Cuni028Duni028Euni028Funi0290uni0291uni0292uni0293uni0294uni0295uni0296uni0297uni0298uni0299uni029Auni029Buni029Cuni029Duni029Euni029Funi02A0uni02A1uni02A2uni02A3uni02A4uni02A5uni02A6uni02A7uni02A8uni02A9uni02AAuni02ABuni02ACuni02ADuni02AEuni02AFuni02B0uni02B1uni02B2uni02B3uni02B4uni02B5uni02B6uni02B7uni02B8uni02B9uni02BBuni02BCuni02BDuni02BEuni02BFuni02C0uni02C1uni02C8uni02C9uni02CCuni02CDuni02D0uni02D1uni02D2uni02D3uni02D6uni02D7uni02DEuni02E0uni02E1uni02E2uni02E3uni02E4uni02E5uni02E6uni02E7uni02E8uni02E9uni02EEuni02F3 gravecomb acutecombuni0302 tildecombuni0304uni0305uni0306uni0307uni0308 hookabovecombuni030Auni030Buni030Cuni030Duni030Euni030Funi0310uni0311uni0312uni0313uni0314uni0315uni0316uni0317uni0318uni0319uni031Auni031Buni031Cuni031Duni031Euni031Funi0320uni0321uni0322 dotbelowcombuni0324uni0325uni0326uni0327uni0328uni0329uni032Auni032Buni032Cuni032Duni032Euni032Funi0330uni0331uni0332uni0333uni0334uni0335uni0336uni0337uni0338uni0339uni033Auni033Buni033Cuni033Duni033Euni033Funi0343uni0358uni0361uni0374uni0375uni037Auni037Etonos dieresistonos Alphatonos anoteleia EpsilontonosEtatonos Iotatonos Omicrontonos Upsilontonos OmegatonosiotadieresistonosAlphaBetaGammauni0394EpsilonZetaEtaThetaIotaKappaLambdaMuNuXiOmicronPiRhoSigmaTauUpsilonPhiChiPsi IotadieresisUpsilondieresis alphatonos epsilontonosetatonos iotatonosupsilondieresistonosalphabetagammadeltaepsilonzetaetathetaiotakappalambdauni03BCnuxiomicronrhosigma1sigmatauupsilonphichipsiomega iotadieresisupsilondieresis omicrontonos upsilontonos omegatonosuni03D0theta1Upsilon1uni03D3uni03D4phi1omega1uni03D7uni03D8uni03D9uni03DAuni03DBuni03DCuni03DDuni03DEuni03DFuni03E0uni03E1uni03F0uni03F1uni03F2uni03F3uni03F4uni03F5uni03F6uni03F7uni03F8uni03F9uni03FAuni03FBuni03FCuni03FDuni03FEuni03FFuni0400uni0401uni0402uni0403uni0404uni0405uni0406uni0407uni0408uni0409uni040Auni040Buni040Cuni040Duni040Euni040Funi0410uni0411uni0412uni0413uni0414uni0415uni0416uni0417uni0418uni0419uni041Auni041Buni041Cuni041Duni041Euni041Funi0420uni0421uni0422uni0423uni0424uni0425uni0426uni0427uni0428uni0429uni042Auni042Buni042Cuni042Duni042Euni042Funi0430uni0431uni0432uni0433uni0434uni0435uni0436uni0437uni0438uni0439uni043Auni043Buni043Cuni043Duni043Euni043Funi0440uni0441uni0442uni0443uni0444uni0445uni0446uni0447uni0448uni0449uni044Auni044Buni044Cuni044Duni044Euni044Funi0450uni0451uni0452uni0453uni0454uni0455uni0456uni0457uni0458uni0459uni045Auni045Buni045Cuni045Duni045Euni045Funi0472uni0473uni0490uni0491uni0492uni0493uni0494uni0495uni0496uni0497uni0498uni0499uni049Auni049Buni04A2uni04A3uni04AAuni04ABuni04ACuni04ADuni04AEuni04AFuni04B0uni04B1uni04B2uni04B3uni04BAuni04BBuni04C0uni04C1uni04C2uni04C3uni04C4uni04C7uni04C8uni04CBuni04CCuni04CFuni04D0uni04D1uni04D2uni04D3uni04D4uni04D5uni04D6uni04D7uni04D8uni04D9uni04DAuni04DBuni04DCuni04DDuni04DEuni04DFuni04E0uni04E1uni04E2uni04E3uni04E4uni04E5uni04E6uni04E7uni04E8uni04E9uni04EAuni04EBuni04ECuni04EDuni04EEuni04EFuni04F0uni04F1uni04F2uni04F3uni04F4uni04F5uni04F6uni04F7uni04F8uni04F9uni0510uni0511uni051Auni051Buni051Cuni051Duni0606uni0607uni0609uni060Auni060Cuni0615uni061Buni061Funi0621uni0622uni0623uni0624uni0625uni0626uni0627uni0628uni0629uni062Auni062Buni062Cuni062Duni062Euni062Funi0630uni0631uni0632uni0633uni0634uni0635uni0636uni0637uni0638uni0639uni063Auni0640uni0641uni0642uni0643uni0644uni0645uni0646uni0647uni0648uni0649uni064Auni064Buni064Cuni064Duni064Euni064Funi0650uni0651uni0652uni0653uni0654uni0655uni065Auni0660uni0661uni0662uni0663uni0664uni0665uni0666uni0667uni0668uni0669uni066Auni066Buni066Cuni066Duni0674uni0679uni067Auni067Buni067Euni067Funi0680uni0683uni0684uni0686uni0687uni0691uni0698uni06A4uni06A9uni06AFuni06BEuni06CCuni06F0uni06F1uni06F2uni06F3uni06F4uni06F5uni06F6uni06F7uni06F8uni06F9uni0E81uni0E82uni0E84uni0E87uni0E88uni0E8Auni0E8Duni0E94uni0E95uni0E96uni0E97uni0E99uni0E9Auni0E9Buni0E9Cuni0E9Duni0E9Euni0E9Funi0EA1uni0EA2uni0EA3uni0EA5uni0EA7uni0EAAuni0EABuni0EADuni0EAEuni0EAFuni0EB0uni0EB1uni0EB2uni0EB3uni0EB4uni0EB5uni0EB6uni0EB7uni0EB8uni0EB9uni0EBBuni0EBCuni0EC8uni0EC9uni0ECAuni0ECBuni0ECCuni0ECDuni10D0uni10D1uni10D2uni10D3uni10D4uni10D5uni10D6uni10D7uni10D8uni10D9uni10DAuni10DBuni10DCuni10DDuni10DEuni10DFuni10E0uni10E1uni10E2uni10E3uni10E4uni10E5uni10E6uni10E7uni10E8uni10E9uni10EAuni10EBuni10ECuni10EDuni10EEuni10EFuni10F0uni10F1uni10F2uni10F3uni10F4uni10F5uni10F6uni10F7uni10F8uni10F9uni10FAuni10FBuni10FCuni1D02uni1D08uni1D09uni1D14uni1D16uni1D17uni1D1Duni1D1Euni1D1Funi1D2Cuni1D2Duni1D2Euni1D30uni1D31uni1D32uni1D33uni1D34uni1D35uni1D36uni1D37uni1D38uni1D39uni1D3Auni1D3Buni1D3Cuni1D3Euni1D3Funi1D40uni1D41uni1D42uni1D43uni1D44uni1D45uni1D46uni1D47uni1D48uni1D49uni1D4Auni1D4Buni1D4Cuni1D4Duni1D4Euni1D4Funi1D50uni1D51uni1D52uni1D53uni1D54uni1D55uni1D56uni1D57uni1D58uni1D59uni1D5Auni1D5Buni1D62uni1D63uni1D64uni1D65uni1D77uni1D78uni1D7Buni1D85uni1D9Buni1D9Cuni1D9Duni1D9Euni1D9Funi1DA0uni1DA1uni1DA2uni1DA3uni1DA4uni1DA5uni1DA6uni1DA7uni1DA8uni1DA9uni1DAAuni1DABuni1DACuni1DADuni1DAEuni1DAFuni1DB0uni1DB1uni1DB2uni1DB3uni1DB4uni1DB5uni1DB6uni1DB7uni1DB9uni1DBAuni1DBBuni1DBCuni1DBDuni1DBEuni1DBFuni1E00uni1E01uni1E02uni1E03uni1E04uni1E05uni1E06uni1E07uni1E08uni1E09uni1E0Auni1E0Buni1E0Cuni1E0Duni1E0Euni1E0Funi1E10uni1E11uni1E12uni1E13uni1E18uni1E19uni1E1Auni1E1Buni1E1Cuni1E1Duni1E1Euni1E1Funi1E20uni1E21uni1E22uni1E23uni1E24uni1E25uni1E26uni1E27uni1E28uni1E29uni1E2Auni1E2Buni1E2Cuni1E2Duni1E30uni1E31uni1E32uni1E33uni1E34uni1E35uni1E36uni1E37uni1E38uni1E39uni1E3Auni1E3Buni1E3Cuni1E3Duni1E3Euni1E3Funi1E40uni1E41uni1E42uni1E43uni1E44uni1E45uni1E46uni1E47uni1E48uni1E49uni1E4Auni1E4Buni1E54uni1E55uni1E56uni1E57uni1E58uni1E59uni1E5Auni1E5Buni1E5Cuni1E5Duni1E5Euni1E5Funi1E60uni1E61uni1E62uni1E63uni1E68uni1E69uni1E6Auni1E6Buni1E6Cuni1E6Duni1E6Euni1E6Funi1E70uni1E71uni1E72uni1E73uni1E74uni1E75uni1E76uni1E77uni1E7Cuni1E7Duni1E7Euni1E7FWgravewgraveWacutewacute Wdieresis wdieresisuni1E86uni1E87uni1E88uni1E89uni1E8Auni1E8Buni1E8Cuni1E8Duni1E8Euni1E8Funi1E90uni1E91uni1E92uni1E93uni1E94uni1E95uni1E96uni1E97uni1E98uni1E99uni1E9Buni1E9Funi1EA0uni1EA1uni1EACuni1EADuni1EB0uni1EB1uni1EB6uni1EB7uni1EB8uni1EB9uni1EBCuni1EBDuni1EC6uni1EC7uni1ECAuni1ECBuni1ECCuni1ECDuni1ED8uni1ED9uni1EDAuni1EDBuni1EDCuni1EDDuni1EE0uni1EE1uni1EE2uni1EE3uni1EE4uni1EE5uni1EE8uni1EE9uni1EEAuni1EEBuni1EEEuni1EEFuni1EF0uni1EF1Ygraveygraveuni1EF4uni1EF5uni1EF8uni1EF9uni1F00uni1F01uni1F02uni1F03uni1F04uni1F05uni1F06uni1F07uni1F08uni1F09uni1F0Auni1F0Buni1F0Cuni1F0Duni1F0Euni1F0Funi1F10uni1F11uni1F12uni1F13uni1F14uni1F15uni1F18uni1F19uni1F1Auni1F1Buni1F1Cuni1F1Duni1F20uni1F21uni1F22uni1F23uni1F24uni1F25uni1F26uni1F27uni1F28uni1F29uni1F2Auni1F2Buni1F2Cuni1F2Duni1F2Euni1F2Funi1F30uni1F31uni1F32uni1F33uni1F34uni1F35uni1F36uni1F37uni1F38uni1F39uni1F3Auni1F3Buni1F3Cuni1F3Duni1F3Euni1F3Funi1F40uni1F41uni1F42uni1F43uni1F44uni1F45uni1F48uni1F49uni1F4Auni1F4Buni1F4Cuni1F4Duni1F50uni1F51uni1F52uni1F53uni1F54uni1F55uni1F56uni1F57uni1F59uni1F5Buni1F5Duni1F5Funi1F60uni1F61uni1F62uni1F63uni1F64uni1F65uni1F66uni1F67uni1F68uni1F69uni1F6Auni1F6Buni1F6Cuni1F6Duni1F6Euni1F6Funi1F70uni1F71uni1F72uni1F73uni1F74uni1F75uni1F76uni1F77uni1F78uni1F79uni1F7Auni1F7Buni1F7Cuni1F7Duni1F80uni1F81uni1F82uni1F83uni1F84uni1F85uni1F86uni1F87uni1F88uni1F89uni1F8Auni1F8Buni1F8Cuni1F8Duni1F8Euni1F8Funi1F90uni1F91uni1F92uni1F93uni1F94uni1F95uni1F96uni1F97uni1F98uni1F99uni1F9Auni1F9Buni1F9Cuni1F9Duni1F9Euni1F9Funi1FA0uni1FA1uni1FA2uni1FA3uni1FA4uni1FA5uni1FA6uni1FA7uni1FA8uni1FA9uni1FAAuni1FABuni1FACuni1FADuni1FAEuni1FAFuni1FB0uni1FB1uni1FB2uni1FB3uni1FB4uni1FB6uni1FB7uni1FB8uni1FB9uni1FBAuni1FBBuni1FBCuni1FBDuni1FBEuni1FBFuni1FC0uni1FC1uni1FC2uni1FC3uni1FC4uni1FC6uni1FC7uni1FC8uni1FC9uni1FCAuni1FCBuni1FCCuni1FCDuni1FCEuni1FCFuni1FD0uni1FD1uni1FD2uni1FD3uni1FD6uni1FD7uni1FD8uni1FD9uni1FDAuni1FDBuni1FDDuni1FDEuni1FDFuni1FE0uni1FE1uni1FE2uni1FE3uni1FE4uni1FE5uni1FE6uni1FE7uni1FE8uni1FE9uni1FEAuni1FEBuni1FECuni1FEDuni1FEEuni1FEFuni1FF2uni1FF3uni1FF4uni1FF6uni1FF7uni1FF8uni1FF9uni1FFAuni1FFBuni1FFCuni1FFDuni1FFEuni2000uni2001uni2002uni2003uni2004uni2005uni2006uni2007uni2008uni2009uni200Auni2010uni2011 figuredashuni2015 underscoredbl quotereverseduni201Funi2023uni202Funi2031minuteseconduni2034uni2035uni2036uni2037 exclamdbluni203Duni203Euni2045uni2046uni2047uni2048uni2049uni205Funi2070uni2071uni2074uni2075uni2076uni2077uni2078uni2079uni207Auni207Buni207Cuni207Duni207Euni207Funi2080uni2081uni2082uni2083uni2084uni2085uni2086uni2087uni2088uni2089uni208Auni208Buni208Cuni208Duni208Euni2090uni2091uni2092uni2093uni2094uni20A0 colonmonetaryuni20A2lirauni20A5uni20A6pesetauni20A8uni20A9uni20AAdongEurouni20ADuni20AEuni20AFuni20B0uni20B1uni20B2uni20B3uni20B4uni20B5uni2102uni2105uni210Duni210Euni210Funi2115uni2116uni2117uni2119uni211Auni211Duni2124uni2126uni212Auni212B estimatedonethird twothirdsuni2155uni2156uni2157uni2158uni2159uni215A oneeighth threeeighths fiveeighths seveneighthsuni215F arrowleftarrowup arrowright arrowdown arrowboth arrowupdnuni2196uni2197uni2198uni2199uni219Auni219Buni219Cuni219Duni219Euni219Funi21A0uni21A1uni21A2uni21A3uni21A4uni21A5uni21A6uni21A7 arrowupdnbseuni21A9uni21AAuni21ABuni21ACuni21ADuni21AEuni21AFuni21B0uni21B1uni21B2uni21B3uni21B4carriagereturnuni21B6uni21B7uni21B8uni21B9uni21BAuni21BBuni21BCuni21BDuni21BEuni21BFuni21C0uni21C1uni21C2uni21C3uni21C4uni21C5uni21C6uni21C7uni21C8uni21C9uni21CAuni21CBuni21CCuni21CDuni21CEuni21CF arrowdblleft arrowdblup arrowdblright arrowdbldown arrowdblbothuni21D5uni21D6uni21D7uni21D8uni21D9uni21DAuni21DBuni21DCuni21DDuni21DEuni21DFuni21E0uni21E1uni21E2uni21E3uni21E4uni21E5uni21E6uni21E7uni21E8uni21E9uni21EAuni21EBuni21ECuni21EDuni21EEuni21EFuni21F0uni21F1uni21F2uni21F3uni21F4uni21F5uni21F6uni21F7uni21F8uni21F9uni21FAuni21FBuni21FCuni21FDuni21FEuni21FF universaluni2201 existentialuni2204emptysetgradientelement notelementuni220Asuchthatuni220Cuni220Duni2213uni2215 asteriskmathuni2218uni2219uni221Buni221C proportional orthogonalangle logicaland logicalor intersectionunionuni222Cuni222Duni2238uni2239uni223Auni223Bsimilaruni223Duni2241uni2242uni2243uni2244 congruentuni2246uni2247uni2249uni224Auni224Buni224Cuni224Duni224Euni224Funi2250uni2251uni2252uni2253uni2254uni2255uni2256uni2257uni2258uni2259uni225Auni225Buni225Cuni225Duni225Euni225F equivalenceuni2262uni2263uni2266uni2267uni2268uni2269uni226Duni226Euni226Funi2270uni2271uni2272uni2273uni2274uni2275uni2276uni2277uni2278uni2279uni227Auni227Buni227Cuni227Duni227Euni227Funi2280uni2281 propersubsetpropersuperset notsubsetuni2285 reflexsubsetreflexsupersetuni2288uni2289uni228Auni228Buni228Funi2290uni2291uni2292 circleplusuni2296circlemultiplyuni2298uni2299uni229Auni229Buni229Cuni229Duni229Euni229Funi22A0uni22A1dotmathuni22C6uni22CDuni22DAuni22DBuni22DCuni22DDuni22DEuni22DFuni22E0uni22E1uni22E2uni22E3uni22E4uni22E5uni22E6uni22E7uni22E8uni22E9uni22EFuni2300uni2301houseuni2303uni2304uni2305uni2306uni2308uni2309uni230Auni230Buni230Cuni230Duni230Euni230F revlogicalnotuni2311uni2312uni2313uni2314uni2315uni2318uni2319uni231Cuni231Duni231Euni231F integraltp integralbtuni2325uni2326uni2327uni2328uni232Buni2335uni2337uni2338uni2339uni233Auni233Buni233Cuni233Duni233Euni2341uni2342uni2343uni2344uni2347uni2348uni2349uni234Buni234Cuni234Duni2350uni2352uni2353uni2354uni2357uni2358uni2359uni235Auni235Buni235Cuni235Euni235Funi2360uni2363uni2364uni2365uni2368uni2369uni236Buni236Cuni236Duni236Euni236Funi2370uni2373uni2374uni2375uni2376uni2377uni2378uni2379uni237Auni237Duni2380uni2381uni2382uni2383uni2388uni2389uni238Auni238Buni2395uni239Buni239Cuni239Duni239Euni239Funi23A0uni23A1uni23A2uni23A3uni23A4uni23A5uni23A6uni23A7uni23A8uni23A9uni23AAuni23ABuni23ACuni23ADuni23AEuni23CEuni23CFuni2423SF100000uni2501SF110000uni2503uni2504uni2505uni2506uni2507uni2508uni2509uni250Auni250BSF010000uni250Duni250Euni250FSF030000uni2511uni2512uni2513SF020000uni2515uni2516uni2517SF040000uni2519uni251Auni251BSF080000uni251Duni251Euni251Funi2520uni2521uni2522uni2523SF090000uni2525uni2526uni2527uni2528uni2529uni252Auni252BSF060000uni252Duni252Euni252Funi2530uni2531uni2532uni2533SF070000uni2535uni2536uni2537uni2538uni2539uni253Auni253BSF050000uni253Duni253Euni253Funi2540uni2541uni2542uni2543uni2544uni2545uni2546uni2547uni2548uni2549uni254Auni254Buni254Cuni254Duni254Euni254FSF430000SF240000SF510000SF520000SF390000SF220000SF210000SF250000SF500000SF490000SF380000SF280000SF270000SF260000SF360000SF370000SF420000SF190000SF200000SF230000SF470000SF480000SF410000SF450000SF460000SF400000SF540000SF530000SF440000uni256Duni256Euni256Funi2570uni2571uni2572uni2573uni2574uni2575uni2576uni2577uni2578uni2579uni257Auni257Buni257Cuni257Duni257Euni257Fupblockuni2581uni2582uni2583dnblockuni2585uni2586uni2587blockuni2589uni258Auni258Blfblockuni258Duni258Euni258Frtblockltshadeshadedkshadeuni2594uni2595uni2596uni2597uni2598uni2599uni259Auni259Buni259Cuni259Duni259Euni259F filledboxH22073uni25A2uni25A3uni25A4uni25A5uni25A6uni25A7uni25A8uni25A9H18543H18551 filledrectuni25ADuni25AEuni25AFuni25B0uni25B1triagupuni25B3uni25B4uni25B5uni25B6uni25B7uni25B8uni25B9triagrtuni25BBtriagdnuni25BDuni25BEuni25BFuni25C0uni25C1uni25C2uni25C3triaglfuni25C5uni25C6uni25C7uni25C8uni25C9circleuni25CCuni25CDuni25CEH18533uni25D0uni25D1uni25D2uni25D3uni25D4uni25D5uni25D6uni25D7 invbullet invcircleuni25DAuni25DBuni25DCuni25DDuni25DEuni25DFuni25E0uni25E1uni25E2uni25E3uni25E4uni25E5 openbulletuni25E7uni25E8uni25E9uni25EAuni25EBuni25ECuni25EDuni25EEuni25EFuni25F0uni25F1uni25F2uni25F3uni25F4uni25F5uni25F6uni25F7uni25F8uni25F9uni25FAuni25FBuni25FCuni25FDuni25FEuni25FFuni2600uni2601uni2602uni2603uni2604uni2605uni2606uni2607uni2608uni2609uni260Auni260Buni260Cuni260Duni260Euni260Funi2610uni2611uni2612uni2613uni2614uni2615uni2616uni2617uni2618uni2619uni261Auni261Buni261Cuni261Duni261Euni261Funi2620uni2621uni2622uni2623uni2624uni2625uni2626uni2627uni2628uni2629uni262Auni262Buni262Cuni262Duni262Euni262Funi2638uni2639 smileface invsmilefacesununi263Duni263Euni263Ffemaleuni2641maleuni2643uni2644uni2645uni2646uni2647uni2648uni2649uni264Auni264Buni264Cuni264Duni264Euni264Funi2650uni2651uni2652uni2653uni2654uni2655uni2656uni2657uni2658uni2659uni265Auni265Buni265Cuni265Duni265Euni265Fspadeuni2661uni2662clubuni2664heartdiamonduni2667uni2668uni2669 musicalnotemusicalnotedbluni266Cuni266Duni266Euni266Funi2670uni2671uni2672uni2673uni2674uni2675uni2676uni2677uni2678uni2679uni267Auni267Buni267Cuni267Duni267Euni267Funi2680uni2681uni2682uni2683uni2684uni2685uni2686uni2687uni2688uni2689uni268Auni268Buni2690uni2691uni2692uni2693uni2694uni2695uni2696uni2697uni2698uni2699uni269Auni269Buni269Cuni26A0uni26A1uni26B0uni26B1uni2701uni2702uni2703uni2704uni2706uni2707uni2708uni2709uni270Cuni270Duni270Euni270Funi2710uni2711uni2712uni2713uni2714uni2715uni2716uni2717uni2718uni2719uni271Auni271Buni271Cuni271Duni271Euni271Funi2720uni2721uni2722uni2723uni2724uni2725uni2726uni2727uni2729uni272Auni272Buni272Cuni272Duni272Euni272Funi2730uni2731uni2732uni2733uni2734uni2735uni2736uni2737uni2738uni2739uni273Auni273Buni273Cuni273Duni273Euni273Funi2740uni2741uni2742uni2743uni2744uni2745uni2746uni2747uni2748uni2749uni274Auni274Buni274Duni274Funi2750uni2751uni2752uni2756uni2758uni2759uni275Auni275Buni275Cuni275Duni275Euni2761uni2762uni2763uni2764uni2765uni2766uni2767uni2768uni2769uni276Auni276Buni276Cuni276Duni276Euni276Funi2770uni2771uni2772uni2773uni2774uni2775uni2794uni2798uni2799uni279Auni279Buni279Cuni279Duni279Euni279Funi27A0uni27A1uni27A2uni27A3uni27A4uni27A5uni27A6uni27A7uni27A8uni27A9uni27AAuni27ABuni27ACuni27ADuni27AEuni27AFuni27B1uni27B2uni27B3uni27B4uni27B5uni27B6uni27B7uni27B8uni27B9uni27BAuni27BBuni27BCuni27BDuni27BEuni27C5uni27C6uni27E0uni27E8uni27E9uni29EBuni29FAuni29FBuni2A2Funi2B12uni2B13uni2B14uni2B15uni2B16uni2B17uni2B18uni2B19uni2B1Auni2C64uni2C6Euni2C6Funi2C75uni2C76uni2C77uni2C79uni2C7Auni2C7Cuni2C7Duni2E18uni2E22uni2E23uni2E24uni2E25uni2E2EuniA708uniA709uniA70AuniA70BuniA70CuniA70DuniA70EuniA70FuniA710uniA711uniA712uniA713uniA714uniA715uniA716uniA71BuniA71CuniA71DuniA71EuniA71FuniA789uniA78AuniA78BuniA78CuniF6C5uniFB52uniFB53uniFB54uniFB55uniFB56uniFB57uniFB58uniFB59uniFB5AuniFB5BuniFB5CuniFB5DuniFB5EuniFB5FuniFB60uniFB61uniFB62uniFB63uniFB64uniFB65uniFB66uniFB67uniFB68uniFB69uniFB6AuniFB6BuniFB6CuniFB6DuniFB6EuniFB6FuniFB70uniFB71uniFB72uniFB73uniFB74uniFB75uniFB76uniFB77uniFB78uniFB79uniFB7AuniFB7BuniFB7CuniFB7DuniFB7EuniFB7FuniFB80uniFB81uniFB8AuniFB8BuniFB8CuniFB8DuniFB8EuniFB8FuniFB90uniFB91uniFB92uniFB93uniFB94uniFB95uniFB9EuniFB9FuniFBAAuniFBABuniFBACuniFBADuniFBE8uniFBE9uniFBFCuniFBFDuniFBFEuniFBFFuniFE70uniFE71uniFE72uniFE73uniFE74uniFE76uniFE77uniFE78uniFE79uniFE7AuniFE7BuniFE7CuniFE7DuniFE7EuniFE7FuniFE80uniFE81uniFE82uniFE83uniFE84uniFE85uniFE86uniFE87uniFE88uniFE89uniFE8AuniFE8BuniFE8CuniFE8DuniFE8EuniFE8FuniFE90uniFE91uniFE92uniFE93uniFE94uniFE95uniFE96uniFE97uniFE98uniFE99uniFE9AuniFE9BuniFE9CuniFE9DuniFE9EuniFE9FuniFEA0uniFEA1uniFEA2uniFEA3uniFEA4uniFEA5uniFEA6uniFEA7uniFEA8uniFEA9uniFEAAuniFEABuniFEACuniFEADuniFEAEuniFEAFuniFEB0uniFEB1uniFEB2uniFEB3uniFEB4uniFEB5uniFEB6uniFEB7uniFEB8uniFEB9uniFEBAuniFEBBuniFEBCuniFEBDuniFEBEuniFEBFuniFEC0uniFEC1uniFEC2uniFEC3uniFEC4uniFEC5uniFEC6uniFEC7uniFEC8uniFEC9uniFECAuniFECBuniFECCuniFECDuniFECEuniFECFuniFED0uniFED1uniFED2uniFED3uniFED4uniFED5uniFED6uniFED7uniFED8uniFED9uniFEDAuniFEDBuniFEDCuniFEDDuniFEDEuniFEDFuniFEE0uniFEE1uniFEE2uniFEE3uniFEE4uniFEE5uniFEE6uniFEE7uniFEE8uniFEE9uniFEEAuniFEEBuniFEECuniFEEDuniFEEEuniFEEFuniFEF0uniFEF1uniFEF2uniFEF3uniFEF4uniFEF5uniFEF6uniFEF7uniFEF8uniFEF9uniFEFAuniFEFBuniFEFCuniFEFFuniFFF9uniFFFAuniFFFBuniFFFCuniFFFDu1D670u1D671u1D672u1D673u1D674u1D675u1D676u1D677u1D678u1D679u1D67Au1D67Bu1D67Cu1D67Du1D67Eu1D67Fu1D680u1D681u1D682u1D683u1D684u1D685u1D686u1D687u1D688u1D689u1D68Au1D68Bu1D68Cu1D68Du1D68Eu1D68Fu1D690u1D691u1D692u1D693u1D694u1D695u1D696u1D697u1D698u1D699u1D69Au1D69Bu1D69Cu1D69Du1D69Eu1D69Fu1D6A0u1D6A1u1D6A2u1D6A3u1D7F6u1D7F7u1D7F8u1D7F9u1D7FAu1D7FBu1D7FCu1D7FDu1D7FEu1D7FF dlLtcaron DiaeresisAcuteTildeGrave CircumflexCaron fractionslash uni0311.case uni0306.case uni0307.case uni030B.case uni030F.case thinquestion uni0304.caseunderbar underbar.wideunderbar.smalljotdiaeresis.symbols arabic_dot arabic_2dots arabic_3dots uni066E.fina uni06A1.init uni06A1.medi uni066F.fina uni06A1.finaarabic_3dots_aarabic_2dots_a arabic_4dotsarabic_gaf_bararabic_gaf_bar_a arabic_ringEng.altuni066Euni066Funi067Cuni067Duni0681uni0682uni0685uni0692uni06A1uni06B5uni06BAuni06C6uni06CEuni06D5]A GA% } % 2  %%@Y}2}Y&Y@&//2G@Gddkߖږ؍ }:Ս :  ϊ̖ˋ%}Ś   ]%]@%AA dd@2(-}-d   ..A]%]@%%%A  %d%BSx~}~}}|{zwvut uu@t tss@rqponSonm(nSm(lk2ji2hgfedcbcbba`_^Z ^]d\[Z [Z YXWVUU2TSRQ}PONM-MLK(JIJ7ICIHEHGCGdFEFEDCD7CBCC@@ BABB@ A@AA@ @? @@@ ? ? ?@@d>=-=<;(:9B9d818K76-65K404K3032B21-10/-/. .-,--@ ,,,@@+*%+* *%):)('&%B%E$#""! -!} -KBBF-B-B-B@  @   @    @  @7    -:-:-d++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++mapproxy-1.11.0/mapproxy/image/fonts/LICENSE000066400000000000000000000113201320454472400205520ustar00rootroot00000000000000Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) Bitstream Vera Fonts Copyright ------------------------------ Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is a trademark of Bitstream, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces. The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Bitstream" or the word "Vera". This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Bitstream Vera" names. The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. Except as contained in this notice, the names of Gnome, the Gnome Foundation, and Bitstream Inc., shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from the Gnome Foundation or Bitstream Inc., respectively. For further information, contact: fonts at gnome dot org. Arev Fonts Copyright ------------------------------ Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the modifications to the Bitstream Vera Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces. The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Tavmjong Bah" or the word "Arev". This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Tavmjong Bah Arev" names. The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. Except as contained in this notice, the name of Tavmjong Bah shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from Tavmjong Bah. For further information, contact: tavmjong @ free . fr. $Id: LICENSE 2133 2007-11-28 02:46:28Z lechimp $ mapproxy-1.11.0/mapproxy/image/fonts/__init__.py000066400000000000000000000000001320454472400216470ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/image/mask.py000066400000000000000000000053121320454472400177250ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2012 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.compat.image import Image, ImageDraw from mapproxy.srs import SRS, make_lin_transf from mapproxy.image import ImageSource from mapproxy.image.opts import create_image from mapproxy.util.geom import flatten_to_polygons def mask_image_source_from_coverage(img_source, bbox, bbox_srs, coverage, image_opts=None): if image_opts is None: image_opts = img_source.image_opts img = img_source.as_image() img = mask_image(img, bbox, bbox_srs, coverage) result = create_image(img.size, image_opts) result.paste(img, (0, 0), img) return ImageSource(result, image_opts=image_opts) def mask_image(img, bbox, bbox_srs, coverage): geom = mask_polygons(bbox, SRS(bbox_srs), coverage) mask = image_mask_from_geom(img.size, bbox, geom) img = img.convert('RGBA') img.paste((255, 255, 255, 0), (0, 0), mask) return img def mask_polygons(bbox, bbox_srs, coverage): coverage = coverage.transform_to(bbox_srs) coverage = coverage.intersection(bbox, bbox_srs) return flatten_to_polygons(coverage.geom) def image_mask_from_geom(size, bbox, polygons): mask = Image.new('L', size, 255) if len(polygons) == 0: return mask transf = make_lin_transf(bbox, (0, 0) + size) # use negative ~.1 pixel buffer buffer = -0.1 * min((bbox[2] - bbox[0]) / size[0], (bbox[3] - bbox[1]) / size[1]) draw = ImageDraw.Draw(mask) def draw_polygon(p): draw.polygon([transf(coord) for coord in p.exterior.coords], fill=0) for ring in p.interiors: draw.polygon([transf(coord) for coord in ring.coords], fill=255) for p in polygons: # little bit smaller polygon does not include touched pixels outside coverage buffered = p.buffer(buffer, resolution=1, join_style=2) if buffered.is_empty: # can be empty after negative buffer continue if buffered.type == 'MultiPolygon': # negative buffer can turn polygon into multipolygon for p in buffered: draw_polygon(p) else: draw_polygon(buffered) return mask mapproxy-1.11.0/mapproxy/image/merge.py000066400000000000000000000260071320454472400200750ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010-2016 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Image and tile manipulation (transforming, merging, etc). """ from collections import namedtuple from mapproxy.compat.image import Image, ImageColor, ImageChops, ImageMath from mapproxy.compat.image import has_alpha_composite_support from mapproxy.image import BlankImageSource, ImageSource from mapproxy.image.opts import create_image, ImageOptions from mapproxy.image.mask import mask_image import logging log = logging.getLogger('mapproxy.image') class LayerMerger(object): """ Merge multiple layers into one image. """ def __init__(self): self.layers = [] self.cacheable = True def add(self, img, coverage=None): """ Add one layer image to merge. Bottom-layers first. """ if img is not None: self.layers.append((img, coverage)) class LayerMerger(LayerMerger): def merge(self, image_opts, size=None, bbox=None, bbox_srs=None, coverage=None): """ Merge the layers. If the format is not 'png' just return the last image. :param format: The image format for the result. :param size: The size for the merged output. :rtype: `ImageSource` """ if not self.layers: return BlankImageSource(size=size, image_opts=image_opts, cacheable=True) if len(self.layers) == 1: layer_img, layer_coverage = self.layers[0] layer_opts = layer_img.image_opts if (((layer_opts and not layer_opts.transparent) or image_opts.transparent) and (not size or size == layer_img.size) and (not layer_coverage or not layer_coverage.clip) and not coverage): # layer is opaque, no need to make transparent or add bgcolor return layer_img if size is None: size = self.layers[0][0].size cacheable = self.cacheable result = create_image(size, image_opts) for layer_img, layer_coverage in self.layers: if not layer_img.cacheable: cacheable = False img = layer_img.as_image() layer_image_opts = layer_img.image_opts if layer_image_opts is None: opacity = None else: opacity = layer_image_opts.opacity if layer_coverage and layer_coverage.clip: img = mask_image(img, bbox, bbox_srs, layer_coverage) if result.mode != 'RGBA': merge_composite = False else: merge_composite = has_alpha_composite_support() if 'transparency' in img.info: # non-paletted PNGs can have a fixed transparency value # convert to RGBA to have full alpha img = img.convert('RGBA') if merge_composite: if opacity is not None and opacity < 1.0: # fade-out img to add opacity value img = img.convert("RGBA") alpha = img.split()[3] alpha = ImageChops.multiply( alpha, ImageChops.constant(alpha, int(255 * opacity)) ) img.putalpha(alpha) if img.mode in ('RGBA', 'P'): # assume paletted images have transparency if img.mode == 'P': img = img.convert('RGBA') result = Image.alpha_composite(result, img) else: result.paste(img, (0, 0)) else: if opacity is not None and opacity < 1.0: img = img.convert(result.mode) result = Image.blend(result, img, layer_image_opts.opacity) elif img.mode in ('RGBA', 'P'): # assume paletted images have transparency if img.mode == 'P': img = img.convert('RGBA') # paste w transparency mask from layer result.paste(img, (0, 0), img) else: result.paste(img, (0, 0)) # apply global clip coverage if coverage: bg = create_image(size, image_opts) mask = mask_image(result, bbox, bbox_srs, coverage) bg.paste(result, (0, 0), mask) result = bg return ImageSource(result, size=size, image_opts=image_opts, cacheable=cacheable) band_ops = namedtuple("band_ops", ["dst_band", "src_img", "src_band", "factor"]) class BandMerger(object): """ Merge bands from multiple sources into one image. sources: r: [{source: nir_cache, band: 0, factor: 0.4}, {source: dop_cache, band: 0, factor: 0.6}] g: [{source: dop_cache, band: 2}] b: [{source: dop_cache, band: 1}] sources: l: [ {source: dop_cache, band: 0, factor: 0.6}, {source: dop_cache, band: 1, factor: 0.3}, {source: dop_cache, band: 2, factor: 0.1}, ] """ def __init__(self, mode=None): self.ops = [] self.cacheable = True self.mode = mode self.max_band = {} self.max_src_images = 0 def add_ops(self, dst_band, src_img, src_band, factor=1.0): self.ops.append(band_ops( dst_band=dst_band, src_img=src_img, src_band=src_band, factor=factor, )) # store highest requested band index for each source self.max_band[src_img] = max(self.max_band.get(src_img, 0), src_band) self.max_src_images = max(src_img+1, self.max_src_images) def merge(self, sources, image_opts, size=None, bbox=None, bbox_srs=None, coverage=None): if len(sources) < self.max_src_images: return BlankImageSource(size=size, image_opts=image_opts, cacheable=True) if size is None: size = sources[0].size # load src bands src_img_bands = [] for i, layer_img in enumerate(sources): img = layer_img.as_image() if i not in self.max_band: # do not split img if not requested by any op src_img_bands.append(None) continue if self.max_band[i] == 3 and img.mode != 'RGBA': # convert to RGBA if band idx 3 is requestd (e.g. P or RGB src) img = img.convert('RGBA') elif img.mode == 'P': img = img.convert('RGB') src_img_bands.append(img.split()) tmp_mode = self.mode if tmp_mode == 'RGBA': result_bands = [None, None, None, None] elif tmp_mode == 'RGB': result_bands = [None, None, None] elif tmp_mode == 'L': result_bands = [None] else: raise ValueError("unsupported destination mode %s", image_opts.mode) for op in self.ops: chan = src_img_bands[op.src_img][op.src_band] if op.factor != 1.0: chan = ImageMath.eval("convert(int(float(a) * %f), 'L')" % op.factor, a=chan) if result_bands[op.dst_band] is None: result_bands[op.dst_band] = chan else: result_bands[op.dst_band] = ImageChops.add( result_bands[op.dst_band], chan, ) else: result_bands[op.dst_band] = chan for i, b in enumerate(result_bands): if b is None: # band not set b = Image.new("L", size, 255 if i == 3 else 0) result_bands[i] = b result = Image.merge(tmp_mode, result_bands) return ImageSource(result, size=size, image_opts=image_opts) def merge_images(layers, image_opts, size=None, bbox=None, bbox_srs=None, merger=None): """ Merge multiple images into one. :param images: list of `ImageSource`, bottom image first :param format: the format of the output `ImageSource` :param size: size of the merged image, if ``None`` the size of the first image is used :param bbox: Bounding box :param bbox_srs: Bounding box SRS :param merger: Image merger :rtype: `ImageSource` """ if merger is None: merger = LayerMerger() # BandMerger does not have coverage support, passing only images if isinstance(merger, BandMerger): sources = [l[0] if isinstance(l, tuple) else l for l in layers] return merger.merge(sources, image_opts=image_opts, size=size, bbox=bbox, bbox_srs=bbox_srs) for layer in layers: if isinstance(layer, tuple): merger.add(layer[0], layer[1]) else: merger.add(layer) return merger.merge(image_opts=image_opts, size=size, bbox=bbox, bbox_srs=bbox_srs) def concat_legends(legends, format='png', size=None, bgcolor='#ffffff', transparent=True): """ Merge multiple legends into one :param images: list of `ImageSource`, bottom image first :param format: the format of the output `ImageSource` :param size: size of the merged image, if ``None`` the size will be calculated :rtype: `ImageSource` """ if not legends: return BlankImageSource(size=(1,1), image_opts=ImageOptions(bgcolor=bgcolor, transparent=transparent)) if len(legends) == 1: return legends[0] legends = legends[:] legends.reverse() if size is None: legend_width = 0 legend_height = 0 legend_position_y = [] #iterate through all legends, last to first, calc img size and remember the y-position for legend in legends: legend_position_y.append(legend_height) tmp_img = legend.as_image() legend_width = max(legend_width, tmp_img.size[0]) legend_height += tmp_img.size[1] #images shall not overlap themselfs size = [legend_width, legend_height] bgcolor = ImageColor.getrgb(bgcolor) if transparent: img = Image.new('RGBA', size, bgcolor+(0,)) else: img = Image.new('RGB', size, bgcolor) for i in range(len(legends)): legend_img = legends[i].as_image() if legend_img.mode == 'RGBA': # paste w transparency mask from layer img.paste(legend_img, (0, legend_position_y[i]), legend_img) else: img.paste(legend_img, (0, legend_position_y[i])) return ImageSource(img, image_opts=ImageOptions(format=format)) mapproxy-1.11.0/mapproxy/image/message.py000066400000000000000000000276701320454472400204310ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import os import pkg_resources from mapproxy.config import base_config, abspath from mapproxy.compat.image import Image, ImageColor, ImageDraw, ImageFont from mapproxy.image import ImageSource from mapproxy.image.opts import create_image, ImageOptions from mapproxy.compat import string_type _pil_ttf_support = True import logging log_system = logging.getLogger('mapproxy.system') def message_image(message, size, image_opts, bgcolor='#ffffff', transparent=False): """ Creates an image with text (`message`). This can be used to create in_image exceptions. For dark `bgcolor` the font color is white, otherwise black. :param message: the message to put in the image :param size: the size of the output image :param format: the output format of the image :param bgcolor: the background color of the image :param transparent: if True and the `format` supports it, return a transparent image :rtype: `ImageSource` """ eimg = ExceptionImage(message, image_opts=image_opts) return eimg.draw(size=size) def attribution_image(message, size, image_opts=None, inverse=False): """ Creates an image with text attribution (`message`). :param message: the message to put in the image :param size: the size of the output image :param format: the output format of the image :param inverse: if true, write white text :param transparent: if True and the `format` supports it, return a transparent image :rtype: `ImageSource` """ if image_opts is None: image_opts = ImageOptions(transparent=True) aimg = AttributionImage(message, image_opts=image_opts, inverse=inverse) return aimg.draw(size=size) class MessageImage(object): """ Base class for text rendering in images (for watermarks, exception images, etc.) :ivar font_name: the font name for the text :ivar font_size: the font size of the text :ivar font_color: the color of the font as a tuple :ivar box_color: the color of the box behind the text. color as a tuple or ``None`` """ font_name = 'DejaVu Sans Mono' font_size = 10 font_color = ImageColor.getrgb('black') box_color = None linespacing = 5 padding = 3 placement = 'ul' def __init__(self, message, image_opts): self.message = message self.image_opts = image_opts self._font = None @property def font(self): global _pil_ttf_support if self._font is None: if self.font_name != 'default' and _pil_ttf_support: try: self._font = ImageFont.truetype(font_file(self.font_name), self.font_size) except ImportError: _pil_ttf_support = False log_system.warn("Couldn't load TrueType fonts, " "PIL needs to be build with freetype support.") except IOError: _pil_ttf_support = False log_system.warn("Couldn't load find TrueType font ", self.font_name) if self._font is None: self._font = ImageFont.load_default() return self._font def new_image(self, size): return Image.new('RGBA', size) def draw(self, img=None, size=None, in_place=True): """ Create the message image. Either draws on top of `img` or creates a new image with the given `size`. """ if not ((img and not size) or (size and not img)): raise TypeError('need either img or size argument') if img is None: base_img = self.new_image(size) elif not in_place: size = img.size base_img = self.new_image(size) else: base_img = img.as_image() size = base_img.size if not self.message: if img is not None: return img return ImageSource(base_img, size=size, image_opts=self.image_opts) draw = ImageDraw.Draw(base_img) self.draw_msg(draw, size) image_opts = self.image_opts if not in_place and img: image_opts = image_opts or img.image_opts img = img.as_image() converted = False if len(self.font_color) == 4 and img.mode != 'RGBA': # we need RGBA to keep transparency from text converted = img.mode img = img.convert('RGBA') img.paste(base_img, (0, 0), base_img) if converted == 'RGB': # convert image back img = img.convert('RGB') base_img = img return ImageSource(base_img, size=size, image_opts=image_opts) def draw_msg(self, draw, size): td = TextDraw(self.message, font=self.font, bg_color=self.box_color, font_color=self.font_color, placement=self.placement, linespacing=self.linespacing, padding=self.padding) td.draw(draw, size) class ExceptionImage(MessageImage): """ Image for exceptions. """ font_name = 'default' font_size = 9 def __init__(self, message, image_opts): MessageImage.__init__(self, message, image_opts=image_opts.copy()) if not self.image_opts.bgcolor: self.image_opts.bgcolor = '#ffffff' def new_image(self, size): return create_image(size, self.image_opts) @property def font_color(self): if self.image_opts.transparent: return ImageColor.getrgb('black') if _luminance(ImageColor.getrgb(self.image_opts.bgcolor)) < 128: return ImageColor.getrgb('white') return ImageColor.getrgb('black') class WatermarkImage(MessageImage): """ Image with large, faded message. """ font_name = 'DejaVu Sans' font_size = 24 font_color = (128, 128, 128) def __init__(self, message, image_opts, placement='c', opacity=None, font_color=None, font_size=None): MessageImage.__init__(self, message, image_opts=image_opts) if opacity is None: opacity = 30 if font_size: self.font_size = font_size if font_color: self.font_color = font_color self.font_color = self.font_color + tuple([opacity]) self.placement = placement def draw_msg(self, draw, size): td = TextDraw(self.message, self.font, self.font_color) if self.placement in ('l', 'b'): td.placement = 'cL' td.draw(draw, size) if self.placement in ('r', 'b'): td.placement = 'cR' td.draw(draw, size) if self.placement == 'c': td.placement = 'cc' td.draw(draw, size) class AttributionImage(MessageImage): """ Image with attribution information. """ font_name = 'DejaVu Sans' font_size = 10 placement = 'lr' def __init__(self, message, image_opts, inverse=False): MessageImage.__init__(self, message, image_opts=image_opts) self.inverse = inverse @property def font_color(self): if self.inverse: return ImageColor.getrgb('white') else: return ImageColor.getrgb('black') @property def box_color(self): if self.inverse: return (0, 0, 0, 100) else: return (255, 255, 255, 120) class TextDraw(object): def __init__(self, text, font, font_color=None, bg_color=None, placement='ul', padding=5, linespacing=3): if isinstance(text, string_type): text = text.split('\n') self.text = text self.font = font self.bg_color = bg_color self.font_color = font_color self.placement = placement self.padding = (padding, padding, padding, padding) self.linespacing = linespacing def text_boxes(self, draw, size): try: total_bbox, boxes = self._relative_text_boxes(draw) except UnicodeEncodeError: # raised if font does not support unicode self.text = [l.encode('ascii', 'replace') for l in self.text] total_bbox, boxes = self._relative_text_boxes(draw) return self._place_boxes(total_bbox, boxes, size) def draw(self, draw, size): total_bbox, boxes = self.text_boxes(draw, size) if self.bg_color: draw.rectangle( (total_bbox[0]-self.padding[0], total_bbox[1]-self.padding[1], total_bbox[2]+self.padding[2], total_bbox[3]+self.padding[3]), fill=self.bg_color) for text, box in zip(self.text, boxes): draw.text((box[0], box[1]), text, font=self.font, fill=self.font_color) def _relative_text_boxes(self, draw): total_bbox = (1e9, 1e9, -1e9, -1e9) boxes = [] y_offset = 0 for i, line in enumerate(self.text): text_size = draw.textsize(line, font=self.font) text_box = (0, y_offset, text_size[0], text_size[1]+y_offset) boxes.append(text_box) total_bbox = (min(total_bbox[0], text_box[0]), min(total_bbox[1], text_box[1]), max(total_bbox[2], text_box[2]), max(total_bbox[3], text_box[3]), ) y_offset += text_size[1] + self.linespacing return total_bbox, boxes def _move_bboxes(self, boxes, offsets): result = [] for box in boxes: box = box[0]+offsets[0], box[1]+offsets[1], box[2]+offsets[0], box[3]+offsets[1] result.append(tuple(int(x) for x in box)) return result def _place_boxes(self, total_bbox, boxes, size): x_offset = y_offset = None text_size = (total_bbox[2] - total_bbox[0]), (total_bbox[3] - total_bbox[1]) if self.placement[0] == 'u': y_offset = self.padding[1] elif self.placement[0] == 'l': y_offset = size[1] - self.padding[3] - text_size[1] elif self.placement[0] == 'c': y_offset = size[1] // 2 - text_size[1] // 2 if self.placement[1] == 'l': x_offset = self.padding[0] if self.placement[1] == 'L': x_offset = -text_size[0] // 2 elif self.placement[1] == 'r': x_offset = size[0] - self.padding[1] - text_size[0] elif self.placement[1] == 'R': x_offset = size[0] - text_size[0] // 2 elif self.placement[1] == 'c': x_offset = size[0] // 2 - text_size[0] // 2 if x_offset is None or y_offset is None: raise ValueError('placement %r not supported' % self.placement) offsets = x_offset, y_offset return self._move_bboxes([total_bbox], offsets)[0], self._move_bboxes(boxes, offsets) def font_file(font_name): font_dir = base_config().image.font_dir font_name = font_name.replace(' ', '') if font_dir: abspath(font_dir) path = os.path.join(font_dir, font_name + '.ttf') else: path = pkg_resources.resource_filename(__name__, 'fonts/' + font_name + '.ttf') return path def _luminance(color): """ Returns the luminance of a RGB tuple. Uses ITU-R 601-2 luma transform. """ r, g, b = color return r * 299/1000 + g * 587/1000 + b * 114/1000 mapproxy-1.11.0/mapproxy/image/opts.py000066400000000000000000000124441320454472400177630ustar00rootroot00000000000000# -:- encoding: utf-8 -:- # This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import copy from mapproxy.compat import string_type class ImageOptions(object): def __init__(self, mode=None, transparent=None, opacity=None, resampling=None, format=None, bgcolor=None, colors=None, encoding_options=None): self.transparent = transparent self.opacity = opacity self.resampling = resampling if format is not None: format = ImageFormat(format) self.format = format self.mode = mode self.bgcolor = bgcolor self.colors = colors self.encoding_options = encoding_options or {} def __repr__(self): options = [] for k in dir(self): if k.startswith('_'): continue v = getattr(self, k) if v is not None and not hasattr(v, 'im_func') and not hasattr(v, '__func__'): options.append('%s=%r' % (k, v)) return 'ImageOptions(%s)' % (', '.join(options), ) def __eq__(self, other): if not isinstance(other, ImageOptions): return NotImplemented return ( self.transparent == other.transparent and self.opacity == other.opacity and self.resampling == other.resampling and self.format == other.format and self.mode == other.mode and self.bgcolor == other.bgcolor and self.colors == other.colors and self.encoding_options == other.encoding_options ) def copy(self): return copy.copy(self) class ImageFormat(str): def __new__(cls, value, *args, **keywargs): if isinstance(value, ImageFormat): return value return str.__new__(cls, value) @property def mime_type(self): if self.startswith('image/'): return self return 'image/' + self @property def ext(self): ext = self if '/' in ext: ext = ext.split('/', 1)[1] if ';' in ext: ext = ext.split(';', 1)[0] return ext.strip() def __eq__(self, other): if isinstance(other, string_type): other = ImageFormat(other) elif not isinstance(other, ImageFormat): return NotImplemented return self.ext == other.ext def __hash__(self): return hash(str(self)) def __ne__(self, other): return not (self == other) def create_image(size, image_opts=None): """ Create a new image that is compatible with the given `image_opts`. Takes into account mode, transparent, bgcolor. """ from mapproxy.compat.image import Image, ImageColor if image_opts is None: mode = 'RGB' bgcolor = (255, 255, 255) else: mode = image_opts.mode if mode in (None, 'P'): if image_opts.transparent: mode = 'RGBA' else: mode = 'RGB' bgcolor = image_opts.bgcolor or (255, 255, 255) if isinstance(bgcolor, string_type): bgcolor = ImageColor.getrgb(bgcolor) if image_opts.transparent and len(bgcolor) == 3: bgcolor = bgcolor + (0, ) if image_opts.mode == 'I': bgcolor = bgcolor[0] return Image.new(mode, size, bgcolor) class ImageFormats(object): def __init__(self): self.format_options = {} def add(self, opts): assert opts.format is not None self.format_options[opts.format] = opts def options(self, format): opts = self.format_options.get(format) if not opts: opts = ImageOptions(transparent=False, format=format) return opts def compatible_image_options(img_opts, base_opts=None): """ Return ImageOptions that is compatible with all given `img_opts`. """ if any(True for o in img_opts if o.colors == 0): colors = 0 else: colors = max(o.colors or 0 for o in img_opts) transparent = None for o in img_opts: if o.transparent == False: transparent = False break if o.transparent == True: transparent = True if any(True for o in img_opts if o.mode): # I < P < RGB < RGBA :) mode = max(o.mode for o in img_opts if o.mode) else: mode = None if base_opts: options = base_opts.copy() if options.colors is None: options.colors = colors if options.mode is None: options.mode = mode if options.transparent is None: options.transparent = transparent else: options = img_opts[0].copy() options.colors = colors options.transparent = transparent options.mode = mode return optionsmapproxy-1.11.0/mapproxy/image/tile.py000066400000000000000000000133701320454472400177320ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os from mapproxy.image import ImageSource from mapproxy.image.transform import ImageTransformer from mapproxy.image.opts import create_image import logging log = logging.getLogger(__name__) class TileMerger(object): """ Merge multiple tiles into one image. """ def __init__(self, tile_grid, tile_size): """ :param tile_grid: the grid size :type tile_grid: ``(int(x_tiles), int(y_tiles))`` :param tile_size: the size of each tile """ self.tile_grid = tile_grid self.tile_size = tile_size def merge(self, ordered_tiles, image_opts): """ Merge all tiles into one image. :param ordered_tiles: list of tiles, sorted row-wise (top to bottom) :rtype: `ImageSource` """ if self.tile_grid == (1, 1): assert len(ordered_tiles) == 1 if ordered_tiles[0] is not None: tile = ordered_tiles.pop() return tile src_size = self._src_size() result = create_image(src_size, image_opts) cacheable = True for i, source in enumerate(ordered_tiles): if source is None: continue try: if not source.cacheable: cacheable = False tile = source.as_image() pos = self._tile_offset(i) tile.draft(image_opts.mode, self.tile_size) result.paste(tile, pos) source.close_buffers() except IOError as e: if e.errno is None: # PIL error log.warn('unable to load tile %s, removing it (reason was: %s)' % (source, str(e))) if getattr(source, 'filename'): if os.path.exists(source.filename): os.remove(source.filename) else: raise return ImageSource(result, size=src_size, image_opts=image_opts, cacheable=cacheable) def _src_size(self): width = self.tile_grid[0]*self.tile_size[0] height = self.tile_grid[1]*self.tile_size[1] return width, height def _tile_offset(self, i): """ Return the image offset (upper-left coord) of the i-th tile, where the tiles are ordered row-wise, top to bottom. """ return (i%self.tile_grid[0]*self.tile_size[0], i//self.tile_grid[0]*self.tile_size[1]) class TileSplitter(object): """ Splits a large image into multiple tiles. """ def __init__(self, meta_tile, image_opts): self.meta_img = meta_tile.as_image() self.image_opts = image_opts def get_tile(self, crop_coord, tile_size): """ Return the cropped tile. :param crop_coord: the upper left pixel coord to start :param tile_size: width and height of the new tile :rtype: `ImageSource` """ minx, miny = crop_coord maxx = minx + tile_size[0] maxy = miny + tile_size[1] if (minx < 0 or miny < 0 or maxx > self.meta_img.size[0] or maxy > self.meta_img.size[1]): crop = self.meta_img.crop(( max(minx, 0), max(miny, 0), min(maxx, self.meta_img.size[0]), min(maxy, self.meta_img.size[1]))) result = create_image(tile_size, self.image_opts) result.paste(crop, (abs(min(minx, 0)), abs(min(miny, 0)))) crop = result else: crop = self.meta_img.crop((minx, miny, maxx, maxy)) return ImageSource(crop, size=tile_size, image_opts=self.image_opts) class TiledImage(object): """ An image built-up from multiple tiles. """ def __init__(self, tiles, tile_grid, tile_size, src_bbox, src_srs): """ :param tiles: all tiles (sorted row-wise, top to bottom) :param tile_grid: the tile grid size :type tile_grid: ``(int(x_tiles), int(y_tiles))`` :param tile_size: the size of each tile :param src_bbox: the bbox of all tiles :param src_srs: the srs of the bbox :param transparent: if the sources are transparent """ self.tiles = tiles self.tile_grid = tile_grid self.tile_size = tile_size self.src_bbox = src_bbox self.src_srs = src_srs def image(self, image_opts): """ Return the tiles as one merged image. :rtype: `ImageSource` """ tm = TileMerger(self.tile_grid, self.tile_size) return tm.merge(self.tiles, image_opts=image_opts) def transform(self, req_bbox, req_srs, out_size, image_opts): """ Return the the tiles as one merged and transformed image. :param req_bbox: the bbox of the output image :param req_srs: the srs of the req_bbox :param out_size: the size in pixel of the output image :rtype: `ImageSource` """ transformer = ImageTransformer(self.src_srs, req_srs) src_img = self.image(image_opts) return transformer.transform(src_img, self.src_bbox, out_size, req_bbox, image_opts) mapproxy-1.11.0/mapproxy/image/transform.py000066400000000000000000000311651320454472400210120ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division from mapproxy.compat.image import Image, transform_uses_center from mapproxy.image import ImageSource, image_filter from mapproxy.srs import make_lin_transf, bbox_equals class ImageTransformer(object): """ Transform images between different bbox and spatial reference systems. :note: The transformation doesn't make a real transformation for each pixel, but a mesh transformation (see `PIL Image.transform`_). It will divide the target image into rectangles (a mesh). The source coordinates for each rectangle vertex will be calculated. The quadrilateral will then be transformed with the source coordinates into the destination quad (affine). The number of quads is calculated dynamically to keep the deviation in the image transformation below one pixel. .. _PIL Image.transform: http://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.transform :: src quad dst quad .----. <- coord- .----. / / transformation | | / / | | .----. img-transformation -> .----.---- | | | ---------------. large src image large dst image """ def __init__(self, src_srs, dst_srs, max_px_err=1): """ :param src_srs: the srs of the source image :param dst_srs: the srs of the target image :param resampling: the resampling method used for transformation :type resampling: nearest|bilinear|bicubic """ self.src_srs = src_srs self.dst_srs = dst_srs self.dst_bbox = self.dst_size = None self.max_px_err = max_px_err def transform(self, src_img, src_bbox, dst_size, dst_bbox, image_opts): """ Transforms the `src_img` between the source and destination SRS of this ``ImageTransformer`` instance. When the ``src_srs`` and ``dst_srs`` are equal the image will be cropped and not transformed. If the `src_bbox` and `dst_bbox` are equal, the `src_img` itself will be returned. :param src_img: the source image for the transformation :param src_bbox: the bbox of the src_img :param dst_size: the size of the result image (in pizel) :type dst_size: ``(int(width), int(height))`` :param dst_bbox: the bbox of the result image :return: the transformed image :rtype: `ImageSource` """ if self._no_transformation_needed(src_img.size, src_bbox, dst_size, dst_bbox): return src_img if self.src_srs == self.dst_srs: result = self._transform_simple(src_img, src_bbox, dst_size, dst_bbox, image_opts) else: result = self._transform(src_img, src_bbox, dst_size, dst_bbox, image_opts) result.cacheable = src_img.cacheable return result def _transform_simple(self, src_img, src_bbox, dst_size, dst_bbox, image_opts): """ Do a simple crop/extent transformation. """ src_quad = (0, 0, src_img.size[0], src_img.size[1]) to_src_px = make_lin_transf(src_bbox, src_quad) minx, miny = to_src_px((dst_bbox[0], dst_bbox[3])) maxx, maxy = to_src_px((dst_bbox[2], dst_bbox[1])) src_res = ((src_bbox[0]-src_bbox[2])/src_img.size[0], (src_bbox[1]-src_bbox[3])/src_img.size[1]) dst_res = ((dst_bbox[0]-dst_bbox[2])/dst_size[0], (dst_bbox[1]-dst_bbox[3])/dst_size[1]) tenth_px_res = (abs(dst_res[0]/(dst_size[0]*10)), abs(dst_res[1]/(dst_size[1]*10))) if (abs(src_res[0]-dst_res[0]) < tenth_px_res[0] and abs(src_res[1]-dst_res[1]) < tenth_px_res[1]): # rounding might result in subpixel inaccuracy # this exact resolutioni match should only happen in clients with # fixed resolutions like OpenLayers minx = int(round(minx)) miny = int(round(miny)) result = src_img.as_image().crop((minx, miny, minx+dst_size[0], miny+dst_size[1])) else: img = img_for_resampling(src_img.as_image(), image_opts.resampling) result = img.transform(dst_size, Image.EXTENT, (minx, miny, maxx, maxy), image_filter[image_opts.resampling]) return ImageSource(result, size=dst_size, image_opts=image_opts) def _transform(self, src_img, src_bbox, dst_size, dst_bbox, image_opts): """ Do a 'real' transformation with a transformed mesh (see above). """ # more recent versions of Pillow use center coordinates for # transformations, we manually need to add half a pixel otherwise if transform_uses_center(): use_center_px = False else: use_center_px = True meshes = transform_meshes( src_size=src_img.size, src_bbox=src_bbox, src_srs=self.src_srs, dst_size=dst_size, dst_bbox=dst_bbox, dst_srs=self.dst_srs, max_px_err=self.max_px_err, use_center_px=use_center_px, ) img = img_for_resampling(src_img.as_image(), image_opts.resampling) result = img.transform(dst_size, Image.MESH, meshes, image_filter[image_opts.resampling]) if False: # draw mesh for debuging from PIL import ImageDraw draw = ImageDraw.Draw(result) for g, _ in meshes: draw.rectangle(g, fill=None, outline=(255, 0, 0)) return ImageSource(result, size=dst_size, image_opts=image_opts) def _no_transformation_needed(self, src_size, src_bbox, dst_size, dst_bbox): """ >>> src_bbox = (-2504688.5428486541, 1252344.271424327, ... -1252344.271424327, 2504688.5428486541) >>> dst_bbox = (-2504688.5431999983, 1252344.2704, ... -1252344.2719999983, 2504688.5416000001) >>> from mapproxy.srs import SRS >>> t = ImageTransformer(SRS(900913), SRS(900913)) >>> t._no_transformation_needed((256, 256), src_bbox, (256, 256), dst_bbox) True """ xres = (dst_bbox[2]-dst_bbox[0])/dst_size[0] yres = (dst_bbox[3]-dst_bbox[1])/dst_size[1] return (src_size == dst_size and self.src_srs == self.dst_srs and bbox_equals(src_bbox, dst_bbox, xres/10, yres/10)) def transform_meshes( src_size, src_bbox, src_srs, dst_size, dst_bbox, dst_srs, max_px_err=1, use_center_px=False, ): """ transform_meshes creates a list of QUAD transformation parameters for PIL's MESH image transformation. Each QUAD is a rectangle in the destination image, like ``(0, 0, 100, 100)`` and a list of four pixel coordinates in the source image that match the destination rectangle. The four points form a quadliteral (i.e. not a rectangle). PIL's image transform uses affine transformation to fill each rectangle in the destination image with data from the source quadliteral. The number of QUADs is calculated dynamically to keep the deviation in the image transformation below one pixel. Image transformations for large map scales can be transformed with 1-4 QUADs most of the time. For low scales, transform_meshes can generate a few hundred QUADs. It generates a maximum of one QUAD per 50 pixel. """ src_bbox = src_srs.align_bbox(src_bbox) dst_bbox = dst_srs.align_bbox(dst_bbox) src_rect = (0, 0, src_size[0], src_size[1]) dst_rect = (0, 0, dst_size[0], dst_size[1]) to_src_px = make_lin_transf(src_bbox, src_rect) to_src_w = make_lin_transf(src_rect, src_bbox) to_dst_w = make_lin_transf(dst_rect, dst_bbox) meshes = [] if use_center_px: px_offset = 0.5 else: px_offset = 0.0 def dst_quad_to_src(quad): src_quad = [] for dst_px in [(quad[0], quad[1]), (quad[0], quad[3]), (quad[2], quad[3]), (quad[2], quad[1])]: dst_w = to_dst_w( (dst_px[0] + px_offset, dst_px[1] + px_offset)) src_w = dst_srs.transform_to(src_srs, dst_w) src_px = to_src_px(src_w) src_quad.extend(src_px) return quad, src_quad res = (dst_bbox[2] - dst_bbox[0]) / dst_size[0] max_err = max_px_err * res def is_good(quad, src_quad): w = quad[2] - quad[0] h = quad[3] - quad[1] if w < 50 or h < 50: return True xc = quad[0] + w / 2.0 - 0.5 yc = quad[1] + h / 2.0 - 0.5 # coordinate for the center of the quad dst_w = to_dst_w((xc, yc)) # actual coordinate for the center of the quad src_px = center_quad_transform(quad, src_quad) real_dst_w = src_srs.transform_to(dst_srs, to_src_w(src_px)) err = max(abs(dst_w[0] - real_dst_w[0]), abs(dst_w[1] - real_dst_w[1])) return err < max_err # recursively add meshes. divide each quad into four sub quad till # accuracy is good enough. def add_meshes(quads): for quad in quads: quad, src_quad = dst_quad_to_src(quad) if is_good(quad, src_quad): meshes.append((quad, src_quad)) else: add_meshes(divide_quad(quad)) add_meshes([(0, 0, dst_size[0], dst_size[1])]) return meshes def center_quad_transform(quad, src_quad): """ center_quad_transfrom transforms the center pixel coordinates from ``quad`` to ``src_quad`` by using affine transformation as used by PIL.Image.transform. """ w = quad[2] - quad[0] h = quad[3] - quad[1] nw = src_quad[0:2] sw = src_quad[2:4] se = src_quad[4:6] ne = src_quad[6:8] x0, y0 = nw As = 1.0 / w At = 1.0 / h a0 = x0 a1 = (ne[0] - x0) * As a2 = (sw[0] - x0) * At a3 = (se[0] - sw[0] - ne[0] + x0) * As * At a4 = y0 a5 = (ne[1] - y0) * As a6 = (sw[1] - y0) * At a7 = (se[1] - sw[1] - ne[1] + y0) * As * At x = w / 2.0 - 0.5 y = h / 2.0 - 0.5 return ( a0 + a1*x + a2*y + a3*x*y, a4 + a5*x + a6*y + a7*x*y ) def img_for_resampling(img, resampling): """ Convert P images to RGB(A) for non-NEAREST resamplings. """ resampling = image_filter[resampling] if img.mode == 'P' and resampling != Image.NEAREST: img.load() # load to get actual palette mode if img.palette is not None: # palette can still be None for cropped images img = img.convert(img.palette.mode) else: img = img.convert('RGBA') return img def divide_quad(quad): """ divide_quad in up to four sub quads. Only divide horizontal if quad is twice as wide then high, and vertical vice versa. PIL.Image.transform expects that the lower-right corner of a quad overlaps by one pixel. >>> divide_quad((0, 0, 500, 500)) [(0, 0, 250, 250), (250, 0, 500, 250), (0, 250, 250, 500), (250, 250, 500, 500)] >>> divide_quad((0, 0, 2000, 500)) [(0, 0, 1000, 500), (1000, 0, 2000, 500)] >>> divide_quad((100, 200, 200, 500)) [(100, 200, 200, 350), (100, 350, 200, 500)] """ w = quad[2] - quad[0] h = quad[3] - quad[1] xc = int(quad[0] + w/2) yc = int(quad[1] + h/2) if w > 2*h: return [ (quad[0], quad[1], xc, quad[3]), (xc, quad[1], quad[2], quad[3]), ] if h > 2*w: return [ (quad[0], quad[1], quad[2], yc), (quad[0], yc, quad[2], quad[3]), ] return [ (quad[0], quad[1], xc, yc), (xc, quad[1], quad[2], yc), (quad[0], yc, xc, quad[3]), (xc, yc, quad[2], quad[3]), ] mapproxy-1.11.0/mapproxy/layer.py000066400000000000000000000360331320454472400170300ustar00rootroot00000000000000# -:- encoding: utf-8 -:- # This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Layers that can get maps/infos from different sources/caches. """ from __future__ import division from mapproxy.grid import NoTiles, GridError, merge_resolution_range, bbox_intersects, bbox_contains from mapproxy.image import SubImageSource, bbox_position_in_image from mapproxy.image.opts import ImageOptions from mapproxy.image.tile import TiledImage from mapproxy.srs import SRS, bbox_equals, merge_bbox, make_lin_transf from mapproxy.proj import ProjError from mapproxy.compat import iteritems import logging from functools import reduce log = logging.getLogger(__name__) class BlankImage(Exception): pass class MapError(Exception): pass class MapBBOXError(Exception): pass class MapLayer(object): supports_meta_tiles = False res_range = None coverage = None def __init__(self, image_opts=None): self.image_opts = image_opts or ImageOptions() def _get_opacity(self): return self.image_opts.opacity def _set_opacity(self, value): self.image_opts.opacity = value opacity = property(_get_opacity, _set_opacity) def is_opaque(self, query): """ Whether the query result is opaque. This method is used for optimizations: layers below an opaque layer can be skipped. As sources with `transparent: false` still can return transparent images (min_res/max_res/coverages), implementations of this method need to be certain that the image is indeed opaque. is_opaque should return False if in doubt. """ return False def check_res_range(self, query): if (self.res_range and not self.res_range.contains(query.bbox, query.size, query.srs)): raise BlankImage() def get_map(self, query): raise NotImplementedError def combined_layer(self, other, query): return None class LimitedLayer(object): """ Wraps an existing layer temporary and stores additional attributes for geographical limits. """ def __init__(self, layer, coverage): self._layer = layer self.coverage = coverage def __getattr__(self, name): return getattr(self._layer, name) def combined_layer(self, other, query): if self.coverage == other.coverage: combined = self._layer.combined_layer(other, query) if combined: return LimitedLayer(combined, self.coverage) return None def get_info(self, query): if self.coverage: if not self.coverage.contains(query.coord, query.srs): return None return self._layer.get_info(query) class InfoLayer(object): def get_info(self, query): raise NotImplementedError class MapQuery(object): """ Internal query for a map with a specific extent, size, srs, etc. """ def __init__(self, bbox, size, srs, format='image/png', transparent=False, tiled_only=False, dimensions=None): self.bbox = bbox self.size = size self.srs = srs self.format = format self.transparent = transparent self.tiled_only = tiled_only self.dimensions = dimensions or {} def dimensions_for_params(self, params): """ Return subset of the dimensions. >>> mq = MapQuery(None, None, None, dimensions={'Foo': 1, 'bar': 2}) >>> mq.dimensions_for_params(set(['FOO', 'baz'])) {'Foo': 1} """ params = [p.lower() for p in params] return dict((k, v) for k, v in iteritems(self.dimensions) if k.lower() in params) def __repr__(self): return "MapQuery(bbox=%(bbox)s, size=%(size)s, srs=%(srs)r, format=%(format)s)" % self.__dict__ class InfoQuery(object): def __init__(self, bbox, size, srs, pos, info_format, format=None, feature_count=None): self.bbox = bbox self.size = size self.srs = srs self.pos = pos self.info_format = info_format self.format = format self.feature_count = feature_count @property def coord(self): return make_lin_transf((0, 0, self.size[0], self.size[1]), self.bbox)(self.pos) class LegendQuery(object): def __init__(self, format, scale): self.format = format self.scale = scale class Dimension(list): def __init__(self, identifier, values, default=None): self.identifier = identifier if not default and values: default = values[0] self.default = default list.__init__(self, values) def map_extent_from_grid(grid): """ >>> from mapproxy.grid import tile_grid_for_epsg >>> map_extent_from_grid(tile_grid_for_epsg('EPSG:900913')) ... #doctest: +NORMALIZE_WHITESPACE MapExtent((-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244), SRS('EPSG:900913')) """ return MapExtent(grid.bbox, grid.srs) class MapExtent(object): """ >>> me = MapExtent((5, 45, 15, 55), SRS(4326)) >>> me.llbbox (5, 45, 15, 55) >>> [int(x) for x in me.bbox_for(SRS(900913))] [556597, 5621521, 1669792, 7361866] >>> [int(x) for x in me.bbox_for(SRS(4326))] [5, 45, 15, 55] """ is_default = False def __init__(self, bbox, srs): self._llbbox = None self.bbox = bbox self.srs = srs @property def llbbox(self): if not self._llbbox: self._llbbox = self.srs.transform_bbox_to(SRS(4326), self.bbox) return self._llbbox def bbox_for(self, srs): if srs == self.srs: return self.bbox return self.srs.transform_bbox_to(srs, self.bbox) def __repr__(self): return "%s(%r, %r)" % (self.__class__.__name__, self.bbox, self.srs) def __eq__(self, other): if not isinstance(other, MapExtent): return NotImplemented if self.srs != other.srs: return False if self.bbox != other.bbox: return False return True def __ne__(self, other): if not isinstance(other, MapExtent): return NotImplemented return not self.__eq__(other) def __add__(self, other): if not isinstance(other, MapExtent): raise NotImplemented if other.is_default: return self if self.is_default: return other return MapExtent(merge_bbox(self.llbbox, other.llbbox), SRS(4326)) def contains(self, other): if not isinstance(other, MapExtent): raise NotImplemented if self.is_default: # DefaultMapExtent contains everything return True return bbox_contains(self.bbox, other.bbox_for(self.srs)) def intersects(self, other): if not isinstance(other, MapExtent): raise NotImplemented return bbox_intersects(self.bbox, other.bbox_for(self.srs)) def intersection(self, other): """ Returns the intersection of `self` and `other`. >>> e = DefaultMapExtent().intersection(MapExtent((0, 0, 10, 10), SRS(4326))) >>> e.bbox, e.srs ((0, 0, 10, 10), SRS('EPSG:4326')) """ if not self.intersects(other): return None source = self.bbox sub = other.bbox_for(self.srs) return MapExtent(( max(source[0], sub[0]), max(source[1], sub[1]), min(source[2], sub[2]), min(source[3], sub[3])), self.srs) class DefaultMapExtent(MapExtent): """ Default extent that covers the whole world. Will not affect other extents when added. >>> m1 = MapExtent((0, 0, 10, 10), SRS(4326)) >>> m2 = MapExtent((10, 0, 20, 10), SRS(4326)) >>> m3 = DefaultMapExtent() >>> (m1 + m2).bbox (0, 0, 20, 10) >>> (m1 + m3).bbox (0, 0, 10, 10) """ is_default = True def __init__(self): MapExtent.__init__(self, (-180, -90, 180, 90), SRS(4326)) def merge_layer_extents(layers): if not layers: return DefaultMapExtent() layers = layers[:] extent = layers.pop().extent for layer in layers: extent = extent + layer.extent return extent class ResolutionConditional(MapLayer): supports_meta_tiles = True def __init__(self, one, two, resolution, srs, extent, opacity=None): MapLayer.__init__(self) self.one = one self.two = two self.res_range = merge_layer_res_ranges([one, two]) self.resolution = resolution self.srs = srs self.opacity = opacity self.extent = extent def get_map(self, query): self.check_res_range(query) bbox = query.bbox if query.srs != self.srs: bbox = query.srs.transform_bbox_to(self.srs, bbox) xres = (bbox[2] - bbox[0]) / query.size[0] yres = (bbox[3] - bbox[1]) / query.size[1] res = min(xres, yres) log.debug('actual res: %s, threshold res: %s', res, self.resolution) if res > self.resolution: return self.one.get_map(query) else: return self.two.get_map(query) class SRSConditional(MapLayer): supports_meta_tiles = True PROJECTED = 'PROJECTED' GEOGRAPHIC = 'GEOGRAPHIC' def __init__(self, layers, extent, opacity=None): MapLayer.__init__(self) # TODO geographic/projected fallback self.srs_map = {} self.res_range = merge_layer_res_ranges([l[0] for l in layers]) for layer, srss in layers: for srs in srss: self.srs_map[srs] = layer self.extent = extent self.opacity = opacity def get_map(self, query): self.check_res_range(query) layer = self._select_layer(query.srs) return layer.get_map(query) def _select_layer(self, query_srs): # srs exists if query_srs in self.srs_map: return self.srs_map[query_srs] # srs_type exists srs_type = self.GEOGRAPHIC if query_srs.is_latlong else self.PROJECTED if srs_type in self.srs_map: return self.srs_map[srs_type] # first with same type is_latlong = query_srs.is_latlong for srs in self.srs_map: if hasattr(srs, 'is_latlong') and srs.is_latlong == is_latlong: return self.srs_map[srs] # return first return self.srs_map.itervalues().next() class DirectMapLayer(MapLayer): supports_meta_tiles = True def __init__(self, source, extent): MapLayer.__init__(self) self.source = source self.res_range = getattr(source, 'res_range', None) self.extent = extent def get_map(self, query): self.check_res_range(query) return self.source.get_map(query) def merge_layer_res_ranges(layers): ranges = [s.res_range for s in layers if hasattr(s, 'res_range')] if ranges: ranges = reduce(merge_resolution_range, ranges) return ranges class CacheMapLayer(MapLayer): supports_meta_tiles = True def __init__(self, tile_manager, extent=None, image_opts=None, max_tile_limit=None): MapLayer.__init__(self, image_opts=image_opts) self.tile_manager = tile_manager self.grid = tile_manager.grid self.extent = extent or map_extent_from_grid(self.grid) self.res_range = merge_layer_res_ranges(self.tile_manager.sources) self.max_tile_limit = max_tile_limit def get_map(self, query): self.check_res_range(query) if query.tiled_only: self._check_tiled(query) query_extent = MapExtent(query.bbox, query.srs) if not query.tiled_only and self.extent and not self.extent.contains(query_extent): if not self.extent.intersects(query_extent): raise BlankImage() size, offset, bbox = bbox_position_in_image(query.bbox, query.size, self.extent.bbox_for(query.srs)) if size[0] == 0 or size[1] == 0: raise BlankImage() src_query = MapQuery(bbox, size, query.srs, query.format) resp = self._image(src_query) result = SubImageSource(resp, size=query.size, offset=offset, image_opts=self.image_opts, cacheable=resp.cacheable) else: result = self._image(query) return result def _check_tiled(self, query): if query.format != self.tile_manager.format: raise MapError("invalid tile format, use %s" % self.tile_manager.format) if query.size != self.grid.tile_size: raise MapError("invalid tile size (use %dx%d)" % self.grid.tile_size) def _image(self, query): try: src_bbox, tile_grid, affected_tile_coords = \ self.grid.get_affected_tiles(query.bbox, query.size, req_srs=query.srs) except NoTiles: raise BlankImage() except GridError as ex: raise MapBBOXError(ex.args[0]) num_tiles = tile_grid[0] * tile_grid[1] if self.max_tile_limit and num_tiles >= self.max_tile_limit: raise MapBBOXError("too many tiles") if query.tiled_only: if num_tiles > 1: raise MapBBOXError("not a single tile") bbox = query.bbox if not bbox_equals(bbox, src_bbox, abs((bbox[2]-bbox[0])/query.size[0]/10), abs((bbox[3]-bbox[1])/query.size[1]/10)): raise MapBBOXError("query does not align to tile boundaries") with self.tile_manager.session(): tile_collection = self.tile_manager.load_tile_coords(affected_tile_coords, with_metadata=query.tiled_only) if tile_collection.empty: raise BlankImage() if query.tiled_only: tile = tile_collection[0].source tile.image_opts = self.tile_manager.image_opts tile.cacheable = tile_collection[0].cacheable return tile tile_sources = [tile.source for tile in tile_collection] tiled_image = TiledImage(tile_sources, src_bbox=src_bbox, src_srs=self.grid.srs, tile_grid=tile_grid, tile_size=self.grid.tile_size) try: return tiled_image.transform(query.bbox, query.srs, query.size, self.tile_manager.image_opts) except ProjError: raise MapBBOXError("could not transform query BBOX") except IOError as ex: from mapproxy.source import SourceError raise SourceError("unable to transform image: %s" % ex) mapproxy-1.11.0/mapproxy/multiapp.py000066400000000000000000000166301320454472400175500ustar00rootroot00000000000000# -:- encoding: utf-8 -:- # This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os from mapproxy.request import Request from mapproxy.response import Response from mapproxy.util.collections import LRU from mapproxy.wsgiapp import make_wsgi_app as make_mapproxy_wsgi_app from mapproxy.compat import iteritems from threading import Lock import logging log = logging.getLogger(__name__) def asbool(value): """ >>> all([asbool(True), asbool('trUE'), asbool('ON'), asbool(1)]) True >>> any([asbool(False), asbool('false'), asbool('foo'), asbool(None)]) False """ value = str(value).lower() return value in ('1', 'true', 'yes', 'on') def app_factory(global_options, config_dir, allow_listing=False, **local_options): """ Create a new MultiMapProxy app. :param config_dir: directory with all mapproxy configurations :param allow_listing: allow to list all available apps """ return make_wsgi_app(config_dir, asbool(allow_listing)) def make_wsgi_app(config_dir, allow_listing=True, debug=False): """ Create a MultiMapProxy with the given config directory. :param config_dir: the directory with all project configurations. :param allow_listing: True if MapProxy should list all instances at the root URL """ config_dir = os.path.abspath(config_dir) loader = DirectoryConfLoader(config_dir) return MultiMapProxy(loader, list_apps=allow_listing, debug=debug) class MultiMapProxy(object): def __init__(self, loader, list_apps=False, app_cache_size=100, debug=False): self.loader = loader self.list_apps = list_apps self._app_init_lock = Lock() self.apps = LRU(app_cache_size) self.debug = debug def __call__(self, environ, start_response): req = Request(environ) return self.handle(req)(environ, start_response) def handle(self, req): app_name = req.pop_path() if not app_name: return self.index_list(req) if not app_name or ( app_name not in self.apps and not self.loader.app_available(app_name) ): return Response('not found', status=404) # safe instance/app name for authorization req.environ['mapproxy.instance_name'] = app_name return self.proj_app(app_name) def index_list(self, req): """ Return greeting response with a list of available apps (if enabled with list_apps). """ import mapproxy.version html = "

Welcome to MapProxy %s

" % mapproxy.version.version url = req.script_url if self.list_apps: html += "

available instances:

' html += '' return Response(html, content_type='text/html') def proj_app(self, proj_name): """ Return the (cached) project app. """ proj_app, timestamps = self.apps.get(proj_name, (None, None)) if proj_app: if self.loader.needs_reload(proj_name, timestamps): # discard cached app proj_app = None if not proj_app: with self._app_init_lock: proj_app, timestamps = self.apps.get(proj_name, (None, None)) if self.loader.needs_reload(proj_name, timestamps): proj_app, timestamps = self.create_app(proj_name) self.apps[proj_name] = proj_app, timestamps else: proj_app, timestamps = self.apps[proj_name] return proj_app def create_app(self, proj_name): """ Returns a new configured MapProxy app and a dict with the timestamps of all configuration files. """ mapproxy_conf = self.loader.app_conf(proj_name)['mapproxy_conf'] log.info('initializing project app %s with %s', proj_name, mapproxy_conf) app = make_mapproxy_wsgi_app(mapproxy_conf, debug=self.debug) return app, app.config_files class ConfLoader(object): def needs_reload(self, app_name, timestamps): """ Returns ``True`` if the configuration of `app_name` changed since `timestamp`. """ raise NotImplementedError() def app_available(self, app_name): """ Returns ``True`` if `app_name` is available. """ raise NotImplementedError() def available_apps(self): """ Returns a list with all available lists. """ raise NotImplementedError() def app_conf(self, app_name): """ Returns a configuration dict for the given `app_name`, None if the app is not found. The configuration dict contains at least 'mapproxy_conf' with the filename of the configuration. """ raise NotImplementedError() class DirectoryConfLoader(ConfLoader): """ Load application configurations from a directory. """ def __init__(self, base_dir, suffix='.yaml'): self.base_dir = base_dir self.suffix = suffix def needs_reload(self, app_name, timestamps): if not timestamps: return True for conf_file, timestamp in iteritems(timestamps): m_time = os.path.getmtime(conf_file) if m_time > timestamp: return True return False def _is_conf_file(self, fname): if not os.path.isfile(fname): return False if self.suffix: return fname.lower().endswith(self.suffix) else: return True def app_name_from_filename(self, fname): """ >>> DirectoryConfLoader('/tmp/').app_name_from_filename('/tmp/foobar.yaml') 'foobar' """ _path, fname = os.path.split(fname) app_name, _ext = os.path.splitext(fname) return app_name def filename_from_app_name(self, app_name): """ >>> DirectoryConfLoader('/tmp/').filename_from_app_name('foobar') '/tmp/foobar.yaml' """ return os.path.join(self.base_dir, app_name + self.suffix or '') def available_apps(self): apps = [] for f in os.listdir(self.base_dir): if self._is_conf_file(os.path.join(self.base_dir, f)): app_name = self.app_name_from_filename(f) apps.append(app_name) apps.sort() return apps def app_available(self, app_name): conf_file = self.filename_from_app_name(app_name) return self._is_conf_file(conf_file) def app_conf(self, app_name): conf_file = self.filename_from_app_name(app_name) if not self._is_conf_file(conf_file): return None return {'mapproxy_conf': conf_file} mapproxy-1.11.0/mapproxy/proj.py000066400000000000000000000175571320454472400167000ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ ctypes based replacement of pyroj (with pyproj fallback). This module implements the `Proj`, `transform` and `set_datapath` class/functions. This module is a drop-in replacement for pyproj. It does implement just enough to work for MapProxy, i.e. there is no numpy support, etc. It uses the C libproj library. If the library could not be found/loaded it will fallback to pyroj. You can force the usage of either backend by setting the environment variables MAPPROXY_USE_LIBPROJ or MAPPROXY_USE_PYPROJ to any value. """ from __future__ import print_function import os import sys from mapproxy.util.lib import load_library import ctypes from ctypes import ( c_void_p, c_char_p, c_int, c_double, c_long, POINTER, create_string_buffer, addressof, ) c_double_p = POINTER(c_double) FINDERCMD = ctypes.CFUNCTYPE(c_char_p, c_char_p) import logging log_system = logging.getLogger('mapproxy.system') __all__ = ['Proj', 'transform', 'set_datapath', 'ProjError'] def init_libproj(): libproj = load_library('libproj') if libproj is None: return libproj.pj_init_plus.argtypes = [c_char_p] libproj.pj_init_plus.restype = c_void_p libproj.pj_is_latlong.argtypes = [c_void_p] libproj.pj_is_latlong.restype = c_int libproj.pj_get_def.argtypes = [c_void_p, c_int] libproj.pj_get_def.restype = c_void_p libproj.pj_strerrno.argtypes = [c_int] libproj.pj_strerrno.restype = c_char_p libproj.pj_get_errno_ref.argtypes = [] libproj.pj_get_errno_ref.restype = POINTER(c_int) # free proj objects libproj.pj_free.argtypes = [c_void_p] # free() wrapper libproj.pj_dalloc.argtypes = [c_void_p] libproj.pj_transform.argtypes = [c_void_p, c_void_p, c_long, c_int, c_double_p, c_double_p, c_double_p] libproj.pj_transform.restype = c_int if hasattr(libproj, 'pj_set_searchpath'): libproj.pj_set_searchpath.argtypes = [c_int, POINTER(c_char_p)] libproj.pj_set_finder.argtypes = [FINDERCMD] return libproj class SearchPath(object): def __init__(self): self.path = None self.finder_results = {} def clear(self): self.path = None self.finder_results = {} def set_searchpath(self, path): self.clear() if path is not None: path = path.encode(sys.getfilesystemencoding() or 'utf-8') self.path = path def finder(self, name): if self.path is None: return None if name in self.finder_results: result = self.finder_results[name] else: sysname = os.path.join(self.path, name) result = self.finder_results[name] = create_string_buffer(sysname) return addressof(result) # search_path and finder_func must be defined in module # context to avoid garbage collection search_path = SearchPath() finder_func = FINDERCMD(search_path.finder) _finder_callback_set = False class ProjError(RuntimeError): pass class ProjInitError(ProjError): pass def try_pyproj_import(): try: from pyproj import Proj, transform, set_datapath except ImportError: return False log_system.info('using pyproj for coordinate transformation') return Proj, transform, set_datapath def try_libproj_import(): libproj = init_libproj() if libproj is None: return False log_system.info('using libproj for coordinate transformation') RAD_TO_DEG = 57.29577951308232 DEG_TO_RAD = .0174532925199432958 class Proj(object): def __init__(self, proj_def=None, init=None): if init: self._proj = libproj.pj_init_plus(b'+init=' + init.encode('ascii')) else: self._proj = libproj.pj_init_plus(proj_def.encode('ascii')) if not self._proj: errno = libproj.pj_get_errno_ref().contents raise ProjInitError('error initializing Proj(proj_def=%r, init=%r): %s' % (proj_def, init, libproj.pj_strerrno(errno))) self.srs = self._srs() self._latlong = bool(libproj.pj_is_latlong(self._proj)) def is_latlong(self): """ >>> Proj(init='epsg:4326').is_latlong() True >>> Proj(init='epsg:4258').is_latlong() True >>> Proj(init='epsg:31467').is_latlong() False >>> Proj('+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 ' ... '+lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m ' ... '+nadgrids=@null +no_defs').is_latlong() False """ return self._latlong def _srs(self): res = libproj.pj_get_def(self._proj, 0) srs_def = ctypes.c_char_p(res).value libproj.pj_dalloc(res) return srs_def def __del__(self): if self._proj and libproj: libproj.pj_free(self._proj) self._proj = None def transform(from_srs, to_srs, x, y, z=None): if from_srs == to_srs: return (x, y) if z is None else (x, y, z) if isinstance(x, (float, int)): x = [x] y = [y] assert len(x) == len(y) if from_srs.is_latlong(): x = [x*DEG_TO_RAD for x in x] y = [y*DEG_TO_RAD for y in y] x = (c_double * len(x))(*x) y = (c_double * len(y))(*y) if z is not None: z = (c_double * len(z))(*z) else: # use explicit null pointer instead of None # http://bugs.python.org/issue4606 z = c_double_p() res = libproj.pj_transform(from_srs._proj, to_srs._proj, len(x), 0, x, y, z) if res: raise ProjError(libproj.pj_strerrno(res)) if to_srs.is_latlong(): x = [x*RAD_TO_DEG for x in x] y = [y*RAD_TO_DEG for y in y] else: x = x[:] y = y[:] if len(x) == 1: x = x[0] y = y[0] z = z[0] if z else None return (x, y) if z is None else (x, y, z) def set_datapath(path): global _finder_callback_set if not _finder_callback_set: libproj.pj_set_finder(finder_func) _finder_callback_set = True search_path.set_searchpath(path) return Proj, transform, set_datapath proj_imports = [] if 'MAPPROXY_USE_LIBPROJ' in os.environ: proj_imports = [try_libproj_import] if 'MAPPROXY_USE_PYPROJ' in os.environ: proj_imports = [try_pyproj_import] if not proj_imports: if sys.platform == 'win32': # prefer pyproj on windows proj_imports = [try_pyproj_import, try_libproj_import] else: proj_imports = [try_libproj_import, try_pyproj_import] for try_import in proj_imports: res = try_import() if res: Proj, transform, set_datapath = res break else: raise ImportError('could not find libproj or pyproj') if __name__ == '__main__': prj1 = Proj(init='epsg:4326') prj2 = Proj(init='epsg:31467') coords = [(8.2, 8.22, 8.3), (53.1, 53.15, 53.2)] # coords = [(8, 9, 10), (50, 50, 50)] print(coords) coords = transform(prj1, prj2, *coords) print(coords) coords = transform(prj2, prj1, *coords) print(coords) mapproxy-1.11.0/mapproxy/request/000077500000000000000000000000001320454472400170255ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/request/__init__.py000066400000000000000000000013411320454472400211350ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.request.base import Request, url_decode __all__ = ['Request', 'url_decode']mapproxy-1.11.0/mapproxy/request/arcgis.py000066400000000000000000000203301320454472400206450ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from functools import partial as fp from mapproxy.compat import string_type from mapproxy.compat.modules import urlparse from mapproxy.request.base import RequestParams, BaseRequest from mapproxy.srs import make_lin_transf class ArcGISExportRequestParams(RequestParams): """ Supported params f, bbox(required), size, dpi, imageSR, bboxSR, format, layerDefs, layers, transparent, time, layerTimeOptions. @param layers: Determines which layers appear on the exported map. There are four ways to specify layers: show, hide, include, exclude. (ex show:1,2) """ def _get_format(self): """ The requested format as string (w/o any 'image/', 'text/', etc prefixes) """ return self["format"] def _set_format(self, format): self["format"] = format.rsplit("/")[-1] format = property(_get_format, _set_format) del _get_format del _set_format def _get_bbox(self): """ ``bbox`` as a tuple (minx, miny, maxx, maxy). """ if 'bbox' not in self.params or self.params['bbox'] is None: return None points = [float(val) for val in self.params['bbox'].split(',')] return tuple(points[:4]) def _set_bbox(self, value): if value is not None and not isinstance(value, string_type): value = ','.join(str(x) for x in value) self['bbox'] = value bbox = property(_get_bbox, _set_bbox) del _get_bbox del _set_bbox def _get_size(self): """ Size of the request in pixel as a tuple (width, height), or None if one is missing. """ if 'size' not in self.params or self.params['size'] is None: return None dim = [float(val) for val in self.params['size'].split(',')] return tuple(dim[:2]) def _set_size(self, value): if value is not None and not isinstance(value, string_type): value = ','.join(str(x) for x in value) self['size'] = value size = property(_get_size, _set_size) del _get_size del _set_size def _get_srs(self, key): return self.params.get(key, None) def _set_srs(self, srs, key): if hasattr(srs, 'srs_code'): code = srs.srs_code else: code = srs self.params[key] = code.rsplit(":", 1)[-1] bboxSR = property(fp(_get_srs, key="bboxSR"), fp(_set_srs, key="bboxSR")) imageSR = property(fp(_get_srs, key="imageSR"), fp(_set_srs, key="imageSR")) del _get_srs del _set_srs class ArcGISIdentifyRequestParams(ArcGISExportRequestParams): def _get_format(self): """ The requested format as string (w/o any 'image/', 'text/', etc prefixes) """ return self["format"] def _set_format(self, format): self["format"] = format.rsplit("/")[-1] format = property(_get_format, _set_format) del _get_format del _set_format def _get_bbox(self): """ ``bbox`` as a tuple (minx, miny, maxx, maxy). """ if 'mapExtent' not in self.params or self.params['mapExtent'] is None: return None points = [float(val) for val in self.params['mapExtent'].split(',')] return tuple(points[:4]) def _set_bbox(self, value): if value is not None and not isinstance(value, string_type): value = ','.join(str(x) for x in value) self['mapExtent'] = value bbox = property(_get_bbox, _set_bbox) del _get_bbox del _set_bbox def _get_size(self): """ Size of the request in pixel as a tuple (width, height), or None if one is missing. """ if 'imageDisplay' not in self.params or self.params['imageDisplay'] is None: return None dim = [float(val) for val in self.params['imageDisplay'].split(',')] return tuple(dim[:2]) def _set_size(self, value): if value is not None and not isinstance(value, string_type): value = ','.join(str(x) for x in value) + ',96' self['imageDisplay'] = value size = property(_get_size, _set_size) del _get_size del _set_size def _get_pos(self): size = self.size vals = self['geometry'].split(',') x, y = float(vals[0]), float(vals[1]) return make_lin_transf(self.bbox, (0, 0, size[0], size[1]))((x, y)) def _set_pos(self, value): size = self.size req_coord = make_lin_transf((0, 0, size[0], size[1]), self.bbox)(value) self['geometry'] = '%f,%f' % req_coord pos = property(_get_pos, _set_pos) del _get_pos del _set_pos @property def srs(self): srs = self.params.get('sr', None) if srs: return 'EPSG:%s' % srs @srs.setter def srs(self, srs): if hasattr(srs, 'srs_code'): code = srs.srs_code else: code = srs self.params['sr'] = code.rsplit(':', 1)[-1] class ArcGISRequest(BaseRequest): request_params = ArcGISExportRequestParams fixed_params = {"f": "image"} def __init__(self, param=None, url='', validate=False, http=None): BaseRequest.__init__(self, param, url, validate, http) self.url = rest_endpoint(url) def copy(self): return self.__class__(param=self.params.copy(), url=self.url) @property def query_string(self): params = self.params.copy() for key, value in self.fixed_params.items(): params[key] = value return params.query_string class ArcGISIdentifyRequest(BaseRequest): request_params = ArcGISIdentifyRequestParams fixed_params = {'geometryType': 'esriGeometryPoint'} def __init__(self, param=None, url='', validate=False, http=None): BaseRequest.__init__(self, param, url, validate, http) self.url = rest_identify_endpoint(url) def copy(self): return self.__class__(param=self.params.copy(), url=self.url) @property def query_string(self): params = self.params.copy() for key, value in self.fixed_params.items(): params[key] = value return params.query_string def create_identify_request(req_data, param): req_data = req_data.copy() # Pop the URL off the request data. url = req_data['url'] del req_data['url'] return ArcGISIdentifyRequest(url=url, param=req_data) def create_request(req_data, param): req_data = req_data.copy() # Pop the URL off the request data. url = req_data['url'] del req_data['url'] if 'format' in param: req_data['format'] = param['format'] if 'transparent' in req_data: # Convert boolean to a string. req_data['transparent'] = str(req_data['transparent']) return ArcGISRequest(url=url, param=req_data) def rest_endpoint(url): parts = urlparse.urlsplit(url) path = parts.path.rstrip('/').split('/') if path[-1] in ('export', 'exportImage'): if path[-2] == 'MapServer': path[-1] = 'export' elif path[-2] == 'ImageServer': path[-1] = 'exportImage' elif path[-1] == 'MapServer': path.append('export') elif path[-1] == 'ImageServer': path.append('exportImage') parts = parts[0], parts[1], '/'.join(path), parts[3], parts[4] return urlparse.urlunsplit(parts) def rest_identify_endpoint(url): parts = urlparse.urlsplit(url) path = parts.path.rstrip('/').split('/') if path[-1] in ('export', 'exportImage'): path[-1] = 'identify' elif path[-1] in ('MapServer', 'ImageServer'): path.append('identify') parts = parts[0], parts[1], '/'.join(path), parts[3], parts[4] return urlparse.urlunsplit(parts) mapproxy-1.11.0/mapproxy/request/base.py000066400000000000000000000351701320454472400203170ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Service requests (parsing, handling, etc). """ import cgi from mapproxy.util.py import cached_property from mapproxy.compat import iteritems, PY2, text_type if PY2: from urllib import quote else: from urllib.parse import quote class NoCaseMultiDict(dict): """ This is a dictionary that allows case insensitive access to values. >>> d = NoCaseMultiDict([('A', 'b'), ('a', 'c'), ('B', 'f'), ('c', 'x'), ('c', 'y'), ('c', 'z')]) >>> d['a'] 'b' >>> d.get_all('a') ['b', 'c'] >>> 'a' in d and 'b' in d True """ def _gen_dict(self, mapping=()): """A `NoCaseMultiDict` can be constructed from an iterable of ``(key, value)`` tuples or a dict. """ tmp = {} if isinstance(mapping, NoCaseMultiDict): for key, value in mapping.iteritems(): #pylint: disable-msg=E1103 tmp.setdefault(key.lower(), (key, []))[1].extend(value) else: if isinstance(mapping, dict): itr = iteritems(mapping) else: itr = iter(mapping) for key, value in itr: tmp.setdefault(key.lower(), (key, []))[1].append(value) return tmp def __init__(self, mapping=()): """A `NoCaseMultiDict` can be constructed from an iterable of ``(key, value)`` tuples or a dict. """ dict.__init__(self, self._gen_dict(mapping)) def update(self, mapping=(), append=False): """A `NoCaseMultiDict` can be updated from an iterable of ``(key, value)`` tuples or a dict. """ for _, (key, values) in iteritems(self._gen_dict(mapping)): self.set(key, values, append=append, unpack=True) def __getitem__(self, key): """ Return the first data value for this key. :raise KeyError: if the key does not exist """ if key in self: return dict.__getitem__(self, key.lower())[1][0] raise KeyError(key) def __setitem__(self, key, value): dict.setdefault(self, key.lower(), (key, []))[1][:] = [value] def __delitem__(self, key): dict.__delitem__(self, key.lower()) def __contains__(self, key): return dict.__contains__(self, key.lower()) def __getstate__(self): data = [] for key, values in self.iteritems(): for v in values: data.append((key, v)) return data def __setstate__(self, data): self.__init__(data) def get(self, key, default=None, type_func=None): """Return the default value if the requested data doesn't exist. If `type_func` is provided and is a callable it should convert the value, return it or raise a `ValueError` if that is not possible. In this case the function will return the default as if the value was not found. Example: >>> d = NoCaseMultiDict(dict(foo='42', bar='blub')) >>> d.get('foo', type_func=int) 42 >>> d.get('bar', -1, type_func=int) -1 """ try: rv = self[key] if type_func is not None: rv = type_func(rv) except (KeyError, ValueError): rv = default return rv def get_all(self, key): """ Return all values for the key as a list. Returns an empty list, if the key doesn't exist. """ if key in self: return dict.__getitem__(self, key.lower())[1] else: return [] def set(self, key, value, append=False, unpack=False): """ Set a `value` for the `key`. If `append` is ``True`` the value will be added to other values for this `key`. If `unpack` is True, `value` will be unpacked and each item will be added. """ if key in self: if not append: dict.__getitem__(self, key.lower())[1][:] = [] else: dict.__setitem__(self, key.lower(), (key, [])) if unpack: for v in value: dict.__getitem__(self, key.lower())[1].append(v) else: dict.__getitem__(self, key.lower())[1].append(value) def iteritems(self): """ Iterates over all keys and values. """ if PY2: for _, (key, values) in dict.iteritems(self): yield key, values else: for _, (key, values) in dict.items(self): yield key, values def copy(self): """ Returns a copy of this object. """ return self.__class__(self) def __repr__(self): tmp = [] for key, values in self.iteritems(): tmp.append((key, values)) return '%s(%r)' % (self.__class__.__name__, tmp) def url_decode(qs, charset='utf-8', decode_keys=False, include_empty=True, errors='ignore'): """ Parse query string `qs` and return a `NoCaseMultiDict`. """ tmp = [] for key, value in cgi.parse_qsl(qs, include_empty): if PY2: if decode_keys: key = key.decode(charset, errors) tmp.append((key, value.decode(charset, errors))) else: if not isinstance(key, text_type): key = key.decode(charset, errors) if not isinstance(value, text_type): value = value.decode(charset, errors) tmp.append((key, value)) return NoCaseMultiDict(tmp) class Request(object): charset = 'utf8' def __init__(self, environ): self.environ = environ self.environ['mapproxy.request'] = self script_name = environ.get('HTTP_X_SCRIPT_NAME', '') if script_name: del environ['HTTP_X_SCRIPT_NAME'] environ['SCRIPT_NAME'] = script_name path_info = environ['PATH_INFO'] if path_info.startswith(script_name): environ['PATH_INFO'] = path_info[len(script_name):] @cached_property def args(self): if self.environ.get('QUERY_STRING'): return url_decode(self.environ['QUERY_STRING'], self.charset) else: return {} @property def path(self): path = self.environ.get('PATH_INFO', '') if PY2: return path if path and isinstance(path, bytes): path = path.decode('utf-8') return path def pop_path(self): path = self.path.lstrip('/') if '/' in path: result, rest = path.split('/', 1) self.environ['PATH_INFO'] = '/' + rest else: self.environ['PATH_INFO'] = '' result = path if result: self.environ['SCRIPT_NAME'] = self.environ['SCRIPT_NAME'] + '/' + result return result @cached_property def host(self): if 'HTTP_X_FORWARDED_HOST' in self.environ: # might be a list, return first host only host = self.environ['HTTP_X_FORWARDED_HOST'] host = host.split(',', 1)[0].strip() return host elif 'HTTP_HOST' in self.environ: host = self.environ['HTTP_HOST'] if ':' in host: port = host.split(':')[1] if ((self.url_scheme, port) in (('https', '443'), ('http', '80'))): host = host.split(':')[0] return host result = self.environ['SERVER_NAME'] if ((self.url_scheme, self.environ['SERVER_PORT']) not in (('https', '443'), ('http', '80'))): result += ':' + self.environ['SERVER_PORT'] return result @cached_property def url_scheme(self): scheme = self.environ.get('HTTP_X_FORWARDED_PROTO') if not scheme: scheme = self.environ['wsgi.url_scheme'] return scheme @cached_property def host_url(self): return '%s://%s/' % (self.url_scheme, self.host) @property def script_url(self): "Full script URL without trailing /" return (self.host_url.rstrip('/') + quote(self.environ.get('SCRIPT_NAME', '/').rstrip('/')) ) @property def base_url(self): return (self.host_url.rstrip('/') + quote(self.environ.get('SCRIPT_NAME', '').rstrip('/')) + quote(self.environ.get('PATH_INFO', '')) ) class RequestParams(object): """ This class represents key-value request parameters. It allows case-insensitive access to all keys. Multiple values for a single key will be concatenated (eg. to ``layers=foo&layers=bar`` becomes ``layers: foo,bar``). All values can be accessed as a property. :param param: A dict or ``NoCaseMultiDict``. """ params = None def __init__(self, param=None): self.delimiter = ',' if param is None: self.params = NoCaseMultiDict() else: self.params = NoCaseMultiDict(param) def __str__(self): return self.query_string def get(self, key, default=None, type_func=None): """ Returns the value for `key` or the `default`. `type_func` is called on the value to alter the value (e.g. use ``type_func=int`` to get ints). """ return self.params.get(key, default, type_func) def set(self, key, value, append=False, unpack=False): """ Set a `value` for the `key`. If `append` is ``True`` the value will be added to other values for this `key`. If `unpack` is True, `value` will be unpacked and each item will be added. """ self.params.set(key, value, append=append, unpack=unpack) def update(self, mapping=(), append=False): """ Update internal request parameters from an iterable of ``(key, value)`` tuples or a dict. If `append` is ``True`` the value will be added to other values for this `key`. """ self.params.update(mapping, append=append) def __getattr__(self, name): if name in self: return self[name] else: raise AttributeError("'%s' object has no attribute '%s" % (self.__class__.__name__, name)) def __getitem__(self, key): return self.delimiter.join(map(text_type, self.params.get_all(key))) def __setitem__(self, key, value): """ Set `value` for the `key`. Does not append values (see ``MapRequest.set``). """ self.set(key, value) def __delitem__(self, key): if key in self: del self.params[key] def iteritems(self): for key, values in self.params.iteritems(): yield key, self.delimiter.join((text_type(x) for x in values)) def __contains__(self, key): return self.params and key in self.params def copy(self): return self.__class__(self.params) @property def query_string(self): """ The map request as a query string (the order is not guaranteed). >>> qs = RequestParams(dict(foo='egg', bar='ham%eggs', baz=100)).query_string >>> sorted(qs.split('&')) ['bar=ham%25eggs', 'baz=100', 'foo=egg'] """ kv_pairs = [] for key, values in self.params.iteritems(): value = ','.join(text_type(v) for v in values) kv_pairs.append(key + '=' + quote(value.encode('utf-8'), safe=',')) return '&'.join(kv_pairs) def with_defaults(self, defaults): """ Return this MapRequest with all values from `defaults` overwritten. """ new = self.copy() for key, value in defaults.params.iteritems(): if value != [None]: new.set(key, value, unpack=True) return new class BaseRequest(object): """ This class represents a request with a URL and key-value parameters. :param param: A dict, `NoCaseMultiDict` or ``RequestParams``. :param url: The service URL for the request. :param validate: True if the request should be validated after initialization. """ request_params = RequestParams def __init__(self, param=None, url='', validate=False, http=None): self.delimiter = ',' self.http = http if param is None: self.params = self.request_params(NoCaseMultiDict()) else: if isinstance(param, RequestParams): self.params = self.request_params(param.params) else: self.params = self.request_params(NoCaseMultiDict(param)) self.url = url if validate: self.validate() def __str__(self): return self.complete_url def validate(self): pass @property def raw_params(self): params = {} for key, value in iteritems(self.params): params[key] = value return params @property def query_string(self): return self.params.query_string @property def complete_url(self): """ The complete MapRequest as URL. """ if not self.url: return self.query_string delimiter = '?' if '?' in self.url: delimiter = '&' if self.url[-1] == '?': delimiter = '' return self.url + delimiter + self.query_string def copy_with_request_params(self, req): """ Return a copy of this request ond overwrite all param values from `req`. Use this method for templates (``req_template.copy_with_request_params(actual_values)``). """ new_params = req.params.with_defaults(self.params) return self.__class__(param=new_params, url=self.url) def __repr__(self): return '%s(param=%r, url=%r)' % (self.__class__.__name__, self.params, self.url) def split_mime_type(mime_type): """ >>> split_mime_type('text/xml; charset=utf-8') ('text', 'xml', 'charset=utf-8') """ options = None mime_class = None if '/' in mime_type: mime_class, mime_type = mime_type.split('/', 1) if ';' in mime_type: mime_type, options = [part.strip() for part in mime_type.split(';', 2)] return mime_class, mime_type, options mapproxy-1.11.0/mapproxy/request/tile.py000066400000000000000000000101141320454472400203310ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import re from mapproxy.exception import ( RequestError, XMLExceptionHandler, PlainExceptionHandler, ) import mapproxy.service from mapproxy.template import template_loader get_template = template_loader(mapproxy.service.__name__, 'templates') class TileRequest(object): """ Class for tile requests. """ request_handler_name = 'map' tile_req_re = re.compile(r'''^(?P/[^/]+)/ ((?P1\.0\.0)/)? (?P[^/]+)/ ((?P[^/]+)/)? (?P-?\d+)/ (?P-?\d+)/ (?P-?\d+)\.(?P\w+)''', re.VERBOSE) use_profiles = False req_prefix = '/tiles' origin = None dimensions = {} def __init__(self, request): self.tile = None self.format = None self.http = request self._init_request() self.origin = self.http.args.get('origin') if self.origin not in ('sw', 'nw', None): self.origin = None def _init_request(self): """ Initialize tile request. Sets ``tile`` and ``layer``. :raise RequestError: if the format is not ``/layer/z/x/y.format`` """ match = self.tile_req_re.search(self.http.path) if not match or match.group('begin') != self.req_prefix: raise RequestError('invalid request (%s)' % (self.http.path), request=self) self.layer = match.group('layer') self.dimensions = {} if match.group('layer_spec') is not None: self.dimensions['_layer_spec'] = match.group('layer_spec') if not self.tile: self.tile = tuple([int(match.group(v)) for v in ['x', 'y', 'z']]) if not self.format: self.format = match.group('format') @property def exception_handler(self): return PlainExceptionHandler() class TMSRequest(TileRequest): """ Class for TMS 1.0.0 requests. """ request_handler_name = 'map' req_prefix = '/tms' capabilities_re = re.compile(r''' ^.*/1\.0\.0/? (/(?P[^/]+))? (/(?P[^/]+))? $''', re.VERBOSE) root_request_re = re.compile(r'/tms/?$') use_profiles = True origin = 'sw' def __init__(self, request): self.tile = None self.format = None self.http = request cap_match = self.capabilities_re.match(request.path) root_match = self.root_request_re.match(request.path) if cap_match: if cap_match.group('layer') is not None: self.layer = cap_match.group('layer') self.dimensions = {} if cap_match.group('layer_spec') is not None: self.dimensions['_layer_spec'] = cap_match.group('layer_spec') self.request_handler_name = 'tms_capabilities' elif root_match: self.request_handler_name = 'tms_root_resource' else: self._init_request() @property def exception_handler(self): return TMSExceptionHandler() def tile_request(req): if req.path.startswith('/tms'): return TMSRequest(req) else: return TileRequest(req) class TMSExceptionHandler(XMLExceptionHandler): template_file = 'tms_exception.xml' template_func = get_template mimetype = 'text/xml' status_code = 404 def render(self, request_error): if request_error.internal: self.status_code = 500 return XMLExceptionHandler.render(self, request_error)mapproxy-1.11.0/mapproxy/request/wms/000077500000000000000000000000001320454472400176335ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/request/wms/__init__.py000066400000000000000000000643601320454472400217550ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Service requests (parsing, handling, etc). """ import codecs from mapproxy.request.wms import exception from mapproxy.exception import RequestError from mapproxy.srs import SRS, make_lin_transf from mapproxy.request.base import RequestParams, BaseRequest, split_mime_type from mapproxy.compat import string_type, iteritems import logging log = logging.getLogger(__name__) class WMSMapRequestParams(RequestParams): """ This class represents key-value parameters for WMS map requests. All values can be accessed as a property. Some properties return processed values. ``size`` returns a tuple of the width and height, ``layers`` returns an iterator of all layers, etc. """ def _get_layers(self): """ List with all layer names. """ return sum((layers.split(',') for layers in self.params.get_all('layers')), []) def _set_layers(self, layers): if isinstance(layers, (list, tuple)): layers = ','.join(layers) self.params['layers'] = layers layers = property(_get_layers, _set_layers) del _get_layers del _set_layers def _get_bbox(self): """ ``bbox`` as a tuple (minx, miny, maxx, maxy). """ if 'bbox' not in self.params or self.params['bbox'] is None: return None points = map(float, self.params['bbox'].split(',')) return tuple(points) def _set_bbox(self, value): if value is not None and not isinstance(value, string_type): value = ','.join(str(x) for x in value) self['bbox'] = value bbox = property(_get_bbox, _set_bbox) del _get_bbox del _set_bbox def _get_size(self): """ Size of the request in pixel as a tuple (width, height), or None if one is missing. """ if 'height' not in self or 'width' not in self: return None width = int(self.params['width']) height = int(self.params['height']) return (width, height) def _set_size(self, value): self['width'] = str(value[0]) self['height'] = str(value[1]) size = property(_get_size, _set_size) del _get_size del _set_size def _get_srs(self): return self.params.get('srs', None) def _set_srs(self, srs): if hasattr(srs, 'srs_code'): self.params['srs'] = srs.srs_code else: self.params['srs'] = srs srs = property(_get_srs, _set_srs) del _get_srs del _set_srs def _get_transparent(self): """ ``True`` if transparent is set to true, otherwise ``False``. """ if self.get('transparent', 'false').lower() == 'true': return True return False def _set_transparent(self, transparent): self.params['transparent'] = str(transparent).lower() transparent = property(_get_transparent, _set_transparent) del _get_transparent del _set_transparent @property def bgcolor(self): """ The background color in PIL format (#rrggbb). Defaults to '#ffffff'. """ color = self.get('bgcolor', '0xffffff') return '#'+color[2:] def _get_format(self): """ The requested format as string (w/o any 'image/', 'text/', etc prefixes) """ _mime_class, format, options = split_mime_type(self.get('format', default='')) return format def _set_format(self, format): if '/' not in format: format = 'image/' + format self['format'] = format format = property(_get_format, _set_format) del _get_format del _set_format @property def format_mime_type(self): return self.get('format') def __repr__(self): return '%s(param=%r)' % (self.__class__.__name__, self.params) class WMSRequest(BaseRequest): request_params = RequestParams request_handler_name = None fixed_params = {} expected_param = [] non_strict_params = set() #pylint: disable-msg=E1102 xml_exception_handler = None def __init__(self, param=None, url='', validate=False, non_strict=False, **kw): self.non_strict = non_strict BaseRequest.__init__(self, param=param, url=url, validate=validate, **kw) self.adapt_to_111() def adapt_to_111(self): pass def adapt_params_to_version(self): params = self.params.copy() for key, value in iteritems(self.fixed_params): params[key] = value if 'styles' not in params: params['styles'] = '' return params @property def query_string(self): return self.adapt_params_to_version().query_string class WMSMapRequest(WMSRequest): """ Base class for all WMS GetMap requests. :ivar requests: the ``RequestParams`` class for this request :ivar request_handler_name: the name of the server handler :ivar fixed_params: parameters that are fixed for a request :ivar expected_param: required parameters, used for validating """ request_params = WMSMapRequestParams request_handler_name = 'map' fixed_params = {'request': 'GetMap', 'service': 'WMS'} expected_param = ['version', 'request', 'layers', 'styles', 'srs', 'bbox', 'width', 'height', 'format'] #pylint: disable-msg=E1102 xml_exception_handler = None prevent_image_exception = False def __init__(self, param=None, url='', validate=False, non_strict=False, **kw): WMSRequest.__init__(self, param=param, url=url, validate=validate, non_strict=non_strict, **kw) def validate(self): self.validate_param() self.validate_bbox() self.validate_styles() def validate_param(self): missing_param = [] for param in self.expected_param: if self.non_strict and param in self.non_strict_params: continue if param not in self.params: missing_param.append(param) if missing_param: if 'format' in missing_param: self.params['format'] = 'image/png' raise RequestError('missing parameters ' + str(missing_param), request=self) def validate_bbox(self): x0, y0, x1, y1 = self.params.bbox if x0 >= x1 or y0 >= y1: raise RequestError('invalid bbox ' + self.params.get('bbox', None), request=self) def validate_format(self, image_formats): format = self.params['format'] if format not in image_formats: format = self.params['format'] self.params['format'] = 'image/png' raise RequestError('unsupported image format: ' + format, code='InvalidFormat', request=self) def validate_srs(self, srs): if self.params['srs'].upper() not in srs: raise RequestError('unsupported srs: ' + self.params['srs'], code='InvalidSRS', request=self) def validate_styles(self): if 'styles' in self.params: styles = self.params['styles'] if not set(styles.split(',')).issubset(set(['default', '', 'inspire_common:DEFAULT'])): raise RequestError('unsupported styles: ' + self.params['styles'], code='StyleNotDefined', request=self) @property def exception_handler(self): if self.prevent_image_exception: return self.xml_exception_handler() if 'exceptions' in self.params: if 'image' in self.params['exceptions'].lower(): return exception.WMSImageExceptionHandler() elif 'blank' in self.params['exceptions'].lower(): return exception.WMSBlankExceptionHandler() return self.xml_exception_handler() def copy(self): return self.__class__(param=self.params.copy(), url=self.url) class Version(object): _versions = {} def __new__(cls, version): if version in cls._versions: return cls._versions[version] version_obj = object.__new__(cls) version_obj.__init__(version) cls._versions[version] = version_obj return version_obj def __init__(self, version): self.parts = tuple(int(x) for x in version.split('.')) def __lt__(self, other): if not isinstance(other, Version): return NotImplemented return self.parts < other.parts def __ge__(self, other): if not isinstance(other, Version): return NotImplemented return self.parts >= other.parts def __repr__(self): return "Version('%s')" % ('.'.join(str(part) for part in self.parts),) class WMS100MapRequest(WMSMapRequest): version = Version('1.0.0') xml_exception_handler = exception.WMS100ExceptionHandler fixed_params = {'request': 'map', 'wmtver': '1.0.0'} expected_param = ['wmtver', 'request', 'layers', 'styles', 'srs', 'bbox', 'width', 'height', 'format'] def adapt_to_111(self): del self.params['wmtver'] self.params['version'] = '1.0.0' self.params['request'] = 'GetMap' def adapt_params_to_version(self): params = WMSMapRequest.adapt_params_to_version(self) del params['version'] del params['service'] image_format = params['format'] if '/' in image_format: params['format'] = image_format.split('/', 1)[1].upper() return params def validate_format(self, image_formats): format = self.params['format'] image_formats100 = [f.split('/', 1)[1].upper() for f in image_formats] if format in image_formats100: format = 'image/' + format.lower() self.params['format'] = format if format not in image_formats: format = self.params['format'] self.params['format'] = 'image/png' raise RequestError('unsupported image format: ' + format, code='InvalidFormat', request=self) class WMS110MapRequest(WMSMapRequest): version = Version('1.1.0') fixed_params = {'request': 'GetMap', 'version': '1.1.0', 'service': 'WMS'} xml_exception_handler = exception.WMS110ExceptionHandler def adapt_to_111(self): del self.params['wmtver'] class WMS111MapRequest(WMSMapRequest): version = Version('1.1.1') fixed_params = {'request': 'GetMap', 'version': '1.1.1', 'service': 'WMS'} xml_exception_handler = exception.WMS111ExceptionHandler def adapt_to_111(self): del self.params['wmtver'] def switch_bbox_epsg_axis_order(bbox, srs): if bbox is not None and srs is not None: try: if SRS(srs).is_axis_order_ne: return bbox[1], bbox[0], bbox[3], bbox[2] except RuntimeError: log.warn('unknown SRS %s' % srs) return bbox def _switch_bbox(self): self.bbox = switch_bbox_epsg_axis_order(self.bbox, self.srs) class WMS130MapRequestParams(WMSMapRequestParams): """ RequestParams for WMS 1.3.0 GetMap requests. Handles bbox axis-order. """ switch_bbox = _switch_bbox class WMS130MapRequest(WMSMapRequest): version = Version('1.3.0') request_params = WMS130MapRequestParams xml_exception_handler = exception.WMS130ExceptionHandler fixed_params = {'request': 'GetMap', 'version': '1.3.0', 'service': 'WMS'} expected_param = ['version', 'request', 'layers', 'styles', 'crs', 'bbox', 'width', 'height', 'format'] def adapt_to_111(self): del self.params['wmtver'] if 'crs' in self.params: self.params['srs'] = self.params['crs'] del self.params['crs'] self.params.switch_bbox() def adapt_params_to_version(self): params = WMSMapRequest.adapt_params_to_version(self) params.switch_bbox() if 'srs' in params: params['crs'] = params['srs'] del params['srs'] return params def validate_srs(self, srs): # its called crs in 1.3.0 and we validate before adapt_to_111 if self.params['srs'].upper() not in srs: raise RequestError('unsupported crs: ' + self.params['srs'], code='InvalidCRS', request=self) def copy_with_request_params(self, req): new_req = WMSMapRequest.copy_with_request_params(self, req) new_req.params.switch_bbox() return new_req class WMSLegendGraphicRequestParams(WMSMapRequestParams): """ RequestParams for WMS GetLegendGraphic requests. """ def _set_layer(self, value): self.params['layer'] = value def _get_layer(self): """ Layer for which to produce legend graphic. """ return self.params.get('layer') layer = property(_get_layer, _set_layer) del _set_layer del _get_layer @property def sld_version(self): """ Specification version for SLD-specification """ return self.params.get('sld_version') def _set_scale(self, value): self.params['scale'] = value def _get_scale(self): if self.params.get('scale') is not None: return float(self['scale']) return None scale = property(_get_scale,_set_scale) del _set_scale del _get_scale class WMSFeatureInfoRequestParams(WMSMapRequestParams): """ RequestParams for WMS GetFeatureInfo requests. """ @property def query_layers(self): """ List with all query_layers. """ return sum((layers.split(',') for layers in self.params.get_all('query_layers')), []) def _get_pos(self): """x, y query image coordinates (in pixel)""" if '.' in self['x'] or '.' in self['y']: return float(self['x']), float(self['y']) return int(self['x']), int(self['y']) def _set_pos(self, value): self['x'] = str(value[0]) self['y'] = str(value[1]) pos = property(_get_pos, _set_pos) del _get_pos del _set_pos @property def pos_coords(self): """x, y query coordinates (in request SRS)""" width, height = self.size bbox = self.bbox return make_lin_transf((0, 0, width, height), bbox)(self.pos) class WMS130FeatureInfoRequestParams(WMSFeatureInfoRequestParams): switch_bbox = _switch_bbox class WMSLegendGraphicRequest(WMSMapRequest): request_params = WMSLegendGraphicRequestParams request_handler_name = 'legendgraphic' non_strict_params = set(['sld_version', 'scale']) fixed_params = {'request': 'GetLegendGraphic', 'service': 'WMS', 'sld_version': '1.1.0'} expected_param = ['version', 'request', 'layer', 'format', 'sld_version'] def validate(self): self.validate_param() self.validate_sld_version() def validate_sld_version(self): if self.params.get('sld_version', '1.1.0') != '1.1.0': raise RequestError('invalid sld_version ' + self.params.get('sld_version'), request=self) class WMS111LegendGraphicRequest(WMSLegendGraphicRequest): version = Version('1.1.1') fixed_params = WMSLegendGraphicRequest.fixed_params.copy() fixed_params['version'] = '1.1.1' xml_exception_handler = exception.WMS111ExceptionHandler class WMS130LegendGraphicRequest(WMSLegendGraphicRequest): version = Version('1.3.0') fixed_params = WMSLegendGraphicRequest.fixed_params.copy() fixed_params['version'] = '1.3.0' xml_exception_handler = exception.WMS130ExceptionHandler class WMSFeatureInfoRequest(WMSMapRequest): non_strict_params = set(['format', 'styles']) def validate_format(self, image_formats): if self.non_strict: return WMSMapRequest.validate_format(self, image_formats) class WMS111FeatureInfoRequest(WMSFeatureInfoRequest): version = Version('1.1.1') request_params = WMSFeatureInfoRequestParams xml_exception_handler = exception.WMS111ExceptionHandler request_handler_name = 'featureinfo' fixed_params = WMS111MapRequest.fixed_params.copy() fixed_params['request'] = 'GetFeatureInfo' expected_param = WMSMapRequest.expected_param[:] + ['query_layers', 'x', 'y'] class WMS110FeatureInfoRequest(WMSFeatureInfoRequest): version = Version('1.1.0') request_params = WMSFeatureInfoRequestParams xml_exception_handler = exception.WMS110ExceptionHandler request_handler_name = 'featureinfo' fixed_params = WMS110MapRequest.fixed_params.copy() fixed_params['request'] = 'GetFeatureInfo' expected_param = WMSMapRequest.expected_param[:] + ['query_layers', 'x', 'y'] class WMS100FeatureInfoRequest(WMSFeatureInfoRequest): version = Version('1.0.0') request_params = WMSFeatureInfoRequestParams xml_exception_handler = exception.WMS100ExceptionHandler request_handler_name = 'featureinfo' fixed_params = WMS100MapRequest.fixed_params.copy() fixed_params['request'] = 'feature_info' expected_param = WMS100MapRequest.expected_param[:] + ['query_layers', 'x', 'y'] def adapt_to_111(self): del self.params['wmtver'] def adapt_params_to_version(self): params = WMSMapRequest.adapt_params_to_version(self) del params['version'] return params class WMS130FeatureInfoRequest(WMS130MapRequest): # XXX: this class inherits from WMS130MapRequest to reuse # the axis order stuff version = Version('1.3.0') request_params = WMS130FeatureInfoRequestParams xml_exception_handler = exception.WMS130ExceptionHandler request_handler_name = 'featureinfo' fixed_params = WMS130MapRequest.fixed_params.copy() fixed_params['request'] = 'GetFeatureInfo' expected_param = WMS130MapRequest.expected_param[:] + ['query_layers', 'i', 'j'] non_strict_params = set(['format', 'styles']) def adapt_to_111(self): WMS130MapRequest.adapt_to_111(self) # only set x,y when present, # avoids empty values for request templates if 'i' in self.params: self.params['x'] = self.params['i'] if 'j' in self.params: self.params['y'] = self.params['j'] del self.params['i'] del self.params['j'] def adapt_params_to_version(self): params = WMS130MapRequest.adapt_params_to_version(self) params['i'] = self.params['x'] params['j'] = self.params['y'] del params['x'] del params['y'] return params def validate_format(self, image_formats): if self.non_strict: return WMSMapRequest.validate_format(self, image_formats) class WMSCapabilitiesRequest(WMSRequest): request_handler_name = 'capabilities' exception_handler = None mime_type = 'text/xml' fixed_params = {} def __init__(self, param=None, url='', validate=False, non_strict=False, **kw): WMSRequest.__init__(self, param=param, url=url, validate=validate, **kw) def adapt_to_111(self): pass def validate(self): pass class WMS100CapabilitiesRequest(WMSCapabilitiesRequest): version = Version('1.0.0') capabilities_template = 'wms100capabilities.xml' fixed_params = {'request': 'capabilities', 'wmtver': '1.0.0'} @property def exception_handler(self): return exception.WMS100ExceptionHandler() class WMS110CapabilitiesRequest(WMSCapabilitiesRequest): version = Version('1.1.0') capabilities_template = 'wms110capabilities.xml' mime_type = 'application/vnd.ogc.wms_xml' fixed_params = {'request': 'GetCapabilities', 'version': '1.1.0', 'service': 'WMS'} @property def exception_handler(self): return exception.WMS110ExceptionHandler() class WMS111CapabilitiesRequest(WMSCapabilitiesRequest): version = Version('1.1.1') capabilities_template = 'wms111capabilities.xml' mime_type = 'application/vnd.ogc.wms_xml' fixed_params = {'request': 'GetCapabilities', 'version': '1.1.1', 'service': 'WMS'} @property def exception_handler(self): return exception.WMS111ExceptionHandler() class WMS130CapabilitiesRequest(WMSCapabilitiesRequest): version = Version('1.3.0') capabilities_template = 'wms130capabilities.xml' fixed_params = {'request': 'GetCapabilities', 'version': '1.3.0', 'service': 'WMS'} @property def exception_handler(self): return exception.WMS130ExceptionHandler() request_mapping = {Version('1.0.0'): {'featureinfo': WMS100FeatureInfoRequest, 'map': WMS100MapRequest, 'capabilities': WMS100CapabilitiesRequest}, Version('1.1.0'): {'featureinfo': WMS110FeatureInfoRequest, 'map': WMS110MapRequest, 'capabilities': WMS110CapabilitiesRequest}, Version('1.1.1'): {'featureinfo': WMS111FeatureInfoRequest, 'map': WMS111MapRequest, 'capabilities': WMS111CapabilitiesRequest, 'legendgraphic': WMS111LegendGraphicRequest}, Version('1.3.0'): {'featureinfo': WMS130FeatureInfoRequest, 'map': WMS130MapRequest, 'capabilities': WMS130CapabilitiesRequest, 'legendgraphic': WMS130LegendGraphicRequest}, } def _parse_version(req): if 'version' in req.args: return Version(req.args['version']) if 'wmtver' in req.args: return Version(req.args['wmtver']) return Version('1.1.1') # default def _parse_request_type(req): if 'request' in req.args: request_type = req.args['request'].lower() if request_type in ('getmap', 'map'): return 'map' elif request_type in ('getfeatureinfo', 'feature_info'): return 'featureinfo' elif request_type in ('getcapabilities', 'capabilities'): return 'capabilities' elif request_type in ('getlegendgraphic',): return 'legendgraphic' else: return request_type else: return None def negotiate_version(version, supported_versions=None): """ >>> negotiate_version(Version('0.9.0')) Version('1.0.0') >>> negotiate_version(Version('2.0.0')) Version('1.3.0') >>> negotiate_version(Version('1.1.1')) Version('1.1.1') >>> negotiate_version(Version('1.1.0')) Version('1.1.0') >>> negotiate_version(Version('1.1.0'), [Version('1.0.0')]) Version('1.0.0') >>> negotiate_version(Version('1.3.0'), sorted([Version('1.1.0'), Version('1.1.1')])) Version('1.1.1') """ if not supported_versions: supported_versions = list(request_mapping.keys()) supported_versions.sort() if version < supported_versions[0]: return supported_versions[0] # smallest version we support if version > supported_versions[-1]: return supported_versions[-1] # highest version we support while True: next_highest_version = supported_versions.pop() if version >= next_highest_version: return next_highest_version def wms_request(req, validate=True, strict=True, versions=None): version = _parse_version(req) req_type = _parse_request_type(req) if versions and version not in versions: version_requests = None else: version_requests = request_mapping.get(version, None) if version_requests is None: negotiated_version = negotiate_version(version, supported_versions=versions) version_requests = request_mapping[negotiated_version] req_class = version_requests.get(req_type, None) if req_class is None: # use map request to get an exception handler for the requested version dummy_req = version_requests['map'](param=req.args, url=req.base_url, validate=False) raise RequestError("unknown WMS request type '%s'" % req_type, request=dummy_req) return req_class(param=req.args, url=req.base_url, validate=True, non_strict=not strict, http=req) def create_request(req_data, param, req_type='map', version='1.1.1', abspath=None): url = req_data['url'] req_data = req_data.copy() del req_data['url'] if 'request_format' in param: req_data['format'] = param['request_format'] elif 'format' in param: req_data['format'] = param['format'] if 'info_format' in param: req_data['info_format'] = param['info_format'] if 'transparent' in req_data: # we don't want a boolean req_data['transparent'] = str(req_data['transparent']) if req_data.get('sld', '').startswith('file://'): sld_path = req_data['sld'][len('file://'):] if abspath: sld_path = abspath(sld_path) with codecs.open(sld_path, 'r', 'utf-8') as f: req_data['sld_body'] = f.read() del req_data['sld'] return request_mapping[Version(version)][req_type](url=url, param=req_data) info_formats = { Version('1.3.0'): (('text', 'text/plain'), ('html', 'text/html'), ('xml', 'text/xml'), ('json', 'application/json'), ), None: (('text', 'text/plain'), ('html', 'text/html'), ('xml', 'application/vnd.ogc.gml'), ('json', 'application/json'), ) } def infotype_from_mimetype(version, mime_type): if version in info_formats: formats = info_formats[version] else: formats = info_formats[None] # default for t, m in formats: if m == mime_type: return t def mimetype_from_infotype(version, info_type): if version in info_formats: formats = info_formats[version] else: formats = info_formats[None] # default for t, m in formats: if t == info_type: return m return 'text/plain' mapproxy-1.11.0/mapproxy/request/wms/exception.py000066400000000000000000000066311320454472400222110ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Service exception handling (WMS exceptions, XML, in_image, etc.). """ from mapproxy.exception import ExceptionHandler, XMLExceptionHandler from mapproxy.response import Response from mapproxy.image.message import message_image from mapproxy.image.opts import ImageOptions import mapproxy.service from mapproxy.template import template_loader get_template = template_loader(mapproxy.service.__name__, 'templates') class WMSXMLExceptionHandler(XMLExceptionHandler): template_func = get_template class WMS100ExceptionHandler(WMSXMLExceptionHandler): """ Exception handler for OGC WMS 1.0.0 ServiceExceptionReports """ template_file = 'wms100exception.xml' content_type = 'text/xml' class WMS110ExceptionHandler(WMSXMLExceptionHandler): """ Exception handler for OGC WMS 1.1.0 ServiceExceptionReports """ template_file = 'wms110exception.xml' mimetype = 'application/vnd.ogc.se_xml' class WMS111ExceptionHandler(WMSXMLExceptionHandler): """ Exception handler for OGC WMS 1.1.1 ServiceExceptionReports """ template_file = 'wms111exception.xml' mimetype = 'application/vnd.ogc.se_xml' class WMS130ExceptionHandler(WMSXMLExceptionHandler): """ Exception handler for OGC WMS 1.3.0 ServiceExceptionReports """ template_file = 'wms130exception.xml' mimetype = 'text/xml' class WMSImageExceptionHandler(ExceptionHandler): """ Exception handler for image exceptions. """ def render(self, request_error): request = request_error.request params = request.params format = params.format size = params.size if size is None: size = (256, 256) transparent = ('transparent' in params and params['transparent'].lower() == 'true') bgcolor = WMSImageExceptionHandler._bgcolor(request.params) image_opts = ImageOptions(format=format, bgcolor=bgcolor, transparent=transparent) result = message_image(request_error.msg, size=size, image_opts=image_opts) return Response(result.as_buffer(), content_type=params.format_mime_type) @staticmethod def _bgcolor(params): """ >>> WMSImageExceptionHandler._bgcolor({'bgcolor': '0Xf0ea42'}) '#f0ea42' >>> WMSImageExceptionHandler._bgcolor({}) '#ffffff' """ if 'bgcolor' in params: color = params['bgcolor'] if color.lower().startswith('0x'): color = '#' + color[2:] else: color = '#ffffff' return color class WMSBlankExceptionHandler(WMSImageExceptionHandler): """ Exception handler for blank image exceptions. """ def render(self, request_error): request_error.msg = '' return WMSImageExceptionHandler.render(self, request_error) mapproxy-1.11.0/mapproxy/request/wmts.py000066400000000000000000000310441320454472400203730ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Service requests (parsing, handling, etc). """ import re from mapproxy.exception import RequestError from mapproxy.request.base import RequestParams, BaseRequest, split_mime_type from mapproxy.request.tile import TileRequest from mapproxy.exception import XMLExceptionHandler from mapproxy.template import template_loader import mapproxy.service get_template = template_loader(mapproxy.service.__name__, 'templates') class WMTS100ExceptionHandler(XMLExceptionHandler): template_func = get_template template_file = 'wmts100exception.xml' content_type = 'text/xml' status_codes = { None: 500, 'TileOutOfRange': 400, 'MissingParameterValue': 400, 'InvalidParameterValue': 400, 'OperationNotSupported': 501 } class WMTSTileRequestParams(RequestParams): """ This class represents key-value parameters for WMTS map requests. All values can be accessed as a property. Some properties return processed values. ``size`` returns a tuple of the width and height, ``layers`` returns an iterator of all layers, etc. """ @property def layer(self): """ List with all layer names. """ return self['layer'] def _get_coord(self): x = int(self['tilecol']) y = int(self['tilerow']) z = self['tilematrix'] return x, y, z def _set_coord(self, value): x, y, z = value self['tilecol'] = x self['tilerow'] = y self['tilematrix'] = z coord = property(_get_coord, _set_coord) del _get_coord del _set_coord def _get_format(self): """ The requested format as string (w/o any 'image/', 'text/', etc prefixes) """ _mime_class, format, options = split_mime_type(self.get('format', default='')) return format def _set_format(self, format): if '/' not in format: format = 'image/' + format self['format'] = format format = property(_get_format, _set_format) del _get_format del _set_format @property def format_mime_type(self): return self.get('format') @property def dimensions(self): expected_param = set(['version', 'request', 'layer', 'style', 'tilematrixset', 'tilematrix', 'tilerow', 'tilecol', 'format', 'service']) dimensions = {} for key, value in self.iteritems(): if key not in expected_param: dimensions[key.lower()] = value return dimensions def __repr__(self): return '%s(param=%r)' % (self.__class__.__name__, self.params) class WMTSRequest(BaseRequest): request_params = WMTSTileRequestParams request_handler_name = None fixed_params = {} expected_param = [] non_strict_params = set() #pylint: disable=E1102 xml_exception_handler = None def __init__(self, param=None, url='', validate=False, non_strict=False, **kw): self.non_strict = non_strict BaseRequest.__init__(self, param=param, url=url, validate=validate, **kw) def validate(self): pass @property def query_string(self): return self.params.query_string class WMTS100TileRequest(WMTSRequest): """ Base class for all WMTS GetTile requests. :ivar requests: the ``RequestParams`` class for this request :ivar request_handler_name: the name of the server handler :ivar fixed_params: parameters that are fixed for a request :ivar expected_param: required parameters, used for validating """ request_params = WMTSTileRequestParams request_handler_name = 'tile' fixed_params = {'request': 'GetTile', 'version': '1.0.0', 'service': 'WMTS'} xml_exception_handler = WMTS100ExceptionHandler expected_param = ['version', 'request', 'layer', 'style', 'tilematrixset', 'tilematrix', 'tilerow', 'tilecol', 'format'] #pylint: disable=E1102 def __init__(self, param=None, url='', validate=False, non_strict=False, **kw): WMTSRequest.__init__(self, param=param, url=url, validate=validate, non_strict=non_strict, **kw) def make_tile_request(self): self.layer = self.params.layer self.tilematrixset = self.params.tilematrixset self.format = self.params.format # TODO self.tile = (int(self.params.coord[0]), int(self.params.coord[1]), int(self.params.coord[2])) self.origin = 'nw' self.dimensions = self.params.dimensions def validate(self): missing_param = [] for param in self.expected_param: if self.non_strict and param in self.non_strict_params: continue if param not in self.params: missing_param.append(param) if missing_param: if 'format' in missing_param: self.params['format'] = 'image/png' raise RequestError('missing parameters ' + str(missing_param), request=self) self.validate_styles() def validate_styles(self): if 'styles' in self.params: styles = self.params['styles'] if styles.replace(',', '').strip() != '': raise RequestError('unsupported styles: ' + self.params['styles'], code='StyleNotDefined', request=self) @property def exception_handler(self): return self.xml_exception_handler() def copy(self): return self.__class__(param=self.params.copy(), url=self.url) class WMTSFeatureInfoRequestParams(WMTSTileRequestParams): """ RequestParams for WMTS GetFeatureInfo requests. """ def _get_pos(self): """x, y query image coordinates (in pixel)""" return int(self['i']), int(self['j']) def _set_pos(self, value): self['i'] = str(int(round(value[0]))) self['j'] = str(int(round(value[1]))) pos = property(_get_pos, _set_pos) del _get_pos del _set_pos class WMTS100FeatureInfoRequest(WMTS100TileRequest): request_params = WMTSFeatureInfoRequestParams request_handler_name = 'featureinfo' fixed_params = WMTS100TileRequest.fixed_params.copy() fixed_params['request'] = 'GetFeatureInfo' expected_param = WMTS100TileRequest.expected_param[:] + ['infoformat', 'i', 'j'] non_strict_params = set(['format', 'styles']) class WMTS100CapabilitiesRequest(WMTSRequest): request_handler_name = 'capabilities' capabilities_template = 'wmts100capabilities.xml' exception_handler = None mime_type = 'text/xml' fixed_params = {} def __init__(self, param=None, url='', validate=False, non_strict=False, **kw): WMTSRequest.__init__(self, param=param, url=url, validate=validate, **kw) request_mapping = { 'featureinfo': WMTS100FeatureInfoRequest, 'tile': WMTS100TileRequest, 'capabilities': WMTS100CapabilitiesRequest } def _parse_request_type(req): if 'request' in req.args: request_type = req.args['request'].lower() if request_type.startswith('get'): request_type = request_type[3:] if request_type in ('tile', 'featureinfo', 'capabilities'): return request_type return None def wmts_request(req, validate=True): req_type = _parse_request_type(req) req_class = request_mapping.get(req_type, None) if req_class is None: # use map request to get an exception handler for the requested version dummy_req = request_mapping['tile'](param=req.args, url=req.base_url, validate=False) raise RequestError("unknown WMTS request type '%s'" % req_type, request=dummy_req) return req_class(param=req.args, url=req.base_url, validate=True, http=req) def create_request(req_data, param, req_type='tile'): url = req_data['url'] req_data = req_data.copy() del req_data['url'] if 'request_format' in param: req_data['format'] = param['request_format'] elif 'format' in param: req_data['format'] = param['format'] # req_data['bbox'] = param['bbox'] # if isinstance(req_data['bbox'], types.ListType): # req_data['bbox'] = ','.join(str(x) for x in req_data['bbox']) # req_data['srs'] = param['srs'] return request_mapping[req_type](url=url, param=req_data) class InvalidWMTSTemplate(Exception): pass class URLTemplateConverter(object): var_re = re.compile(r'(?:\\{)?\\{(\w+)\\}(?:\\})?') # TODO {{}} format is deprecated, change to this in 1.6 # var_re = re.compile(r'\\{(\w+)\\}') variables = { 'TileMatrixSet': r'[\w_.:-]+', 'TileMatrix': r'\d+', 'TileRow': r'-?\d+', 'TileCol': r'-?\d+', 'Style': r'[\w_.:-]+', 'Layer': r'[\w_.:-]+', 'Format': r'\w+' } required = set(['TileCol', 'TileRow', 'TileMatrix', 'TileMatrixSet', 'Layer']) def __init__(self, template): self.template = template self.found = set() self.dimensions = [] self._regexp = None self.regexp() def substitute_var(self, match): var = match.group(1) if var in self.variables: var_type_re = self.variables[var] else: self.dimensions.append(var) var = var.lower() var_type_re = r'[\w_.,:-]+' self.found.add(var) return r'(?P<%s>%s)' % (var, var_type_re) def regexp(self): if self._regexp: return self._regexp converted_re = self.var_re.sub(self.substitute_var, re.escape(self.template)) wmts_re = re.compile(converted_re) if not self.found.issuperset(self.required): raise InvalidWMTSTemplate('missing required variables in WMTS restful template: %s' % self.required.difference(self.found)) self._regexp = wmts_re return wmts_re class WMTS100RestTileRequest(TileRequest): """ Class for TMS-like KML requests. """ xml_exception_handler = WMTS100ExceptionHandler request_handler_name = 'tile' origin = 'nw' url_converter = None def __init__(self, request): self.http = request self.url = request.base_url self.dimensions = {} def make_tile_request(self): """ Initialize tile request. Sets ``tile`` and ``layer`` and ``format``. :raise RequestError: if the format does not match the URL template`` """ match = self.tile_req_re.search(self.http.path) if not match: raise RequestError('invalid request (%s)' % (self.http.path), request=self) req_vars = match.groupdict() self.layer = req_vars['Layer'] self.tile = int(req_vars['TileCol']), int(req_vars['TileRow']), int(req_vars['TileMatrix']) self.format = req_vars.get('Format') self.tilematrixset = req_vars['TileMatrixSet'] if self.url_converter and self.url_converter.dimensions: for dim in self.url_converter.dimensions: self.dimensions[dim.lower()] = req_vars[dim.lower()] @property def exception_handler(self): return self.xml_exception_handler() RESTFUL_CAPABILITIES_PATH = '/1.0.0/WMTSCapabilities.xml' class WMTS100RestCapabilitiesRequest(object): """ Class for RESTful WMTS capabilities requests. """ xml_exception_handler = WMTS100ExceptionHandler request_handler_name = 'capabilities' capabilities_template = 'wmts100capabilities.xml' def __init__(self, request): self.http = request self.url = request.base_url[:-len(RESTFUL_CAPABILITIES_PATH)] @property def exception_handler(self): return self.xml_exception_handler() def make_wmts_rest_request_parser(url_converter_): class WMTSRequestWrapper(WMTS100RestTileRequest): url_converter = url_converter_ tile_req_re = url_converter.regexp() def wmts_request(req): if req.path.endswith(RESTFUL_CAPABILITIES_PATH): return WMTS100RestCapabilitiesRequest(req) return WMTSRequestWrapper(req) return wmts_request mapproxy-1.11.0/mapproxy/response.py000066400000000000000000000170541320454472400175540ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Service responses. """ import hashlib from mapproxy.util.times import format_httpdate, parse_httpdate, timestamp from mapproxy.compat import PY2, text_type, iteritems class Response(object): charset = 'utf-8' default_content_type = 'text/plain' block_size = 1024 * 32 def __init__(self, response, status=None, content_type=None, mimetype=None): self.response = response if status is None: status = 200 self.status = status self._timestamp = None self.headers = {} if mimetype: if mimetype.startswith('text/'): content_type = mimetype + '; charset=' + self.charset else: content_type = mimetype if content_type is None: content_type = self.default_content_type self.headers['Content-type'] = content_type def _status_set(self, status): if isinstance(status, int): status = status_code(status) self._status = status def _status_get(self): return self._status status = property(_status_get, _status_set) def _last_modified_set(self, date): if not date: return self._timestamp = timestamp(date) self.headers['Last-modified'] = format_httpdate(self._timestamp) def _last_modified_get(self): return self.headers.get('Last-modified', None) last_modified = property(_last_modified_get, _last_modified_set) def _etag_set(self, value): self.headers['ETag'] = value def _etag_get(self): return self.headers.get('ETag', None) etag = property(_etag_get, _etag_set) def cache_headers(self, timestamp=None, etag_data=None, max_age=None, no_cache=False): """ Set cache-related headers. :param timestamp: local timestamp of the last modification of the response content :param etag_data: list that will be used to build an ETag hash. calls the str function on each item. :param max_age: the maximum cache age in seconds """ if etag_data: hash_src = ''.join((str(x) for x in etag_data)).encode('ascii') self.etag = hashlib.md5(hash_src).hexdigest() if no_cache: assert not timestamp and not max_age self.headers['Cache-Control'] = 'no-cache, no-store' self.headers['Pragma'] = 'no-cache' self.headers['Expires'] = '-1' self.last_modified = timestamp if (timestamp or etag_data) and max_age is not None: self.headers['Cache-control'] = 'public, max-age=%d, s-maxage=%d' % (max_age, max_age) def make_conditional(self, req): """ Make the response conditional to the HTTP headers in the CGI/WSGI `environ`. Checks for ``If-none-match`` and ``If-modified-since`` headers and compares to the etag and timestamp of this response. If the content was not modified the repsonse will changed to HTTP 304 Not Modified. """ if req is None: return environ = req.environ not_modified = False if self.etag == environ.get('HTTP_IF_NONE_MATCH', -1): not_modified = True elif self._timestamp is not None: date = environ.get('HTTP_IF_MODIFIED_SINCE', None) timestamp = parse_httpdate(date) if timestamp is not None and self._timestamp <= timestamp: not_modified = True if not_modified: self.status = 304 self.response = [] if 'Content-type' in self.headers: del self.headers['Content-type'] @property def content_length(self): return int(self.headers.get('Content-length', 0)) @property def content_type(self): return self.headers['Content-type'] @property def data(self): if hasattr(self.response, 'read'): return self.response.read() else: return b''.join(chunk.encode() for chunk in self.response) @property def fixed_headers(self): headers = [] for key, value in iteritems(self.headers): if PY2 and isinstance(value, unicode): value = value.encode('utf-8') headers.append((key, value)) return headers def __call__(self, environ, start_response): if hasattr(self.response, 'read'): if ((not hasattr(self.response, 'ok_to_seek') or self.response.ok_to_seek) and (hasattr(self.response, 'seek') and hasattr(self.response, 'tell'))): self.response.seek(0, 2) # to EOF self.headers['Content-length'] = str(self.response.tell()) self.response.seek(0) if 'wsgi.file_wrapper' in environ: resp_iter = environ['wsgi.file_wrapper'](self.response, self.block_size) else: resp_iter = iter(lambda: self.response.read(self.block_size), b'') elif not self.response: resp_iter = iter([]) elif isinstance(self.response, text_type): self.response = self.response.encode(self.charset) self.headers['Content-length'] = str(len(self.response)) resp_iter = iter([self.response]) elif isinstance(self.response, bytes): self.headers['Content-length'] = str(len(self.response)) resp_iter = iter([self.response]) else: resp_iter = self.response start_response(self.status, self.fixed_headers) return resp_iter def iter_encode(self, chunks): for chunk in chunks: if isinstance(chunk, text_type): chunk = chunk.encode(self.charset) yield chunk # http://www.faqs.org/rfcs/rfc2616.html _status_codes = { 100: 'Continue', 101: 'Switching Protocols', 200: 'OK', 201: 'Created', 202: 'Accepted', 203: 'Non-Authoritative Information', 204: 'No Content', 205: 'Reset Content', 206: 'Partial Content', 300: 'Multiple Choices', 301: 'Moved Permanently', 302: 'Found', 303: 'See Other', 304: 'Not Modified', 305: 'Use Proxy', 307: 'Temporary Redirect', 400: 'Bad Request', 401: 'Unauthorized', 402: 'Payment Required', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 406: 'Not Acceptable', 407: 'Proxy Authentication Required', 408: 'Request Time-out', 409: 'Conflict', 410: 'Gone', 411: 'Length Required', 412: 'Precondition Failed', 413: 'Request Entity Too Large', 414: 'Request-URI Too Large', 415: 'Unsupported Media Type', 416: 'Requested range not satisfiable', 417: 'Expectation Failed', 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable', 504: 'Gateway Time-out', 505: 'HTTP Version not supported', } def status_code(code): return str(code) + ' ' + _status_codes[code] mapproxy-1.11.0/mapproxy/script/000077500000000000000000000000001320454472400166415ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/script/__init__.py000066400000000000000000000000001320454472400207400ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/script/conf/000077500000000000000000000000001320454472400175665ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/script/conf/__init__.py000066400000000000000000000000001320454472400216650ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/script/conf/app.py000066400000000000000000000153721320454472400207300ustar00rootroot00000000000000# -:- encoding: utf-8 -:- # This file is part of the MapProxy project. # Copyright (C) 2013 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function import codecs import sys import os import optparse import logging import textwrap import datetime import xml.etree.ElementTree import yaml from contextlib import contextmanager from io import BytesIO from .sources import sources from .layers import layers from .caches import caches from .seeds import seeds from .utils import update_config, MapProxyYAMLDumper, download_capabilities from mapproxy.compat import iteritems from mapproxy.config.loader import load_configuration from mapproxy.util.ext.wmsparse import parse_capabilities def setup_logging(level=logging.INFO): mapproxy_log = logging.getLogger('mapproxy') mapproxy_log.setLevel(level) # do not init logging when stdout is captured # eg. when running in tests if isinstance(sys.stdout, BytesIO): return ch = logging.StreamHandler(sys.stdout) ch.setLevel(logging.DEBUG) formatter = logging.Formatter( "[%(asctime)s] %(name)s - %(levelname)s - %(message)s") ch.setFormatter(formatter) mapproxy_log.addHandler(ch) def write_header(f, capabilities): print('# MapProxy configuration automatically generated from:', file=f) print('# %s' % capabilities, file=f) print('#', file=f) print('# NOTE: The generated configuration can be highly inefficient,', file=f) print('# especially when multiple layers and caches are requested at once.', file=f) print('# Make sure you understand the generated configuration!', file=f) print('#', file=f) print('# Created on %s with:' % datetime.datetime.now(), file=f) print(' \\\n'.join(textwrap.wrap(' '.join(sys.argv), initial_indent='# ', subsequent_indent='# ')), file=f) print('', file=f) @contextmanager def file_or_stdout(name): if name == '-': yield codecs.getwriter('utf-8')(sys.stdout) else: with open(name, 'wb') as f: yield codecs.getwriter('utf-8')(f) def config_command(args): parser = optparse.OptionParser("usage: %prog autoconfig [options]") parser.add_option('--capabilities', help="URL or filename of WMS 1.1.1/1.3.0 capabilities document") parser.add_option('--output', help="filename for created MapProxy config [default: -]", default="-") parser.add_option('--output-seed', help="filename for created seeding config") parser.add_option('--base', help='base config to include in created MapProxy config') parser.add_option('--overwrite', help='YAML file with overwrites for the created MapProxy config') parser.add_option('--overwrite-seed', help='YAML file with overwrites for the created seeding config') parser.add_option('--force', default=False, action='store_true', help="overwrite existing files") options, args = parser.parse_args(args) if not options.capabilities: parser.print_help() print("\nERROR: --capabilities required", file=sys.stderr) return 2 if not options.output and not options.output_seed: parser.print_help() print("\nERROR: --output and/or --output-seed required", file=sys.stderr) return 2 if not options.force: if options.output and options.output != '-' and os.path.exists(options.output): print("\nERROR: %s already exists, use --force to overwrite" % options.output, file=sys.stderr) return 2 if options.output_seed and options.output_seed != '-' and os.path.exists(options.output_seed): print("\nERROR: %s already exists, use --force to overwrite" % options.output_seed, file=sys.stderr) return 2 log = logging.getLogger('mapproxy_conf_cmd') log.addHandler(logging.StreamHandler()) setup_logging(logging.WARNING) srs_grids = {} if options.base: base = load_configuration(options.base) for name, grid_conf in iteritems(base.grids): if name.startswith('GLOBAL_'): continue srs_grids[grid_conf.tile_grid().srs.srs_code] = name cap_doc = options.capabilities if cap_doc.startswith(('http://', 'https://')): cap_doc = download_capabilities(options.capabilities).read() else: cap_doc = open(cap_doc, 'rb').read() try: cap = parse_capabilities(BytesIO(cap_doc)) except (xml.etree.ElementTree.ParseError, ValueError) as ex: print(ex, file=sys.stderr) print(cap_doc[:1000] + ('...' if len(cap_doc) > 1000 else ''), file=sys.stderr) return 3 overwrite = None if options.overwrite: with open(options.overwrite, 'rb') as f: overwrite = yaml.load(f) overwrite_seed = None if options.overwrite_seed: with open(options.overwrite_seed, 'rb') as f: overwrite_seed = yaml.load(f) conf = {} if options.base: conf['base'] = os.path.abspath(options.base) conf['services'] = {'wms': {'md': {'title': cap.metadata()['title']}}} if overwrite: conf['services'] = update_config(conf['services'], overwrite.pop('service', {})) conf['sources'] = sources(cap) if overwrite: conf['sources'] = update_config(conf['sources'], overwrite.pop('sources', {})) conf['caches'] = caches(cap, conf['sources'], srs_grids=srs_grids) if overwrite: conf['caches'] = update_config(conf['caches'], overwrite.pop('caches', {})) conf['layers'] = layers(cap, conf['caches']) if overwrite: conf['layers'] = update_config(conf['layers'], overwrite.pop('layers', {})) if overwrite: conf = update_config(conf, overwrite) seed_conf = {} seed_conf['seeds'], seed_conf['cleanups'] = seeds(cap, conf['caches']) if overwrite_seed: seed_conf = update_config(seed_conf, overwrite_seed) if options.output: with file_or_stdout(options.output) as f: write_header(f, options.capabilities) yaml.dump(conf, f, default_flow_style=False, Dumper=MapProxyYAMLDumper) if options.output_seed: with file_or_stdout(options.output_seed) as f: write_header(f, options.capabilities) yaml.dump(seed_conf, f, default_flow_style=False, Dumper=MapProxyYAMLDumper) return 0mapproxy-1.11.0/mapproxy/script/conf/caches.py000066400000000000000000000023701320454472400213700ustar00rootroot00000000000000# -:- encoding: utf-8 -:- # This file is part of the MapProxy project. # Copyright (C) 2013 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.compat import iteritems def caches(cap, sources, srs_grids): caches = {} for name, source in iteritems(sources): conf = for_source(name, source, srs_grids) if not conf: continue caches[name[:-len('_wms')] + '_cache'] = conf return caches def for_source(name, source, srs_grids): cache = { 'sources': [name] } grids = [] for srs in source['supported_srs']: if srs in srs_grids: grids.append(srs_grids[srs]) if not grids: return None cache['grids'] = grids return cache mapproxy-1.11.0/mapproxy/script/conf/layers.py000066400000000000000000000027011320454472400214370ustar00rootroot00000000000000# -:- encoding: utf-8 -:- # This file is part of the MapProxy project. # Copyright (C) 2013 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. def layers(cap, caches): return [_layer(cap.layers(), caches)] def _layer(layer, caches): name, conf = for_layer(layer, caches) child_layers = [] for child_layer in layer['layers']: child_layers.append(_layer(child_layer, caches)) if child_layers: conf['layers'] = child_layers return conf def for_layer(layer, caches): conf = { 'title': layer['title'], } if layer['name']: conf['name'] = layer['name'] if layer['name'] + '_cache' in caches: conf['sources'] = [layer['name'] + '_cache'] else: conf['sources'] = [layer['name'] + '_wms'] md = {} if layer['abstract']: md['abstract'] = layer['abstract'] if md: conf['md'] = md return layer['name'], conf mapproxy-1.11.0/mapproxy/script/conf/seeds.py000066400000000000000000000023451320454472400212470ustar00rootroot00000000000000# -:- encoding: utf-8 -:- # This file is part of the MapProxy project. # Copyright (C) 2013 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.compat import iteritems def seeds(cap, caches): seeds = {} cleanups = {} for cache_name, cache in iteritems(caches): for grid in cache['grids']: seeds[cache_name + '_' + grid] = { 'caches': [cache_name], 'grids': [grid], } cleanups[cache_name + '_' + grid] = { 'caches': [cache_name], 'grids': [grid], 'remove_before': { 'time': '1900-01-01T00:00:00', } } return seeds, cleanupsmapproxy-1.11.0/mapproxy/script/conf/sources.py000066400000000000000000000043371320454472400216320ustar00rootroot00000000000000# -:- encoding: utf-8 -:- # This file is part of the MapProxy project. # Copyright (C) 2013 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.srs import SRS import logging def sources(cap): sources = {} for layer in cap.layers_list(): name, conf = for_layer(cap, layer) sources[name+'_wms'] = conf return sources _checked_srs = {} def check_srs(srs): if srs not in _checked_srs: try: SRS(srs) _checked_srs[srs] = True except Exception as ex: logging.getLogger(__name__).warn('unable to initialize srs for %s: %s', srs, ex) _checked_srs[srs] = False return _checked_srs[srs] def for_layer(cap, layer): source = {'type': 'wms'} req = { 'url': layer['url'], 'layers': layer['name'], } if not layer['opaque']: req['transparent'] = True wms_opts = {} if cap.version != '1.1.1': wms_opts['version'] = cap.version if layer['queryable']: wms_opts['featureinfo'] = True if layer['legend']: wms_opts['legendurl'] = layer['legend']['url'] if wms_opts: source['wms_opts'] = wms_opts source['req'] = req source['supported_srs'] = [] for srs in layer['srs']: if check_srs(srs): source['supported_srs'].append(srs) source['supported_srs'].sort() if layer['llbbox']: source['coverage'] = { 'srs': 'EPSG:4326', 'bbox': layer['llbbox'], } res_hint = layer['res_hint'] if res_hint: if res_hint[0]: source['min_res'] = res_hint[0] if res_hint[1]: source['max_res'] = res_hint[1] return layer['name'], source mapproxy-1.11.0/mapproxy/script/conf/utils.py000066400000000000000000000121651320454472400213050ustar00rootroot00000000000000# -:- encoding: utf-8 -:- # This file is part of the MapProxy project. # Copyright (C) 2013 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from copy import copy from mapproxy.compat import iteritems __all__ = ['update_config', 'MapProxyYAMLDumper'] def update_config(conf, overwrites): wildcard_keys = [] for k, v in iteritems(overwrites): if k == '__all__': continue if k.startswith('___') or k.endswith('___'): wildcard_keys.append(k) continue if k.endswith('__extend__'): k = k[:-len('__extend__')] if k not in conf: conf[k] = v elif isinstance(v, list): conf[k].extend(v) else: raise ValueError('cannot extend non-list:', v) elif k not in conf: conf[k] = copy(v) else: if isinstance(conf[k], dict) and isinstance(v, dict): conf[k] = update_config(conf[k], v) else: conf[k] = copy(v) if '__all__' in overwrites: v = overwrites['__all__'] for conf_k, conf_v in iteritems(conf): if isinstance(conf_v, dict): conf[conf_k] = update_config(conf_v, v) else: conf[conf_k] = v if wildcard_keys: for key in wildcard_keys: v = overwrites[key] if key.startswith('___'): key = key[3:] key_check = lambda x: x.endswith(key) else: key = key[:-3] key_check = lambda x: x.startswith(key) for conf_k, conf_v in iteritems(conf): if not key_check(conf_k): continue if isinstance(conf_v, dict): conf[conf_k] = update_config(conf_v, v) else: conf[conf_k] = v return conf from yaml.serializer import Serializer from yaml.nodes import ScalarNode, SequenceNode, MappingNode from yaml.emitter import Emitter from yaml.representer import SafeRepresenter from yaml.resolver import Resolver class _MixedFlowSortedSerializer(Serializer): def serialize_node(self, node, parent, index): # reset any anchors if parent is None: for k in self.anchors: self.anchors[k] = None self.serialized_nodes = {} if isinstance(node, SequenceNode) and all(isinstance(item, ScalarNode) for item in node.value): node.flow_style = True elif isinstance(node, MappingNode): node.value.sort(key=lambda x: x[0].value) return Serializer.serialize_node(self, node, parent, index) class _EmptyNoneRepresenter(SafeRepresenter): def represent_none(self, data): return self.represent_scalar(u'tag:yaml.org,2002:null', u'') _EmptyNoneRepresenter.add_representer(type(None), _EmptyNoneRepresenter.represent_none) class MapProxyYAMLDumper(Emitter, _MixedFlowSortedSerializer, _EmptyNoneRepresenter, Resolver): """ YAML dumper that uses block style by default, except for node-only sequences. Also sorts dicts by key, prevents `none` for empty entries and prevents any anchors. """ def __init__(self, stream, default_style=None, default_flow_style=False, canonical=None, indent=None, width=None, allow_unicode=None, line_break=None, encoding=None, explicit_start=None, explicit_end=None, version=None, tags=None): Emitter.__init__(self, stream, canonical=canonical, indent=indent, width=width, allow_unicode=allow_unicode, line_break=line_break) Serializer.__init__(self, encoding=encoding, explicit_start=explicit_start, explicit_end=explicit_end, version=version, tags=tags) _EmptyNoneRepresenter.__init__(self, default_style=default_style, default_flow_style=default_flow_style) Resolver.__init__(self) from mapproxy.request.base import BaseRequest, url_decode from mapproxy.client.http import open_url from mapproxy.compat.modules import urlparse def wms_capapilities_url(url): parsed_url = urlparse.urlparse(url) base_req = BaseRequest( url=url.split('?', 1)[0], param=url_decode(parsed_url.query), ) base_req.params['service'] = 'WMS' if not base_req.params['version']: base_req.params['version'] = '1.1.1' base_req.params['request'] = 'GetCapabilities' return base_req.complete_url def download_capabilities(url): capabilities_url = wms_capapilities_url(url) return open_url(capabilities_url) mapproxy-1.11.0/mapproxy/script/defrag.py000066400000000000000000000150401320454472400204430ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2017 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function import glob import optparse import os.path import re import sys from collections import OrderedDict from mapproxy.cache.compact import CompactCacheV1, CompactCacheV2 from mapproxy.cache.tile import Tile from mapproxy.config import local_base_config from mapproxy.config.loader import load_configuration, ConfigurationError import logging log = logging.getLogger('mapproxy.defrag') def defrag_command(args=None): parser = optparse.OptionParser("%prog defrag-compact [options] -f mapproxy_conf") parser.add_option("-f", "--mapproxy-conf", dest="mapproxy_conf", help="MapProxy configuration.") parser.add_option("--min-percent", type=float, default=10.0, help="Only defrag if fragmentation is larger (10 means at least 10% of the file does not have to be used)") parser.add_option("--min-mb", type=float, default=1.0, help="Only defrag if fragmentation is larger (2 means at least 2MB the file does not have to be used)") parser.add_option("--dry-run", "-n", action="store_true", help="Do not de-fragment, only print output") parser.add_option("--caches", dest="cache_names", metavar='cache1,cache2,...', help="only defragment the named caches") from mapproxy.script.util import setup_logging import logging setup_logging(logging.INFO, format="[%(asctime)s] %(msg)s") if args: args = args[1:] # remove script name (options, args) = parser.parse_args(args) if not options.mapproxy_conf: parser.print_help() sys.exit(1) try: proxy_configuration = load_configuration(options.mapproxy_conf) except IOError as e: print('ERROR: ', "%s: '%s'" % (e.strerror, e.filename), file=sys.stderr) sys.exit(2) except ConfigurationError as e: print(e, file=sys.stderr) print('ERROR: invalid configuration (see above)', file=sys.stderr) sys.exit(2) with local_base_config(proxy_configuration.base_config): available_caches = OrderedDict() for name, cache_conf in proxy_configuration.caches.items(): for grid, extent, tile_mgr in cache_conf.caches(): if isinstance(tile_mgr.cache, (CompactCacheV1, CompactCacheV2)): available_caches.setdefault(name, []).append(tile_mgr.cache) if options.cache_names: defrag_caches = options.cache_names.split(',') missing = set(defrag_caches).difference(available_caches.keys()) if missing: print('unknown caches: %s' % (', '.join(missing), )) print('available compact caches: %s' % (', '.join(available_caches.keys()), )) sys.exit(1) else: defrag_caches = None for name, caches in available_caches.items(): if defrag_caches and name not in defrag_caches: continue for cache in caches: logger = DefragLog(name) defrag_compact_cache(cache, min_percent=options.min_percent/100, min_bytes=options.min_mb*1024*1024, dry_run=options.dry_run, log_progress=logger, ) def bundle_offset(fname): """ >>> bundle_offset("path/to/R0000C0000.bundle") (0, 0) >>> bundle_offset("path/to/R0380C1380.bundle") (4992, 896) """ match = re.search('R([A-F0-9]{4,})C([A-F0-9]{4,}).bundle$', fname, re.IGNORECASE) if match: r = int(match.group(1), 16) c = int(match.group(2), 16) return c, r class DefragLog(object): def __init__(self, cache_name): self.cache_name = cache_name def log(self, fname, fragmentation, fragmentation_bytes, num, total, defrag): msg = "%s: %3d/%d (%s) fragmentation is %.1f%% (%dkb)" % ( self.cache_name, num, total, fname, fragmentation, fragmentation_bytes/1024 ) if defrag: msg += " - defragmenting" else: msg += " - skipping" log.info(msg) def defrag_compact_cache(cache, min_percent=0.1, min_bytes=1024*1024, log_progress=None, dry_run=False): bundles = glob.glob(os.path.join(cache.cache_dir, 'L??', 'R????C????.bundle')) for i, bundle_file in enumerate(bundles): offset = bundle_offset(bundle_file) b = cache.bundle_class(bundle_file.rstrip('.bundle'), offset) size, file_size = b.size() defrag = 1 - float(size) / file_size defrag_bytes = file_size - size skip = False if defrag < min_percent or defrag_bytes < min_bytes: skip = True if log_progress: log_progress.log( fname=bundle_file, fragmentation=defrag * 100, fragmentation_bytes=defrag_bytes, num=i+1, total=len(bundles), defrag=not skip, ) if skip or dry_run: continue tmp_bundle = os.path.join(cache.cache_dir, 'tmp_defrag') defb = cache.bundle_class(tmp_bundle, offset) stored_tiles = False for y in range(128): tiles = [Tile((x, y, 0)) for x in range(128)] b.load_tiles(tiles) tiles = [t for t in tiles if t.source] if tiles: stored_tiles = True defb.store_tiles(tiles) # remove first # - in case bundle is empty # - windows does not support rename to existing files if os.path.exists(bundle_file): os.remove(bundle_file) if os.path.exists(bundle_file[:-1] + 'x'): os.remove(bundle_file[:-1] + 'x') if stored_tiles: os.rename(tmp_bundle + '.bundle', bundle_file) if os.path.exists(tmp_bundle + '.bundlx'): os.rename(tmp_bundle + '.bundlx', bundle_file[:-1] + 'x') if os.path.exists(tmp_bundle + '.lck'): os.unlink(tmp_bundle + '.lck') mapproxy-1.11.0/mapproxy/script/export.py000066400000000000000000000227351320454472400205450ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2012 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function, division import os import re import shlex import sys import optparse import yaml from mapproxy.srs import SRS from mapproxy.config.coverage import load_coverage from mapproxy.config.loader import ( load_configuration, ConfigurationError, CacheConfiguration, GridConfiguration, ) from mapproxy.util.coverage import BBOXCoverage from mapproxy.seed.util import ProgressLog, format_bbox from mapproxy.seed.seeder import SeedTask, seed_task from mapproxy.config import spec as conf_spec from mapproxy.util.ext.dictspec.validator import validate, ValidationError def parse_levels(level_str): """ >>> parse_levels('1,2,3,6') [1, 2, 3, 6] >>> parse_levels('1..6') [1, 2, 3, 4, 5, 6] >>> parse_levels('1..6, 8, 9, 13..14') [1, 2, 3, 4, 5, 6, 8, 9, 13, 14] """ levels = set() for part in level_str.split(','): part = part.strip() if re.match('\d+..\d+', part): from_level, to_level = part.split('..') levels.update(list(range(int(from_level), int(to_level) + 1))) else: levels.add(int(part)) return sorted(levels) def parse_grid_definition(definition): """ >>> sorted(parse_grid_definition("res=[10000,1000,100,10] srs=EPSG:4326 bbox=5,50,10,60").items()) [('bbox', '5,50,10,60'), ('res', [10000, 1000, 100, 10]), ('srs', 'EPSG:4326')] """ args = shlex.split(definition) grid_conf = {} for arg in args: key, value = arg.split('=') value = yaml.load(value) grid_conf[key] = value validate(conf_spec.grid_opts, grid_conf) return grid_conf def supports_tiled_access(mgr): if len(mgr.sources) == 1 and getattr(mgr.sources[0], 'supports_meta_tiles') == False: return True return False def format_export_task(task, custom_grid): info = [] if custom_grid: grid = "custom grid" else: grid = "grid '%s'" % task.md['grid_name'] info.append("Exporting cache '%s' to '%s' with %s in %s" % ( task.md['cache_name'], task.md['dest'], grid, task.grid.srs.srs_code)) if task.coverage: info.append(' Limited to: %s (EPSG:4326)' % (format_bbox(task.coverage.extent.llbbox), )) info.append(' Levels: %s' % (task.levels, )) return '\n'.join(info) def export_command(args=None): parser = optparse.OptionParser("%prog grids [options] mapproxy_conf") parser.add_option("-f", "--mapproxy-conf", dest="mapproxy_conf", help="MapProxy configuration") parser.add_option("-q", "--quiet", action="count", dest="quiet", default=0, help="reduce number of messages to stdout, repeat to disable progress output") parser.add_option("--source", dest="source", help="source to export (source or cache)") parser.add_option("--grid", help="grid for export. either the name of an existing grid or " "the grid definition as a string") parser.add_option("--dest", help="destination of the export (directory or filename)") parser.add_option("--type", help="type of the export format") parser.add_option("--levels", help="levels to export: e.g 1,2,3 or 1..10") parser.add_option("--fetch-missing-tiles", dest="fetch_missing_tiles", action='store_true', default=False, help="if missing tiles should be fetched from the sources") parser.add_option("--force", action='store_true', default=False, help="overwrite/append to existing --dest files/directories") parser.add_option("-n", "--dry-run", action="store_true", default=False, help="do not export, just print output") parser.add_option("-c", "--concurrency", type="int", dest="concurrency", default=1, help="number of parallel export processes") parser.add_option("--coverage", help="the coverage for the export as a BBOX string, WKT file " "or OGR datasource") parser.add_option("--srs", help="the SRS of the coverage") parser.add_option("--where", help="filter for OGR coverages") from mapproxy.script.util import setup_logging import logging setup_logging(logging.WARN) if args: args = args[1:] # remove script name (options, args) = parser.parse_args(args) if not options.mapproxy_conf: if len(args) != 1: parser.print_help() sys.exit(1) else: options.mapproxy_conf = args[0] required_options = ['mapproxy_conf', 'grid', 'source', 'dest', 'levels'] for required in required_options: if not getattr(options, required): print('ERROR: missing required option --%s' % required.replace('_', '-'), file=sys.stderr) parser.print_help() sys.exit(1) try: conf = load_configuration(options.mapproxy_conf) except IOError as e: print('ERROR: ', "%s: '%s'" % (e.strerror, e.filename), file=sys.stderr) sys.exit(2) except ConfigurationError as e: print(e, file=sys.stderr) print('ERROR: invalid configuration (see above)', file=sys.stderr) sys.exit(2) if '=' in options.grid: try: grid_conf = parse_grid_definition(options.grid) except ValidationError as ex: print('ERROR: invalid grid configuration', file=sys.stderr) for error in ex.errors: print(' ', error, file=sys.stderr) sys.exit(2) except ValueError: print('ERROR: invalid grid configuration', file=sys.stderr) sys.exit(2) options.grid = 'tmp_mapproxy_export_grid' grid_conf['name'] = options.grid custom_grid = True conf.grids[options.grid] = GridConfiguration(grid_conf, conf) else: custom_grid = False if os.path.exists(options.dest) and not options.force: print('ERROR: destination exists, remove first or use --force', file=sys.stderr) sys.exit(2) cache_conf = { 'name': 'export', 'grids': [options.grid], 'sources': [options.source], } if options.type == 'mbtile': cache_conf['cache'] = { 'type': 'mbtiles', 'filename': options.dest, } elif options.type == 'sqlite': cache_conf['cache'] = { 'type': 'sqlite', 'directory': options.dest, } elif options.type == 'geopackage': cache_conf['cache'] = { 'type': 'geopackage', 'filename': options.dest, } elif options.type == 'compact-v1': cache_conf['cache'] = { 'type': 'compact', 'version': 1, 'directory': options.dest, } elif options.type == 'compact-v2': cache_conf['cache'] = { 'type': 'compact', 'version': 2, 'directory': options.dest, } elif options.type in ('tc', 'mapproxy'): cache_conf['cache'] = { 'type': 'file', 'directory': options.dest, } elif options.type == 'arcgis': cache_conf['cache'] = { 'type': 'file', 'directory_layout': 'arcgis', 'directory': options.dest, } elif options.type in ('tms', None): # default cache_conf['cache'] = { 'type': 'file', 'directory_layout': 'tms', 'directory': options.dest, } else: print('ERROR: unsupported --type %s' % (options.type, ), file=sys.stderr) sys.exit(2) if not options.fetch_missing_tiles: for source in conf.sources.values(): source.conf['seed_only'] = True tile_grid, extent, mgr = CacheConfiguration(cache_conf, conf).caches()[0] levels = parse_levels(options.levels) if levels[-1] >= tile_grid.levels: print('ERROR: destination grid only has %d levels' % tile_grid.levels, file=sys.stderr) sys.exit(2) if options.srs: srs = SRS(options.srs) else: srs = tile_grid.srs if options.coverage: seed_coverage = load_coverage( {'datasource': options.coverage, 'srs': srs, 'where': options.where}, base_path=os.getcwd()) else: seed_coverage = BBOXCoverage(tile_grid.bbox, tile_grid.srs) if not supports_tiled_access(mgr): print('WARN: grids are incompatible. needs to scale/reproject tiles for export.', file=sys.stderr) md = dict(name='export', cache_name='cache', grid_name=options.grid, dest=options.dest) task = SeedTask(md, mgr, levels, 1, seed_coverage) print(format_export_task(task, custom_grid=custom_grid)) logger = ProgressLog(verbose=options.quiet==0, silent=options.quiet>=2) try: seed_task(task, progress_logger=logger, dry_run=options.dry_run, concurrency=options.concurrency) except KeyboardInterrupt: print('stopping...', file=sys.stderr) sys.exit(2) mapproxy-1.11.0/mapproxy/script/grids.py000066400000000000000000000163631320454472400203340ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function, division import math import sys import optparse from mapproxy.compat import iteritems from mapproxy.config import local_base_config from mapproxy.config.loader import load_configuration, ConfigurationError from mapproxy.seed.config import ( load_seed_tasks_conf, SeedConfigurationError, SeedingConfiguration ) def format_conf_value(value): if isinstance(value, tuple): # YAMl only supports lists, convert for clarity value = list(value) return repr(value) def _area_from_bbox(bbox): width = bbox[2] - bbox[0] height = bbox[3] - bbox[1] return width * height def grid_coverage_ratio(bbox, srs, coverage): coverage = coverage.transform_to(srs) grid_area = _area_from_bbox(bbox) if coverage.geom: coverage_area = coverage.geom.area else: coverage_area = _area_from_bbox(coverage.bbox) return coverage_area / grid_area def display_grid(grid_conf, coverage=None): print('%s:' % (grid_conf.conf['name'],)) print(' Configuration:') conf_dict = grid_conf.conf.copy() tile_grid = grid_conf.tile_grid() if 'tile_size' not in conf_dict: conf_dict['tile_size*'] = tile_grid.tile_size if 'bbox' not in conf_dict: conf_dict['bbox*'] = tile_grid.bbox if 'origin' not in conf_dict: conf_dict['origin*'] = tile_grid.origin or 'sw' area_ratio = None if coverage: bbox = tile_grid.bbox area_ratio = grid_coverage_ratio(bbox, tile_grid.srs, coverage) for key in sorted(conf_dict): if key == 'name': continue print(' %s: %s' % (key, format_conf_value(conf_dict[key]))) if coverage: print(' Coverage: %s covers approx. %.4f%% of the grid BBOX' % (coverage.name, area_ratio * 100)) print(' Levels: Resolutions, # x * y = total tiles (approx. tiles within coverage)') else: print(' Levels: Resolutions, # x * y = total tiles') max_digits = max([len("%r" % (res,)) for level, res in enumerate(tile_grid.resolutions)]) for level, res in enumerate(tile_grid.resolutions): tiles_in_x, tiles_in_y = tile_grid.grid_sizes[level] total_tiles = tiles_in_x * tiles_in_y spaces = max_digits - len("%r" % (res,)) + 1 if coverage: coverage_tiles = total_tiles * area_ratio print(" %.2d: %r,%s# %6d * %-6d = %10s (%s)" % (level, res, ' '*spaces, tiles_in_x, tiles_in_y, human_readable_number(total_tiles), human_readable_number(coverage_tiles))) else: print(" %.2d: %r,%s# %6d * %-6d = %10s" % (level, res, ' '*spaces, tiles_in_x, tiles_in_y, human_readable_number(total_tiles))) def human_readable_number(num): if num > 10**6: return '%7.2fM' % (num/10**6) if math.isnan(num): return '?' return '%d' % int(num) def display_grids_list(grids): for grid_name in sorted(grids.keys()): print(grid_name) def display_grids(grids, coverage=None): for i, grid_name in enumerate(sorted(grids.keys())): if i != 0: print() display_grid(grids[grid_name], coverage=coverage) def grids_command(args=None): parser = optparse.OptionParser("%prog grids [options] mapproxy_conf") parser.add_option("-f", "--mapproxy-conf", dest="mapproxy_conf", help="MapProxy configuration.") parser.add_option("-g", "--grid", dest="grid_name", help="Display only information about the specified grid.") parser.add_option("--all", dest="show_all", action="store_true", default=False, help="Show also grids that are not referenced by any cache.") parser.add_option("-l", "--list", dest="list_grids", action="store_true", default=False, help="List names of configured grids, which are used by any cache") coverage_group = parser.add_option_group("Approximate the number of tiles within a given coverage") coverage_group.add_option("-s", "--seed-conf", dest="seed_config", help="Seed configuration, where the coverage is defined") coverage_group.add_option("-c", "--coverage-name", dest="coverage", help="Calculate number of tiles when a coverage is given") from mapproxy.script.util import setup_logging import logging setup_logging(logging.WARN) if args: args = args[1:] # remove script name (options, args) = parser.parse_args(args) if not options.mapproxy_conf: if len(args) != 1: parser.print_help() sys.exit(1) else: options.mapproxy_conf = args[0] try: proxy_configuration = load_configuration(options.mapproxy_conf) except IOError as e: print('ERROR: ', "%s: '%s'" % (e.strerror, e.filename), file=sys.stderr) sys.exit(2) except ConfigurationError as e: print(e, file=sys.stderr) print('ERROR: invalid configuration (see above)', file=sys.stderr) sys.exit(2) with local_base_config(proxy_configuration.base_config): if options.show_all or options.grid_name: grids = proxy_configuration.grids else: caches = proxy_configuration.caches grids = {} for cache in caches.values(): grids.update(cache.grid_confs()) grids = dict(grids) if options.grid_name: options.grid_name = options.grid_name.lower() # ignore case for keys grids = dict((key.lower(), value) for (key, value) in iteritems(grids)) if not grids.get(options.grid_name, False): print('grid not found: %s' % (options.grid_name,)) sys.exit(1) coverage = None if options.coverage and options.seed_config: try: seed_conf = load_seed_tasks_conf(options.seed_config, proxy_configuration) except SeedConfigurationError as e: print('ERROR: invalid configuration (see above)', file=sys.stderr) sys.exit(2) if not isinstance(seed_conf, SeedingConfiguration): print('Old seed configuration format not supported') sys.exit(1) coverage = seed_conf.coverage(options.coverage) coverage.name = options.coverage elif (options.coverage and not options.seed_config) or (not options.coverage and options.seed_config): print('--coverage and --seed-conf can only be used together') sys.exit(1) if options.list_grids: display_grids_list(grids) elif options.grid_name: display_grids({options.grid_name: grids[options.grid_name]}, coverage=coverage) else: display_grids(grids, coverage=coverage) mapproxy-1.11.0/mapproxy/script/scales.py000066400000000000000000000101451320454472400204660ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division, print_function import sys import optparse from mapproxy.compat import itertools DEFAULT_DPIS = { 'OGC': 2.54/(0.00028 * 100), } def values_from_stdin(): values = [] for line in sys.stdin: line = line.split('#', 1)[0] if not line.strip(): break values.append(float(line)) return values def scale_to_res(scale_denom, dpi, unit_factor): m_per_px = 2.54 / (dpi * 100) return scale_denom * m_per_px / unit_factor def res_to_scale(res, dpi, unit_factor): m_per_px = 2.54 / (dpi * 100) return res / m_per_px * unit_factor def format_simple(i, scale, res): return '%20.10f # %2d %20.8f' % (res, i, scale) def format_list(i, scale, res): return ' %20.10f, # %2d %20.8f' % (res, i, scale) def repeated_values(values, n): current_factor = 1 step_factor = 10 result = [] for i, value in enumerate(itertools.islice(itertools.cycle(values), n)): if i != 0 and i % len(values) == 0: current_factor *= step_factor result.append(value/current_factor) return result def fill_values(values, n): return values + [values[-1]/(2**x) for x in range(1, n)] def scales_command(args=None): parser = optparse.OptionParser("%prog scales [options] scale/resolution[, ...]") parser.add_option("-l", "--levels", default=1, type=int, metavar='1', help="number of resolutions/scales to calculate") parser.add_option("-d", "--dpi", default='OGC', help="DPI to convert scales (use OGC for .28mm based DPI)") parser.add_option("--unit", default='m', metavar='m', help="use resolutions in meter (m) or degrees (d)") parser.add_option("--eval", default=False, action='store_true', help="evaluate args as Python expression. For example: 360/256") parser.add_option("--repeat", default=False, action='store_true', help="repeat all values, each time /10. For example: 1000 500 250 results in 1000 500 250 100 50 25 10...") parser.add_option("--res-to-scale", default=False, action='store_true', help="convert resolutions to scale") parser.add_option("--as-res-config", default=False, action='store_true', help="output as resolution list for MapProxy grid configuration") if args: args = args[1:] # remove script name (options, args) = parser.parse_args(args) options.levels = max(options.levels, len(args)) dpi = float(DEFAULT_DPIS.get(options.dpi, options.dpi)) if not args: parser.print_help() sys.exit(1) if args[0] == '-': values = values_from_stdin() elif options.eval: values = [eval(a) for a in args] else: values = [float(a) for a in args] values.sort(reverse=True) if options.repeat: values = repeated_values(values, options.levels) if len(values) < options.levels: values = fill_values(values, options.levels) unit_factor = 1 if options.unit == 'd': # calculated from well-known scale set GoogleCRS84Quad unit_factor = 111319.4907932736 calc = scale_to_res if options.res_to_scale: calc = res_to_scale if options.as_res_config: print(' res: [') print(' # res level scale @%.1f DPI' % dpi) format = format_list else: format = format_simple for i, value in enumerate(values): print(format(i, value, calc(value, dpi, unit_factor))) if options.as_res_config: print(' ]') mapproxy-1.11.0/mapproxy/script/util.py000077500000000000000000000315031320454472400201750ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function import io import os import optparse import re import shutil import sys import textwrap import logging from mapproxy.compat import iteritems from mapproxy.script.conf.app import config_command from mapproxy.script.defrag import defrag_command from mapproxy.script.export import export_command from mapproxy.script.grids import grids_command from mapproxy.script.scales import scales_command from mapproxy.script.wms_capabilities import wms_capabilities_command from mapproxy.version import version def setup_logging(level=logging.INFO, format=None): mapproxy_log = logging.getLogger('mapproxy') mapproxy_log.setLevel(level) ch = logging.StreamHandler(sys.stdout) ch.setLevel(level) if not format: format = "[%(asctime)s] %(name)s - %(levelname)s - %(message)s" formatter = logging.Formatter(format) ch.setFormatter(formatter) mapproxy_log.addHandler(ch) def serve_develop_command(args): parser = optparse.OptionParser("usage: %prog serve-develop [options] mapproxy.yaml") parser.add_option("-b", "--bind", dest="address", default='127.0.0.1:8080', help="Server socket [127.0.0.1:8080]. Use 0.0.0.0 for external access. :1234 to change port.") parser.add_option("--debug", default=False, action='store_true', dest="debug", help="Enable debug mode") options, args = parser.parse_args(args) if len(args) != 2: parser.print_help() print("\nERROR: MapProxy configuration required.") sys.exit(1) mapproxy_conf = args[1] host, port = parse_bind_address(options.address) if options.debug and host not in ('localhost', '127.0.0.1'): print(textwrap.dedent("""\ ################# WARNING! ################## Running debug mode with non-localhost address is a serious security vulnerability. #############################################\ """)) if options.debug: setup_logging(level=logging.DEBUG) else: setup_logging() from mapproxy.wsgiapp import make_wsgi_app from mapproxy.config.loader import ConfigurationError from mapproxy.util.ext.serving import run_simple try: app = make_wsgi_app(mapproxy_conf, debug=options.debug) except ConfigurationError: sys.exit(2) extra_files = app.config_files.keys() if options.debug: try: from repoze.profile import ProfileMiddleware app = ProfileMiddleware( app, log_filename='/tmp/mapproxy_profile.log', discard_first_request=True, flush_at_shutdown=True, path='/__profile__', unwind=False, ) print('Installed profiler at /__profile__') except ImportError: pass run_simple(host, port, app, use_reloader=True, processes=1, threaded=True, passthrough_errors=True, extra_files=extra_files) def serve_multiapp_develop_command(args): parser = optparse.OptionParser("usage: %prog serve-multiapp-develop [options] projects/") parser.add_option("-b", "--bind", dest="address", default='127.0.0.1:8080', help="Server socket [127.0.0.1:8080]") parser.add_option("--debug", default=False, action='store_true', dest="debug", help="Enable debug mode") options, args = parser.parse_args(args) if len(args) != 2: parser.print_help() print("\nERROR: MapProxy projects directory required.") sys.exit(1) mapproxy_conf_dir = args[1] host, port = parse_bind_address(options.address) if options.debug and host not in ('localhost', '127.0.0.1'): print(textwrap.dedent("""\ ################# WARNING! ################## Running debug mode with non-localhost address is a serious security vulnerability. #############################################\ """)) setup_logging() from mapproxy.multiapp import make_wsgi_app from mapproxy.util.ext.serving import run_simple app = make_wsgi_app(mapproxy_conf_dir, debug=options.debug) run_simple(host, port, app, use_reloader=True, processes=1, threaded=True, passthrough_errors=True) def parse_bind_address(address, default=('localhost', 8080)): """ >>> parse_bind_address('80') ('localhost', 80) >>> parse_bind_address('0.0.0.0') ('0.0.0.0', 8080) >>> parse_bind_address('0.0.0.0:8081') ('0.0.0.0', 8081) """ if ':' in address: host, port = address.split(':', 1) port = int(port) elif re.match('^\d+$', address): host = default[0] port = int(address) else: host = address port = default[1] return host, port def create_command(args): cmd = CreateCommand(args) cmd.run() class CreateCommand(object): templates = { 'base-config': {}, 'wsgi-app': {}, 'log-ini': {}, } def __init__(self, args): parser = optparse.OptionParser("usage: %prog create [options] [destination]") parser.add_option("-t", "--template", dest="template", help="Create a configuration from this template.") parser.add_option("-l", "--list-templates", dest="list_templates", action="store_true", default=False, help="List all available configuration templates.") parser.add_option("-f", "--mapproxy-conf", dest="mapproxy_conf", help="Existing MapProxy configuration (required for some templates).") parser.add_option("--force", dest="force", action="store_true", default=False, help="Force operation (e.g. overwrite existing files).") self.options, self.args = parser.parse_args(args) self.parser = parser def log_error(self, msg, *args): print('ERROR:', msg % args, file=sys.stderr) def run(self): if self.options.list_templates: print_items(self.templates, title="Available templates") sys.exit(1) elif self.options.template: if self.options.template not in self.templates: self.log_error("unknown template " + self.options.template) sys.exit(1) if len(self.args) != 2: self.log_error("template requires destination argument") sys.exit(1) sys.exit( getattr(self, 'template_' + self.options.template.replace('-', '_'))() ) else: self.parser.print_help() sys.exit(1) @property def mapproxy_conf(self): if not self.options.mapproxy_conf: self.parser.print_help() self.log_error("template requires --mapproxy-conf option") sys.exit(1) return os.path.abspath(self.options.mapproxy_conf) def template_dir(self): import mapproxy.config_template template_dir = os.path.join( os.path.dirname(mapproxy.config_template.__file__), 'base_config') return template_dir def template_wsgi_app(self): app_filename = self.args[1] if '.' not in os.path.basename(app_filename): app_filename += '.py' mapproxy_conf = self.mapproxy_conf if os.path.exists(app_filename) and not self.options.force: self.log_error("%s already exists, use --force", app_filename) return 1 print("writing MapProxy app to %s" % (app_filename, )) template_dir = self.template_dir() app_template = io.open(os.path.join(template_dir, 'config.wsgi'), encoding='utf-8').read() with io.open(app_filename, 'w', encoding='utf-8') as f: f.write(app_template % {'mapproxy_conf': mapproxy_conf, 'here': os.path.dirname(mapproxy_conf)}) return 0 def template_base_config(self): outdir = self.args[1] if not os.path.exists(outdir): os.makedirs(outdir) template_dir = self.template_dir() for filename in ('mapproxy.yaml', 'seed.yaml', 'full_example.yaml', 'full_seed_example.yaml'): to = os.path.join(outdir, filename) from_ = os.path.join(template_dir, filename) if os.path.exists(to) and not self.options.force: self.log_error("%s already exists, use --force", to) return 1 print("writing %s" % (to, )) shutil.copy(from_, to) return 0 def template_log_ini(self): log_filename = self.args[1] if os.path.exists(log_filename) and not self.options.force: self.log_error("%s already exists, use --force", log_filename) return 1 template_dir = self.template_dir() log_template = io.open(os.path.join(template_dir, 'log.ini'), encoding='utf-8').read() with io.open(log_filename, 'w', encoding='utf-8') as f: f.write(log_template) return 0 commands = { 'serve-develop': { 'func': serve_develop_command, 'help': 'Run MapProxy development server.' }, 'serve-multiapp-develop': { 'func': serve_multiapp_develop_command, 'help': 'Run MultiMapProxy development server.' }, 'create': { 'func': create_command, 'help': 'Create example configurations.' }, 'scales': { 'func': scales_command, 'help': 'Convert between scales and resolutions.' }, 'wms-capabilities': { 'func': wms_capabilities_command, 'help': 'Display WMS capabilites.', }, 'grids': { 'func': grids_command, 'help': 'Display detailed informations for configured grids.' }, 'export': { 'func': export_command, 'help': 'Export existing caches.' }, 'autoconfig': { 'func': config_command, 'help': 'Create config from WMS capabilities.' }, 'defrag-compact-cache': { 'func': defrag_command, 'help': 'De-fragmentate compact caches.' } } class NonStrictOptionParser(optparse.OptionParser): def _process_args(self, largs, rargs, values): while rargs: arg = rargs[0] # We handle bare "--" explicitly, and bare "-" is handled by the # standard arg handler since the short arg case ensures that the # len of the opt string is greater than 1. try: if arg == "--": del rargs[0] return elif arg[0:2] == "--": # process a single long option (possibly with value(s)) self._process_long_opt(rargs, values) elif arg[:1] == "-" and len(arg) > 1: # process a cluster of short options (possibly with # value(s) for the last one only) self._process_short_opts(rargs, values) elif self.allow_interspersed_args: largs.append(arg) del rargs[0] else: return except optparse.BadOptionError: largs.append(arg) def print_items(data, title='Commands'): name_len = max(len(name) for name in data) if title: print('%s:' % (title, ), file=sys.stdout) for name, item in iteritems(data): help = item.get('help', '') name = ('%%-%ds' % name_len) % name if help: help = ' ' + help print(' %s%s' % (name, help), file=sys.stdout) def main(): parser = NonStrictOptionParser("usage: %prog COMMAND [options]", add_help_option=False) options, args = parser.parse_args() if len(args) < 1 or args[0] in ('--help', '-h'): parser.print_help() print() print_items(commands) sys.exit(1) if len(args) == 1 and args[0] == '--version': print('MapProxy ' + version) sys.exit(1) command = args[0] if command not in commands: parser.print_help() print() print_items(commands) print('\nERROR: unknown command %s' % (command,), file=sys.stdout) sys.exit(1) args = sys.argv[0:1] + sys.argv[2:] commands[command]['func'](args) if __name__ == '__main__': main()mapproxy-1.11.0/mapproxy/script/wms_capabilities.py000066400000000000000000000134761320454472400225450ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function import sys import optparse from xml import etree from mapproxy.compat import iteritems, BytesIO from mapproxy.compat.modules import urlparse from mapproxy.client.http import open_url, HTTPClientError from mapproxy.request.base import BaseRequest, url_decode from mapproxy.util.ext import wmsparse class PrettyPrinter(object): def __init__(self, indent=4, version='1.1.1'): self.indent = indent self.print_order = ['name', 'title', 'url', 'srs', 'llbbox', 'bbox'] self.marker = '- ' self.version = version def print_line(self, indent, key, value=None, mark_first=False): marker = '' if value is None: value = '' if mark_first: indent = indent - len(self.marker) marker = self.marker print(("%s%s%s: %s" % (' '*indent, marker, key, value))) def _format_output(self, key, value, indent, mark_first=False): if key == 'bbox': self.print_line(indent, key) for srs_code, bbox in iteritems(value): self.print_line(indent+self.indent, srs_code, value=bbox, mark_first=mark_first) else: if isinstance(value, set): value = list(value) self.print_line(indent, key, value=value, mark_first=mark_first) def print_layers(self, capabilities, indent=None, root=False): if root: print("# Note: This is not a valid MapProxy configuration!") print('Capabilities Document Version %s' % (self.version,)) print('Root-Layer:') layer_list = capabilities.layers()['layers'] else: layer_list = capabilities['layers'] indent = indent or self.indent for layer in layer_list: marked_first = False # print ordered items first for item in self.print_order: if layer.get(item, False): if not marked_first: marked_first = True self._format_output(item, layer[item], indent, mark_first=marked_first) else: self._format_output(item, layer[item], indent) # print remaining items except sublayers for key, value in iteritems(layer): if key in self.print_order or key == 'layers': continue self._format_output(key, value, indent) # print the sublayers now if layer.get('layers', False): self.print_line(indent, 'layers') self.print_layers(layer, indent=indent+self.indent) def log_error(msg, *args): print(msg % args, file=sys.stderr) def wms_capapilities_url(url, version): parsed_url = urlparse.urlparse(url) base_req = BaseRequest( url=url.split('?', 1)[0], param=url_decode(parsed_url.query), ) base_req.params['service'] = 'WMS' base_req.params['version'] = version base_req.params['request'] = 'GetCapabilities' return base_req.complete_url def parse_capabilities(fileobj, version='1.1.1'): try: return wmsparse.parse_capabilities(fileobj) except ValueError as ex: log_error('%s\n%s\n%s\n%s\nNot a capabilities document: %s', 'Recieved document:', '-'*80, fileobj.getvalue(), '-'*80, ex.args[0]) sys.exit(1) except etree.ElementTree.ParseError as ex: log_error('%s\n%s\n%s\n%s\nCould not parse the document: %s', 'Recieved document:', '-'*80, fileobj.getvalue(), '-'*80, ex.args[0]) sys.exit(1) def parse_capabilities_url(url, version='1.1.1'): try: capabilities_url = wms_capapilities_url(url, version) capabilities_response = open_url(capabilities_url) except HTTPClientError as ex: log_error('ERROR: %s', ex.args[0]) sys.exit(1) # after parsing capabilities_response will be empty, therefore cache it capabilities = BytesIO(capabilities_response.read()) return parse_capabilities(capabilities, version=version) def wms_capabilities_command(args=None): parser = optparse.OptionParser("%prog wms-capabilities [options] URL", description="Read and parse WMS 1.1.1 capabilities and print out" " information about each layer. It does _not_ return a valid" " MapProxy configuration.") parser.add_option("--host", dest="capabilities_url", help="WMS Capabilites URL") parser.add_option("--version", dest="version", choices=['1.1.1', '1.3.0'], default='1.1.1', help="Request GetCapabilities-document in version 1.1.1 or 1.3.0", metavar="<1.1.1 or 1.3.0>") if args: args = args[1:] # remove script name (options, args) = parser.parse_args(args) if not options.capabilities_url: if len(args) != 1: parser.print_help() sys.exit(2) else: options.capabilities_url = args[0] try: service = parse_capabilities_url(options.capabilities_url, version=options.version) printer = PrettyPrinter(indent=4, version=options.version) printer.print_layers(service, root=True) except KeyError as ex: log_error('XML-Element has no such attribute (%s).' % (ex.args[0],)) sys.exit(1) mapproxy-1.11.0/mapproxy/seed/000077500000000000000000000000001320454472400162555ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/seed/__init__.py000066400000000000000000000000001320454472400203540ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/seed/cachelock.py000066400000000000000000000072101320454472400205430ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2012 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import errno import os import sqlite3 import time from contextlib import contextmanager class CacheLockedError(Exception): pass class CacheLocker(object): def __init__(self, lockfile, polltime=0.1): self.lockfile = lockfile self.polltime = polltime self._initialize_lockfile() def _initialize_lockfile(self): db = sqlite3.connect(self.lockfile) db.execute(""" CREATE TABLE IF NOT EXISTS cache_locks ( cache_name TEXT NOT NULL, created REAL NOT NULL, pid INTEGER NUT NULL ); """) db.commit() db.close() @contextmanager def _exclusive_db_cursor(self): db = sqlite3.connect(self.lockfile, isolation_level="EXCLUSIVE") db.row_factory = sqlite3.Row cur = db.cursor() try: yield cur finally: db.commit() db.close() @contextmanager def lock(self, cache_name, no_block=False): pid = os.getpid() while True: with self._exclusive_db_cursor() as cur: self._add_lock(cur, cache_name, pid) if self._poll(cur, cache_name, pid): break elif no_block: raise CacheLockedError() time.sleep(self.polltime) try: yield finally: with self._exclusive_db_cursor() as cur: self._remove_lock(cur, cache_name, pid) def _poll(self, cur, cache_name, pid): active_locks = False cur.execute("SELECT * from cache_locks where cache_name = ? ORDER BY created", (cache_name, )) for lock in cur: if not active_locks and lock['cache_name'] == cache_name and lock['pid'] == pid: # we are waiting and it is out turn return True if not is_running(lock['pid']): self._remove_lock(cur, lock['cache_name'], lock['pid']) else: active_locks = True return not active_locks def _add_lock(self, cur, cache_name, pid): cur.execute("SELECT count(*) from cache_locks WHERE cache_name = ? AND pid = ?", (cache_name, pid)) if cur.fetchone()[0] == 0: cur.execute("INSERT INTO cache_locks (cache_name, pid, created) VALUES (?, ?, ?)", (cache_name, pid, time.time())) def _remove_lock(self, cur, cache_name, pid): cur.execute("DELETE FROM cache_locks WHERE cache_name = ? AND pid = ?", (cache_name, pid)) class DummyCacheLocker(object): @contextmanager def lock(self, cache_name, no_block=False): yield def is_running(pid): try: os.kill(pid, 0) except OSError as err: if err.errno == errno.ESRCH: return False elif err.errno == errno.EPERM: return True else: raise err else: return True if __name__ == '__main__': locker = CacheLocker('/tmp/cachelock_test') with locker.lock('foo'): passmapproxy-1.11.0/mapproxy/seed/cleanup.py000066400000000000000000000153241320454472400202630ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function import os from mapproxy.compat.itertools import izip_longest from mapproxy.seed.util import format_cleanup_task from mapproxy.util.fs import cleanup_directory from mapproxy.seed.seeder import ( TileWorkerPool, TileWalker, TileCleanupWorker, SeedProgress, ) from mapproxy.seed.util import ProgressLog def cleanup(tasks, concurrency=2, dry_run=False, skip_geoms_for_last_levels=0, verbose=True, progress_logger=None): for task in tasks: print(format_cleanup_task(task)) if task.coverage is False: continue # seed_progress for tilewalker cleanup seed_progress = None # cleanup_progress for os.walk based cleanup cleanup_progress = None if progress_logger and progress_logger.progress_store: progress_logger.current_task_id = task.id start_progress = progress_logger.progress_store.get(task.id) seed_progress = SeedProgress(old_progress_identifier=start_progress) cleanup_progress = DirectoryCleanupProgress(old_dir=start_progress) if task.complete_extent: if callable(getattr(task.tile_manager.cache, 'level_location', None)): simple_cleanup(task, dry_run=dry_run, progress_logger=progress_logger, cleanup_progress=cleanup_progress) continue elif callable(getattr(task.tile_manager.cache, 'remove_level_tiles_before', None)): cache_cleanup(task, dry_run=dry_run, progress_logger=progress_logger) continue tilewalker_cleanup(task, dry_run=dry_run, concurrency=concurrency, skip_geoms_for_last_levels=skip_geoms_for_last_levels, progress_logger=progress_logger, seed_progress=seed_progress, ) def simple_cleanup(task, dry_run, progress_logger=None, cleanup_progress=None): """ Cleanup cache level on file system level. """ for level in task.levels: level_dir = task.tile_manager.cache.level_location(level) if dry_run: def file_handler(filename): print('removing ' + filename) else: file_handler = None if progress_logger: progress_logger.log_message('removing old tiles in ' + normpath(level_dir)) if progress_logger.progress_store: cleanup_progress.step_dir(level_dir) if cleanup_progress.already_processed(): continue progress_logger.progress_store.add( task.id, cleanup_progress.current_progress_identifier(), ) progress_logger.progress_store.write() cleanup_directory(level_dir, task.remove_timestamp, file_handler=file_handler, remove_empty_dirs=True) def cache_cleanup(task, dry_run, progress_logger=None): for level in task.levels: if progress_logger: progress_logger.log_message('removing old tiles for level %s' % level) if not dry_run: task.tile_manager.cache.remove_level_tiles_before(level, task.remove_timestamp) task.tile_manager.cleanup() def normpath(path): # relpath doesn't support UNC if path.startswith('\\'): return path path = os.path.relpath(path) if path.startswith('../../'): path = os.path.abspath(path) return path def tilewalker_cleanup(task, dry_run, concurrency, skip_geoms_for_last_levels, progress_logger=None, seed_progress=None): """ Cleanup tiles with tile traversal. """ task.tile_manager._expire_timestamp = task.remove_timestamp task.tile_manager.minimize_meta_requests = False tile_worker_pool = TileWorkerPool(task, TileCleanupWorker, progress_logger=progress_logger, dry_run=dry_run, size=concurrency) tile_walker = TileWalker(task, tile_worker_pool, handle_stale=True, work_on_metatiles=False, progress_logger=progress_logger, skip_geoms_for_last_levels=skip_geoms_for_last_levels, seed_progress=seed_progress) try: tile_walker.walk() except KeyboardInterrupt: tile_worker_pool.stop(force=True) raise finally: tile_worker_pool.stop() class DirectoryCleanupProgress(object): def __init__(self, old_dir=None): self.old_dir = old_dir self.current_dir = None def step_dir(self, dir): self.current_dir = dir def already_processed(self): return self.can_skip(self.old_dir, self.current_dir) def current_progress_identifier(self): if self.already_processed() or self.current_dir is None: return self.old_dir return self.current_dir @staticmethod def can_skip(old_dir, current_dir): """ Return True if the `current_dir` is before `old_dir` when compared lexicographic. >>> DirectoryCleanupProgress.can_skip(None, '/00') False >>> DirectoryCleanupProgress.can_skip(None, '/00/000/000') False >>> DirectoryCleanupProgress.can_skip('/01/000/001', '/00') True >>> DirectoryCleanupProgress.can_skip('/01/000/001', '/01/000/000') True >>> DirectoryCleanupProgress.can_skip('/01/000/001', '/01/000/000/000') True >>> DirectoryCleanupProgress.can_skip('/01/000/001', '/01/000/001') False >>> DirectoryCleanupProgress.can_skip('/01/000/001', '/01/000/001/000') False """ if old_dir is None: return False if current_dir is None: return False for old, current in izip_longest(old_dir.split(os.path.sep), current_dir.split(os.path.sep), fillvalue=None): if old is None: return False if current is None: return False if old < current: return False if old > current: return True return False def running(self): return True mapproxy-1.11.0/mapproxy/seed/config.py000066400000000000000000000423151320454472400201010ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function import os import sys import time import operator from functools import reduce from mapproxy.cache.dummy import DummyCache from mapproxy.compat import iteritems, itervalues, iterkeys from mapproxy.config import abspath from mapproxy.config.loader import ConfigurationError from mapproxy.config.coverage import load_coverage from mapproxy.srs import SRS, TransformationError from mapproxy.util.py import memoize from mapproxy.util.times import timestamp_from_isodate, timestamp_before from mapproxy.util.coverage import MultiCoverage, BBOXCoverage, GeomCoverage from mapproxy.util.geom import GeometryError, EmptyGeometryError, CoverageReadError from mapproxy.util.yaml import load_yaml_file, YAMLError from mapproxy.seed.util import bidict from mapproxy.seed.seeder import SeedTask, CleanupTask from mapproxy.seed.spec import validate_seed_conf class SeedConfigurationError(ConfigurationError): pass class EmptyCoverageError(Exception): pass import logging log = logging.getLogger('mapproxy.seed.config') def load_seed_tasks_conf(seed_conf_filename, mapproxy_conf): try: conf = load_yaml_file(seed_conf_filename) except YAMLError as ex: raise SeedConfigurationError(ex) if 'views' in conf: # TODO: deprecate old config seed_conf = LegacySeedingConfiguration(conf, mapproxy_conf=mapproxy_conf) else: errors, informal_only = validate_seed_conf(conf) for error in errors: log.warn(error) if not informal_only: raise SeedConfigurationError('invalid configuration') seed_conf = SeedingConfiguration(conf, mapproxy_conf=mapproxy_conf) return seed_conf class LegacySeedingConfiguration(object): """ Read old seed.yaml configuration (with seed and views). """ def __init__(self, seed_conf, mapproxy_conf): self.conf = seed_conf self.mapproxy_conf = mapproxy_conf self.grids = bidict((name, grid_conf.tile_grid()) for name, grid_conf in iteritems(self.mapproxy_conf.grids)) self.seed_tasks = [] self.cleanup_tasks = [] self._init_tasks() def _init_tasks(self): for cache_name, options in iteritems(self.conf['seeds']): remove_before = None if 'remove_before' in options: remove_before = before_timestamp_from_options(options['remove_before']) try: caches = self.mapproxy_conf.caches[cache_name].caches() except KeyError: print('error: cache %s not found. available caches: %s' % ( cache_name, ','.join(self.mapproxy_conf.caches.keys())), file=sys.stderr) return caches = dict((grid, tile_mgr) for grid, extent, tile_mgr in caches) for view in options['views']: view_conf = self.conf['views'][view] coverage = load_coverage(view_conf) cache_srs = view_conf.get('srs', None) if cache_srs is not None: cache_srs = [SRS(s) for s in cache_srs] level = view_conf.get('level', None) assert len(level) == 2 for grid, tile_mgr in iteritems(caches): if cache_srs and grid.srs not in cache_srs: continue md = dict(name=view, cache_name=cache_name, grid_name=self.grids[grid]) levels = list(range(level[0], level[1]+1)) if coverage: if isinstance(coverage, GeomCoverage) and coverage.geom.is_empty: continue seed_coverage = coverage.transform_to(grid.srs) else: seed_coverage = BBOXCoverage(grid.bbox, grid.srs) self.seed_tasks.append(SeedTask(md, tile_mgr, levels, remove_before, seed_coverage)) if remove_before: levels = list(range(grid.levels)) complete_extent = bool(coverage) self.cleanup_tasks.append(CleanupTask(md, tile_mgr, levels, remove_before, seed_coverage, complete_extent=complete_extent)) def seed_tasks_names(self): return self.conf['seeds'].keys() def cleanup_tasks_names(self): return self.conf['seeds'].keys() def seeds(self, names=None): if names is None: return self.seed_tasks else: return [t for t in self.seed_tasks if t.md['name'] in names] def cleanups(self, names=None): if names is None: return self.cleanup_tasks else: return [t for t in self.cleanup_tasks if t.md['name'] in names] class SeedingConfiguration(object): def __init__(self, seed_conf, mapproxy_conf): self.conf = seed_conf self.mapproxy_conf = mapproxy_conf self.grids = bidict((name, grid_conf.tile_grid()) for name, grid_conf in iteritems(self.mapproxy_conf.grids)) @memoize def coverage(self, name): coverage_conf = (self.conf.get('coverages') or {}).get(name) if coverage_conf is None: raise SeedConfigurationError('coverage %s not found. available coverages: %s' % ( name, ','.join((self.conf.get('coverages') or {}).keys()))) try: coverage = load_coverage(coverage_conf) except CoverageReadError as ex: raise SeedConfigurationError("can't load coverage '%s'. %s" % (name, ex)) except GeometryError as ex: raise SeedConfigurationError("invalid geometry in coverage '%s'. %s" % (name, ex)) except EmptyGeometryError as ex: raise EmptyCoverageError("coverage '%s' contains no geometries. %s" % (name, ex)) # without extend we have an empty coverage if not coverage.extent.llbbox: raise EmptyCoverageError("coverage '%s' contains no geometries." % name) return coverage def cache(self, cache_name): cache = {} if cache_name not in self.mapproxy_conf.caches: raise SeedConfigurationError('cache %s not found. available caches: %s' % ( cache_name, ','.join(self.mapproxy_conf.caches.keys()))) for tile_grid, extent, tile_mgr in self.mapproxy_conf.caches[cache_name].caches(): if isinstance(tile_mgr.cache, DummyCache): raise SeedConfigurationError('can\'t seed cache %s (disable_storage: true)' % cache_name) grid_name = self.grids[tile_grid] cache[grid_name] = tile_mgr return cache def seed_tasks_names(self): seeds = self.conf.get('seeds') or {} if seeds: return seeds.keys() return [] def cleanup_tasks_names(self): cleanups = self.conf.get('cleanups') or {} if cleanups: return cleanups.keys() return [] def seeds(self, names=None): """ Return seed tasks. """ tasks = [] if names is None: names = (self.conf.get('seeds') or {}).keys() for seed_name in names: seed_conf = self.conf['seeds'][seed_name] seed_conf = SeedConfiguration(seed_name, seed_conf, self) for task in seed_conf.seed_tasks(): tasks.append(task) return tasks def cleanups(self, names=None): """ Return cleanup tasks. """ tasks = [] if names is None: names = (self.conf.get('cleanups') or {}).keys() for cleanup_name in names: cleanup_conf = self.conf['cleanups'][cleanup_name] cleanup_conf = CleanupConfiguration(cleanup_name, cleanup_conf, self) for task in cleanup_conf.cleanup_tasks(): tasks.append(task) return tasks class ConfigurationBase(object): def __init__(self, name, conf, seeding_conf): self.name = name self.conf = conf self.seeding_conf = seeding_conf self.coverage = self._coverages() self.caches = self._caches() self.grids = self._grids(self.caches) self.levels = levels_from_options(conf) def _coverages(self): coverage = None if 'coverages' in self.conf: try: coverages = [self.seeding_conf.coverage(c) for c in self.conf.get('coverages', {})] except EmptyCoverageError: return False if len(coverages) == 1: coverage = coverages[0] else: coverage = MultiCoverage(coverages) return coverage def _grids(self, caches): grids = [] if 'grids' in self.conf: # grids available for all caches available_grids = reduce(operator.and_, (set(cache) for cache in caches.values())) for grid_name in self.conf['grids']: if grid_name not in available_grids: raise SeedConfigurationError('%s not defined for caches' % grid_name) grids.append(grid_name) else: # check that all caches have the same grids configured last = [] for cache_grids in (set(iterkeys(cache)) for cache in itervalues(caches)): if not last: last = cache_grids else: if last != cache_grids: raise SeedConfigurationError('caches in same seed task require identical grids') grids = list(last or []) return grids def _caches(self): """ Returns a dictionary with all caches for this seed. e.g.: {'seed1': {'grid1': tilemanager1, 'grid2': tilemanager2}} """ caches = {} for cache_name in self.conf.get('caches', []): caches[cache_name] = self.seeding_conf.cache(cache_name) return caches class SeedConfiguration(ConfigurationBase): def __init__(self, name, conf, seeding_conf): ConfigurationBase.__init__(self, name, conf, seeding_conf) self.refresh_timestamp = None if 'refresh_before' in self.conf: self.refresh_timestamp = before_timestamp_from_options(self.conf['refresh_before']) def seed_tasks(self): for grid_name in self.grids: for cache_name, cache in iteritems(self.caches): tile_manager = cache[grid_name] grid = self.seeding_conf.grids[grid_name] if self.coverage is False: coverage = False elif self.coverage: coverage = self.coverage.transform_to(grid.srs) else: coverage = BBOXCoverage(grid.bbox, grid.srs) try: if coverage is not False: coverage.extent.llbbox except TransformationError: raise SeedConfigurationError('%s: coverage transformation error' % self.name) if self.levels: levels = self.levels.for_grid(grid) else: levels = list(range(0, grid.levels)) if not tile_manager.cache.supports_timestamp: if self.refresh_timestamp: # remove everything self.refresh_timestamp = 0 md = dict(name=self.name, cache_name=cache_name, grid_name=grid_name) yield SeedTask(md, tile_manager, levels, self.refresh_timestamp, coverage) class CleanupConfiguration(ConfigurationBase): def __init__(self, name, conf, seeding_conf): ConfigurationBase.__init__(self, name, conf, seeding_conf) self.init_time = time.time() if self.conf.get('remove_all') == True: self.remove_timestamp = 0 elif 'remove_before' in self.conf: self.remove_timestamp = before_timestamp_from_options(self.conf['remove_before']) else: # use now as remove_before date. this should not remove # fresh seeded tiles, since this should be configured before seeding self.remove_timestamp = self.init_time def cleanup_tasks(self): for grid_name in self.grids: for cache_name, cache in iteritems(self.caches): tile_manager = cache[grid_name] grid = self.seeding_conf.grids[grid_name] if self.coverage is False: coverage = False complete_extent = False elif self.coverage: coverage = self.coverage.transform_to(grid.srs) complete_extent = False else: coverage = BBOXCoverage(grid.bbox, grid.srs) complete_extent = True try: if coverage is not False: coverage.extent.llbbox except TransformationError: raise SeedConfigurationError('%s: coverage transformation error' % self.name) if self.levels: levels = self.levels.for_grid(grid) else: levels = list(range(0, grid.levels)) if not tile_manager.cache.supports_timestamp: # for caches without timestamp support (like MBTiles) if self.remove_timestamp is self.init_time or self.remove_timestamp == 0: # remove everything self.remove_timestamp = 0 else: raise SeedConfigurationError("cleanup does not support remove_before for '%s'" " because cache '%s' does not support timestamps" % (self.name, cache_name)) md = dict(name=self.name, cache_name=cache_name, grid_name=grid_name) yield CleanupTask(md, tile_manager, levels, self.remove_timestamp, coverage=coverage, complete_extent=complete_extent) def levels_from_options(conf): levels = conf.get('levels') if levels: if isinstance(levels, list): return LevelsList(levels) from_level = levels.get('from') to_level = levels.get('to') return LevelsRange((from_level, to_level)) resolutions = conf.get('resolutions') if resolutions: if isinstance(resolutions, list): return LevelsResolutionList(resolutions) from_res = resolutions.get('from') to_res = resolutions.get('to') return LevelsResolutionRange((from_res, to_res)) return None def before_timestamp_from_options(conf): """ >>> import time >>> t = before_timestamp_from_options({'hours': 4}) >>> time.time() - t - 4 * 60 * 60 < 1 True """ if 'time' in conf: try: return timestamp_from_isodate(conf['time']) except ValueError: raise SeedConfigurationError( "can't parse time '%s'. should be ISO time string" % (conf["time"], )) if 'mtime' in conf: datasource = abspath(conf['mtime']) try: return os.path.getmtime(datasource) except OSError as ex: raise SeedConfigurationError( "can't parse last modified time from file '%s'." % (datasource, ), ex) deltas = {} for delta_type in ('weeks', 'days', 'hours', 'minutes', 'seconds'): deltas[delta_type] = conf.get(delta_type, 0) return timestamp_before(**deltas) class LevelsList(object): def __init__(self, levels=None): self.levels = levels def for_grid(self, grid): uniqe_valid_levels = set(l for l in self.levels if 0 <= l <= (grid.levels-1)) return sorted(uniqe_valid_levels) class LevelsRange(object): def __init__(self, level_range=None): self.level_range = level_range def for_grid(self, grid): start, stop = self.level_range if start is None: start = 0 if stop is None: stop = 999 stop = min(stop, grid.levels-1) return list(range(start, stop+1)) class LevelsResolutionRange(object): def __init__(self, res_range=None): self.res_range = res_range def for_grid(self, grid): start, stop = self.res_range if start is None: start = 0 else: start = grid.closest_level(start) if stop is None: stop = grid.levels-1 else: stop = grid.closest_level(stop) return list(range(start, stop+1)) class LevelsResolutionList(object): def __init__(self, resolutions=None): self.resolutions = resolutions def for_grid(self, grid): levels = set(grid.closest_level(res) for res in self.resolutions) return sorted(levels) mapproxy-1.11.0/mapproxy/seed/script.py000066400000000000000000000361061320454472400201410ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function import errno import os import re import signal import sys import time import logging from logging.config import fileConfig from subprocess import Popen from optparse import OptionParser, OptionValueError from mapproxy.config.loader import load_configuration, ConfigurationError from mapproxy.seed.config import load_seed_tasks_conf from mapproxy.seed.seeder import seed, SeedInterrupted from mapproxy.seed.cleanup import cleanup from mapproxy.seed.util import (format_seed_task, format_cleanup_task, ProgressLog, ProgressStore) from mapproxy.seed.cachelock import CacheLocker from mapproxy.compat import raw_input SECONDS_PER_DAY = 60 * 60 * 24 SECONDS_PER_MINUTE = 60 def setup_logging(logging_conf=None): if logging_conf is not None: fileConfig(logging_conf, {'here': './'}) mapproxy_log = logging.getLogger('mapproxy') mapproxy_log.setLevel(logging.WARN) ch = logging.StreamHandler(sys.stdout) ch.setLevel(logging.DEBUG) formatter = logging.Formatter( "[%(asctime)s] %(name)s - %(levelname)s - %(message)s") ch.setFormatter(formatter) mapproxy_log.addHandler(ch) def check_duration(option, opt, value, parser): try: setattr(parser.values, option.dest, parse_duration(value)) except ValueError: raise OptionValueError( "option %s: invalid duration value: %r, expected (10s, 15m, 0.5h, 3d, etc)" % (opt, value), ) def parse_duration(string): match = re.match(r'^(\d*.?\d+)(s|m|h|d)', string) if not match: raise ValueError('invalid duration, not in format: 10s, 0.5h, etc.') duration = float(match.group(1)) unit = match.group(2) if unit == 's': return duration duration *= 60 if unit == 'm': return duration duration *= 60 if unit == 'h': return duration duration *= 24 return duration class SeedScript(object): usage = "usage: %prog [options] seed_conf" parser = OptionParser(usage) parser.add_option("-q", "--quiet", action="count", dest="quiet", default=0, help="reduce number of messages to stdout, repeat to disable progress output") parser.add_option("-s", "--seed-conf", dest="seed_file", default=None, help="seed configuration") parser.add_option("-f", "--proxy-conf", dest="conf_file", default=None, help="proxy configuration") parser.add_option("-c", "--concurrency", type="int", dest="concurrency", default=2, help="number of parallel seed processes") parser.add_option("-n", "--dry-run", action="store_true", dest="dry_run", default=False, help="do not seed, just print output") parser.add_option("-l", "--skip-geoms-for-last-levels", type="int", dest="geom_levels", default=0, metavar="N", help="do not check for intersections between tiles" " and seed geometries on the last N levels") parser.add_option("--summary", action="store_true", dest="summary", default=False, help="print summary with all seeding tasks and exit." " does not seed anything.") parser.add_option("-i", "--interactive", action="store_true", dest="interactive", default=False, help="print each task description and ask if it should be seeded") parser.add_option("--seed", action="append", dest="seed_names", metavar='task1,task2,...', help="seed only the named tasks. cleanup is disabled unless " "--cleanup is used. use ALL to select all tasks") parser.add_option("--cleanup", action="append", dest="cleanup_names", metavar='task1,task2,...', help="cleanup only the named tasks. seeding is disabled unless " "--seed is used. use ALL to select all tasks") parser.add_option("--use-cache-lock", action="store_true", default=False, help="use locking to prevent multiple mapproxy-seed calls " "to seed the same cache") parser.add_option("--continue", dest='continue_seed', action="store_true", default=False, help="continue an aborted seed progress") parser.add_option("--progress-file", dest='progress_file', default=None, help="filename for storing the seed progress (for --continue option)") parser.add_option("--duration", dest="duration", help="stop seeding after (120s, 15m, 4h, 0.5d, etc)", type=str, action="callback", callback=check_duration) parser.add_option("--reseed-file", dest="reseed_file", help="start of last re-seed", metavar="FILE", default=None) parser.add_option("--reseed-interval", dest="reseed_interval", help="only start seeding if --reseed-file is older then --reseed-interval", metavar="DURATION", type=str, action="callback", callback=check_duration, default=None) parser.add_option("--log-config", dest='logging_conf', default=None, help="logging configuration") def __call__(self): (options, args) = self.parser.parse_args() if len(args) != 1 and not options.seed_file: self.parser.print_help() sys.exit(1) if not options.seed_file: if len(args) != 1: self.parser.error('missing seed_conf file as last argument or --seed-conf option') else: options.seed_file = args[0] if not options.conf_file: self.parser.error('missing mapproxy configuration -f/--proxy-conf') setup_logging(options.logging_conf) if options.duration: # calls with --duration are handled in call_with_duration sys.exit(self.call_with_duration(options, args)) try: mapproxy_conf = load_configuration(options.conf_file, seed=True) except ConfigurationError as ex: print("ERROR: " + '\n\t'.join(str(ex).split('\n'))) sys.exit(2) if options.use_cache_lock: cache_locker = CacheLocker('.mapproxy_seed.lck') else: cache_locker = None if not sys.stdout.isatty() and options.quiet == 0: # disable verbose output for non-ttys options.quiet = 1 progress = None if options.continue_seed or options.progress_file: if not options.progress_file: options.progress_file = '.mapproxy_seed_progress' progress = ProgressStore(options.progress_file, continue_seed=options.continue_seed) if options.reseed_file: if not os.path.exists(options.reseed_file): # create --reseed-file if missing with open(options.reseed_file, 'w'): pass else: if progress and not os.path.exists(options.progress_file): # we have an existing --reseed-file but no --progress-file # meaning the last seed call was completed if options.reseed_interval and ( os.path.getmtime(options.reseed_file) > (time.time() - options.reseed_interval) ): print("no need for re-seeding") sys.exit(1) os.utime(options.reseed_file, (time.time(), time.time())) with mapproxy_conf: try: seed_conf = load_seed_tasks_conf(options.seed_file, mapproxy_conf) seed_names, cleanup_names = self.task_names(seed_conf, options) seed_tasks = seed_conf.seeds(seed_names) cleanup_tasks = seed_conf.cleanups(cleanup_names) except ConfigurationError as ex: print("error in configuration: " + '\n\t'.join(str(ex).split('\n'))) sys.exit(2) if options.summary: print('========== Seeding tasks ==========') for task in seed_tasks: print(format_seed_task(task)) print('========== Cleanup tasks ==========') for task in cleanup_tasks: print(format_cleanup_task(task)) return 0 try: if options.interactive: seed_tasks, cleanup_tasks = self.interactive(seed_tasks, cleanup_tasks) if seed_tasks: print('========== Seeding tasks ==========') print('Start seeding process (%d task%s)' % ( len(seed_tasks), 's' if len(seed_tasks) > 1 else '')) logger = ProgressLog(verbose=options.quiet==0, silent=options.quiet>=2, progress_store=progress) seed(seed_tasks, progress_logger=logger, dry_run=options.dry_run, concurrency=options.concurrency, cache_locker=cache_locker, skip_geoms_for_last_levels=options.geom_levels) if cleanup_tasks: print('========== Cleanup tasks ==========') print('Start cleanup process (%d task%s)' % ( len(cleanup_tasks), 's' if len(cleanup_tasks) > 1 else '')) logger = ProgressLog(verbose=options.quiet==0, silent=options.quiet>=2, progress_store=progress) cleanup(cleanup_tasks, verbose=options.quiet==0, dry_run=options.dry_run, concurrency=options.concurrency, progress_logger=logger, skip_geoms_for_last_levels=options.geom_levels) except SeedInterrupted: print('\ninterrupted...') return 3 except KeyboardInterrupt: print('\nexiting...') return 2 if progress: progress.remove() def task_names(self, seed_conf, options): seed_names = cleanup_names = [] if options.seed_names: seed_names = split_comma_seperated_option(options.seed_names) if seed_names == ['ALL']: seed_names = None else: avail_seed_names = seed_conf.seed_tasks_names() missing = set(seed_names).difference(avail_seed_names) if missing: print('unknown seed tasks: %s' % (', '.join(missing), )) print('available seed tasks: %s' % (', '.join(avail_seed_names), )) sys.exit(1) elif not options.cleanup_names: seed_names = None # seed all if options.cleanup_names: cleanup_names = split_comma_seperated_option(options.cleanup_names) if cleanup_names == ['ALL']: cleanup_names = None else: avail_cleanup_names = seed_conf.cleanup_tasks_names() missing = set(cleanup_names).difference(avail_cleanup_names) if missing: print('unknown cleanup tasks: %s' % (', '.join(missing), )) print('available cleanup tasks: %s' % (', '.join(avail_cleanup_names), )) sys.exit(1) elif not options.seed_names: cleanup_names = None # cleanup all return seed_names, cleanup_names def call_with_duration(self, options, args): # --duration is implemented by calling mapproxy-seed again in a separate # process (but without --duration) and terminating that process # after --duration argv = sys.argv[:] for i, arg in enumerate(sys.argv): if arg == '--duration': argv = sys.argv[:i] + sys.argv[i+2:] break elif arg.startswith('--duration='): argv = sys.argv[:i] + sys.argv[i+1:] break # call mapproxy-seed again, poll status, terminate after --duration cmd = Popen(args=argv) start = time.time() while True: if (time.time() - start) > options.duration: try: cmd.send_signal(signal.SIGINT) # try to stop with sigint # send sigterm after 10 seconds for _ in range(10): time.sleep(1) if cmd.poll() is not None: break else: cmd.terminate() except OSError as ex: if ex.errno != errno.ESRCH: # no such process raise return 0 if cmd.poll() is not None: return cmd.returncode try: time.sleep(1) except KeyboardInterrupt: # force termination start = 0 def interactive(self, seed_tasks, cleanup_tasks): selected_seed_tasks = [] print('========== Select seeding tasks ==========') for task in seed_tasks: print(format_seed_task(task)) if ask_yes_no_question(' Seed this task (y/n)? '): selected_seed_tasks.append(task) seed_tasks = selected_seed_tasks selected_cleanup_tasks = [] print('========== Select cleanup tasks ==========') for task in cleanup_tasks: print(format_cleanup_task(task)) if ask_yes_no_question(' Cleanup this task (y/n)? '): selected_cleanup_tasks.append(task) cleanup_tasks = selected_cleanup_tasks return seed_tasks, cleanup_tasks def main(): return SeedScript()() def ask_yes_no_question(question): while True: resp = raw_input(question).lower() if resp in ('y', 'yes'): return True elif resp in ('n', 'no'): return False def split_comma_seperated_option(option): """ >>> split_comma_seperated_option(['foo,bar', 'baz']) ['foo', 'bar', 'baz'] """ result = [] if option: for args in option: result.extend(args.split(',')) return result if __name__ == '__main__': main() mapproxy-1.11.0/mapproxy/seed/seeder.py000066400000000000000000000461271320454472400201100ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010, 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function, division import sys from collections import deque from contextlib import contextmanager import time try: import Queue except ImportError: import queue as Queue from mapproxy.config import base_config from mapproxy.grid import MetaGrid from mapproxy.source import SourceError from mapproxy.config import local_base_config from mapproxy.compat.itertools import izip_longest from mapproxy.util.lock import LockTimeout from mapproxy.seed.util import format_seed_task, timestamp from mapproxy.seed.cachelock import DummyCacheLocker, CacheLockedError from mapproxy.seed.util import (exp_backoff, limit_sub_bbox, status_symbol, BackoffError) import logging log = logging.getLogger(__name__) NONE = 0 CONTAINS = -1 INTERSECTS = 1 # do not use multiprocessing on windows, it blows # no lambdas, no anonymous functions/classes, no base_config(), etc. if sys.platform == 'win32': import threading proc_class = threading.Thread queue_class = Queue.Queue else: import multiprocessing proc_class = multiprocessing.Process queue_class = multiprocessing.Queue class TileWorkerPool(object): """ Manages multiple TileWorker. """ def __init__(self, task, worker_class, size=2, dry_run=False, progress_logger=None): self.tiles_queue = queue_class(size) self.task = task self.dry_run = dry_run self.procs = [] self.progress_logger = progress_logger conf = base_config() for _ in range(size): worker = worker_class(self.task, self.tiles_queue, conf) worker.start() self.procs.append(worker) def process(self, tiles, progress): if not self.dry_run: while True: try: self.tiles_queue.put(tiles, timeout=5) except Queue.Full: alive = False for proc in self.procs: if proc.is_alive(): alive = True break if not alive: log.warn('no workers left, stopping') raise SeedInterrupted continue else: break if self.progress_logger: self.progress_logger.log_step(progress) def stop(self, force=False): """ Stop seed workers by sending None-sentinel and joining the workers. :param force: Skip sending None-sentinel and join with a timeout. For use when workers might be shutdown already by KeyboardInterrupt. """ if not force: alives = 0 for proc in self.procs: if proc.is_alive(): alives += 1 while alives: # put None-sentinels to queue as long as we have workers alive try: self.tiles_queue.put(None, timeout=1) alives -= 1 except Queue.Full: alives = 0 for proc in self.procs: if proc.is_alive(): alives += 1 if force: timeout = 1.0 else: timeout = None for proc in self.procs: proc.join(timeout) class TileWorker(proc_class): def __init__(self, task, tiles_queue, conf): proc_class.__init__(self) proc_class.daemon = True self.task = task self.tile_mgr = task.tile_manager self.tiles_queue = tiles_queue self.conf = conf def run(self): with local_base_config(self.conf): try: self.work_loop() except KeyboardInterrupt: return except BackoffError: return class TileSeedWorker(TileWorker): def work_loop(self): while True: tiles = self.tiles_queue.get() if tiles is None: return with self.tile_mgr.session(): exp_backoff(self.tile_mgr.load_tile_coords, args=(tiles,), max_repeat=100, max_backoff=600, exceptions=(SourceError, IOError), ignore_exceptions=(LockTimeout, )) class TileCleanupWorker(TileWorker): def work_loop(self): while True: tiles = self.tiles_queue.get() if tiles is None: return with self.tile_mgr.session(): self.tile_mgr.remove_tile_coords(tiles) class SeedProgress(object): def __init__(self, old_progress_identifier=None): self.progress = 0.0 self.level_progress_percentages = [1.0] self.level_progresses = None self.level_progresses_level = 0 self.progress_str_parts = [] self.old_level_progresses = old_progress_identifier def step_forward(self, subtiles=1): self.progress += self.level_progress_percentages[-1] / subtiles @property def progress_str(self): return ''.join(self.progress_str_parts) @contextmanager def step_down(self, i, subtiles): if self.level_progresses is None: self.level_progresses = [] self.level_progresses = self.level_progresses[:self.level_progresses_level] self.level_progresses.append((i, subtiles)) self.level_progresses_level += 1 self.progress_str_parts.append(status_symbol(i, subtiles)) self.level_progress_percentages.append(self.level_progress_percentages[-1] / subtiles) yield self.level_progress_percentages.pop() self.progress_str_parts.pop() self.level_progresses_level -= 1 if self.level_progresses_level == 0: self.level_progresses = [] def already_processed(self): return self.can_skip(self.old_level_progresses, self.level_progresses) def current_progress_identifier(self): if self.already_processed() or self.level_progresses is None: return self.old_level_progresses return self.level_progresses[:] @staticmethod def can_skip(old_progress, current_progress): """ Return True if the `current_progress` is behind the `old_progress` - when it isn't as far as the old progress. >>> SeedProgress.can_skip(None, [(0, 4)]) False >>> SeedProgress.can_skip([], [(0, 4)]) True >>> SeedProgress.can_skip([(0, 4)], None) False >>> SeedProgress.can_skip([(0, 4)], [(0, 4)]) False >>> SeedProgress.can_skip([(1, 4)], [(0, 4)]) True >>> SeedProgress.can_skip([(0, 4)], [(0, 4), (0, 4)]) False >>> SeedProgress.can_skip([(0, 4), (0, 4), (2, 4)], [(0, 4), (0, 4)]) False >>> SeedProgress.can_skip([(0, 4), (0, 4), (2, 4)], [(0, 4), (0, 4), (1, 4)]) True >>> SeedProgress.can_skip([(0, 4), (0, 4), (2, 4)], [(0, 4), (0, 4), (2, 4)]) False >>> SeedProgress.can_skip([(0, 4), (0, 4), (2, 4)], [(0, 4), (0, 4), (3, 4)]) False >>> SeedProgress.can_skip([(0, 4), (0, 4), (2, 4)], [(0, 4), (1, 4)]) False >>> SeedProgress.can_skip([(0, 4), (0, 4), (2, 4)], [(0, 4), (1, 4), (0, 4)]) False """ if current_progress is None: return False if old_progress is None: return False if old_progress == []: return True for old, current in izip_longest(old_progress, current_progress, fillvalue=None): if old is None: return False if current is None: return False if old < current: return False if old > current: return True return False def running(self): return True class StopProcess(Exception): pass class SeedInterrupted(Exception): pass class TileWalker(object): """ TileWalker traverses through all tiles in a tile grid and calls worker_pool.process for each (meta) tile. It traverses the tile grid (pyramid) depth-first. Intersection with coverages are checked before handling subtiles in the next level, allowing to determine if all subtiles should be seeded or skipped. """ def __init__(self, task, worker_pool, handle_stale=False, handle_uncached=False, work_on_metatiles=True, skip_geoms_for_last_levels=0, progress_logger=None, seed_progress=None): self.tile_mgr = task.tile_manager self.task = task self.worker_pool = worker_pool self.handle_stale = handle_stale self.handle_uncached = handle_uncached self.work_on_metatiles = work_on_metatiles self.skip_geoms_for_last_levels = skip_geoms_for_last_levels self.progress_logger = progress_logger num_seed_levels = len(task.levels) if num_seed_levels >= 4: self.report_till_level = task.levels[num_seed_levels-2] else: self.report_till_level = task.levels[num_seed_levels-1] meta_size = self.tile_mgr.meta_grid.meta_size if self.tile_mgr.meta_grid else (1, 1) self.tiles_per_metatile = meta_size[0] * meta_size[1] self.grid = MetaGrid(self.tile_mgr.grid, meta_size=meta_size, meta_buffer=0) self.count = 0 self.seed_progress = seed_progress or SeedProgress() # It is possible that we 'walk' through the same tile multiple times # when seeding irregular tile grids[0]. limit_sub_bbox prevents that we # recurse into the same area multiple times, but it is still possible # that a tile is processed multiple times. Locking prevents that a tile # is seeded multiple times, but it is possible that we count the same tile # multiple times (in dry-mode, or while the tile is in the process queue). # Tile counts can be off by 280% with sqrt2 grids. # We keep a small cache of already processed tiles to skip most duplicates. # A simple cache of 64 tile coordinates for each level already brings the # difference down to ~8%, which is good enough and faster than a more # sophisticated FIFO cache with O(1) lookup, or even caching all tiles. # [0] irregular tile grids: where one tile does not have exactly 4 subtiles # Typically when you use res_factor, or a custom res list. self.seeded_tiles = {l: deque(maxlen=64) for l in task.levels} def walk(self): assert self.handle_stale or self.handle_uncached bbox = self.task.coverage.extent.bbox_for(self.tile_mgr.grid.srs) if self.seed_progress.already_processed(): # nothing to seed self.seed_progress.step_forward() else: try: self._walk(bbox, self.task.levels) except StopProcess: pass self.report_progress(self.task.levels[0], self.task.coverage.bbox) def _walk(self, cur_bbox, levels, current_level=0, all_subtiles=False): """ :param cur_bbox: the bbox to seed in this call :param levels: list of levels to seed :param all_subtiles: seed all subtiles and do not check for intersections with bbox/geom """ bbox_, tiles, subtiles = self.grid.get_affected_level_tiles(cur_bbox, current_level) total_subtiles = tiles[0] * tiles[1] if len(levels) < self.skip_geoms_for_last_levels: # do not filter in last levels all_subtiles = True subtiles = self._filter_subtiles(subtiles, all_subtiles) if current_level in levels and current_level <= self.report_till_level: self.report_progress(current_level, cur_bbox) if not self.seed_progress.running(): if current_level in levels: self.report_progress(current_level, cur_bbox) self.tile_mgr.cleanup() raise StopProcess() process = False; if current_level in levels: levels = levels[1:] process = True for i, (subtile, sub_bbox, intersection) in enumerate(subtiles): if subtile is None: # no intersection self.seed_progress.step_forward(total_subtiles) continue if levels: # recurse to next level sub_bbox = limit_sub_bbox(cur_bbox, sub_bbox) if intersection == CONTAINS: all_subtiles = True else: all_subtiles = False with self.seed_progress.step_down(i, total_subtiles): if self.seed_progress.already_processed(): self.seed_progress.step_forward() else: self._walk(sub_bbox, levels, current_level=current_level+1, all_subtiles=all_subtiles) if not process: continue # check if subtile was already processed. see comment in __init__ if subtile in self.seeded_tiles[current_level]: if not levels: self.seed_progress.step_forward(total_subtiles) continue self.seeded_tiles[current_level].appendleft(subtile) if not self.work_on_metatiles: # collect actual tiles handle_tiles = self.grid.tile_list(subtile) else: handle_tiles = [subtile] if self.handle_uncached: handle_tiles = [t for t in handle_tiles if t is not None and not self.tile_mgr.is_cached(t)] elif self.handle_stale: handle_tiles = [t for t in handle_tiles if t is not None and self.tile_mgr.is_stale(t)] if handle_tiles: self.count += 1 self.worker_pool.process(handle_tiles, self.seed_progress) if not levels: self.seed_progress.step_forward(total_subtiles) if len(levels) >= 4: # call cleanup to close open caches # for connection based caches self.tile_mgr.cleanup() def report_progress(self, level, bbox): if self.progress_logger: self.progress_logger.log_progress(self.seed_progress, level, bbox, self.count * self.tiles_per_metatile) def _filter_subtiles(self, subtiles, all_subtiles): """ Return an iterator with all sub tiles. Yields (None, None, None) for non-intersecting tiles, otherwise (subtile, subtile_bbox, intersection). """ for subtile in subtiles: if subtile is None: yield None, None, None else: sub_bbox = self.grid.meta_tile(subtile).bbox if all_subtiles: intersection = CONTAINS else: intersection = self.task.intersects(sub_bbox) if intersection: yield subtile, sub_bbox, intersection else: yield None, None, None class SeedTask(object): def __init__(self, md, tile_manager, levels, refresh_timestamp, coverage): self.md = md self.tile_manager = tile_manager self.grid = tile_manager.grid self.levels = levels self.refresh_timestamp = refresh_timestamp self.coverage = coverage @property def id(self): return self.md['name'], self.md['cache_name'], self.md['grid_name'] def intersects(self, bbox): if self.coverage.contains(bbox, self.grid.srs): return CONTAINS if self.coverage.intersects(bbox, self.grid.srs): return INTERSECTS return NONE class CleanupTask(object): """ :param coverage: area for the cleanup :param complete_extent: ``True`` if `coverage` equals the extent of the grid """ def __init__(self, md, tile_manager, levels, remove_timestamp, coverage, complete_extent=False): self.md = md self.tile_manager = tile_manager self.grid = tile_manager.grid self.levels = levels self.remove_timestamp = remove_timestamp self.coverage = coverage self.complete_extent = complete_extent @property def id(self): return 'cleanup', self.md['name'], self.md['cache_name'], self.md['grid_name'] def intersects(self, bbox): if self.coverage.contains(bbox, self.grid.srs): return CONTAINS if self.coverage.intersects(bbox, self.grid.srs): return INTERSECTS return NONE def seed(tasks, concurrency=2, dry_run=False, skip_geoms_for_last_levels=0, progress_logger=None, cache_locker=None): if cache_locker is None: cache_locker = DummyCacheLocker() active_tasks = tasks[::-1] while active_tasks: task = active_tasks[-1] print(format_seed_task(task)) wait = len(active_tasks) == 1 try: with cache_locker.lock(task.md['cache_name'], no_block=not wait): if progress_logger and progress_logger.progress_store: progress_logger.current_task_id = task.id start_progress = progress_logger.progress_store.get(task.id) else: start_progress = None seed_progress = SeedProgress(old_progress_identifier=start_progress) seed_task(task, concurrency, dry_run, skip_geoms_for_last_levels, progress_logger, seed_progress=seed_progress) except CacheLockedError: print(' ...cache is locked, skipping') active_tasks = [task] + active_tasks[:-1] else: active_tasks.pop() def seed_task(task, concurrency=2, dry_run=False, skip_geoms_for_last_levels=0, progress_logger=None, seed_progress=None): if task.coverage is False: return if task.refresh_timestamp is not None: task.tile_manager._expire_timestamp = task.refresh_timestamp task.tile_manager.minimize_meta_requests = False tile_worker_pool = TileWorkerPool(task, TileSeedWorker, dry_run=dry_run, size=concurrency, progress_logger=progress_logger) tile_walker = TileWalker(task, tile_worker_pool, handle_uncached=True, skip_geoms_for_last_levels=skip_geoms_for_last_levels, progress_logger=progress_logger, seed_progress=seed_progress) try: tile_walker.walk() except KeyboardInterrupt: tile_worker_pool.stop(force=True) raise finally: tile_worker_pool.stop() mapproxy-1.11.0/mapproxy/seed/spec.py000066400000000000000000000042341320454472400175640ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.util.ext.dictspec.validator import validate, ValidationError from mapproxy.util.ext.dictspec.spec import one_off, anything, number from mapproxy.util.ext.dictspec.spec import required from mapproxy.config.spec import coverage def validate_seed_conf(conf_dict): """ Validate `conf_dict` agains seed.yaml spec. Returns lists with errors. List is empty when no errors where found. """ try: validate(seed_yaml_spec, conf_dict) except ValidationError as ex: return ex.errors, ex.informal_only else: return [], True time_spec = { 'seconds': number(), 'minutes': number(), 'hours': number(), 'days': number(), 'weeks': number(), 'time': anything(), 'mtime': str(), } from_to_spec = { 'from': number(), 'to': number(), } seed_yaml_spec = { 'coverages': { anything(): coverage, }, 'seeds': { anything(): { required('caches'): [str()], 'grids': [str()], 'coverages': [str()], 'refresh_before': time_spec, 'levels': one_off([int()], from_to_spec), 'resolutions': one_off([int()], from_to_spec), }, }, 'cleanups': { anything(): { required('caches'): [str()], 'grids': [str()], 'coverages': [str()], 'remove_before': time_spec, 'remove_all': bool(), 'levels': one_off([int()], from_to_spec), 'resolutions': one_off([int()], from_to_spec), } }, } mapproxy-1.11.0/mapproxy/seed/util.py000066400000000000000000000201721320454472400176060ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010, 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function, division import os import sys import stat import math import time from datetime import datetime try: import cPickle as pickle except ImportError: import pickle from mapproxy.layer import map_extent_from_grid from mapproxy.util.fs import write_atomic import logging log = logging.getLogger(__name__) class bidict(dict): """ Simplest bi-directional dictionary. """ def __init__(self, iterator): for key, val in iterator: dict.__setitem__(self, key, val) dict.__setitem__(self, val, key) class ProgressStore(object): """ Reads and stores seed progresses to a file. """ def __init__(self, filename=None, continue_seed=True): self.filename = filename if continue_seed: self.status = self.load() else: self.status = {} def load(self): if not os.path.exists(self.filename): pass elif os.stat(self.filename).st_mode & stat.S_IWOTH: log.error('progress file (%s) is world writable, ignoring file', self.filename) else: with open(self.filename, 'rb') as f: try: return pickle.load(f) except (pickle.UnpicklingError, AttributeError, EOFError, ImportError, IndexError): log.error('unable to read progress file (%s), ignoring file', self.filename) return {} def write(self): try: write_atomic(self.filename, pickle.dumps(self.status)) except (IOError, OSError) as ex: log.error('unable to write seed progress: %s', ex) def remove(self): self.status = {} if os.path.exists(self.filename): os.remove(self.filename) def get(self, task_identifier): return self.status.get(task_identifier, None) def add(self, task_identifier, progress_identifier): self.status[task_identifier] = progress_identifier class ProgressLog(object): def __init__(self, out=None, silent=False, verbose=True, progress_store=None): if not out: out = sys.stdout self.out = out self._laststep = time.time() self._lastprogress = 0 self.verbose = verbose self.silent = silent self.current_task_id = None self.progress_store = progress_store def log_message(self, msg): self.out.write('[%s] %s\n' % ( timestamp(), msg, )) self.out.flush() def log_step(self, progress): if not self.verbose: return if (self._laststep + .5) < time.time(): # log progress at most every 500ms self.out.write('[%s] %6.2f%%\t%-20s \r' % ( timestamp(), progress.progress*100, progress.progress_str, )) self.out.flush() self._laststep = time.time() def log_progress(self, progress, level, bbox, tiles): progress_interval = 1 if not self.verbose: progress_interval = 30 log_progess = False if progress.progress == 1.0 or (self._lastprogress + progress_interval) < time.time(): self._lastprogress = time.time() log_progess = True if log_progess: if self.progress_store and self.current_task_id: self.progress_store.add(self.current_task_id, progress.current_progress_identifier()) self.progress_store.write() if self.silent: return if log_progess: self.out.write('[%s] %2s %6.2f%% %s (%d tiles)\n' % ( timestamp(), level, progress.progress*100, format_bbox(bbox), tiles)) self.out.flush() def limit_sub_bbox(bbox, sub_bbox): """ >>> limit_sub_bbox((0, 1, 10, 11), (-1, -1, 9, 8)) (0, 1, 9, 8) >>> limit_sub_bbox((0, 0, 10, 10), (5, 2, 18, 18)) (5, 2, 10, 10) """ minx = max(bbox[0], sub_bbox[0]) miny = max(bbox[1], sub_bbox[1]) maxx = min(bbox[2], sub_bbox[2]) maxy = min(bbox[3], sub_bbox[3]) return minx, miny, maxx, maxy def timestamp(): return datetime.now().strftime('%H:%M:%S') def format_bbox(bbox): return ('%.5f, %.5f, %.5f, %.5f') % tuple(bbox) def status_symbol(i, total): """ >>> status_symbol(0, 1) '0' >>> [status_symbol(i, 4) for i in range(5)] ['.', 'o', 'O', '0', 'X'] >>> [status_symbol(i, 10) for i in range(11)] ['.', '.', 'o', 'o', 'o', 'O', 'O', '0', '0', '0', 'X'] """ symbols = list(' .oO0') i += 1 if 0 < i > total: return 'X' else: return symbols[int(math.ceil(i/(total/4)))] class BackoffError(Exception): pass def exp_backoff(func, args=(), kw={}, max_repeat=10, start_backoff_sec=2, exceptions=(Exception,), ignore_exceptions=tuple(), max_backoff=60): n = 0 while True: try: result = func(*args, **kw) except ignore_exceptions: time.sleep(0.01) except exceptions as ex: if n >= max_repeat: print >>sys.stderr, "An error occured. Giving up" raise BackoffError wait_for = start_backoff_sec * 2**n if wait_for > max_backoff: wait_for = max_backoff print("An error occured. Retry in %d seconds: %r. Retries left: %d" % (wait_for, ex, (max_repeat - n)), file=sys.stderr) time.sleep(wait_for) n += 1 else: return result def format_seed_task(task): info = [] info.append(' %s:' % (task.md['name'], )) if task.coverage is False: info.append(" Empty coverage given for this task") info.append(" Skipped") return '\n'.join(info) info.append(" Seeding cache '%s' with grid '%s' in %s" % ( task.md['cache_name'], task.md['grid_name'], task.grid.srs.srs_code)) if task.coverage: info.append(' Limited to coverage in: %s (EPSG:4326)' % (format_bbox(task.coverage.extent.llbbox), )) else: info.append(' Complete grid: %s (EPSG:4326)' % (format_bbox(map_extent_from_grid(task.grid).llbbox), )) info.append(' Levels: %s' % (task.levels, )) if task.refresh_timestamp: info.append(' Overwriting: tiles older than %s' % datetime.fromtimestamp(task.refresh_timestamp)) elif task.refresh_timestamp == 0: info.append(' Overwriting: all tiles') else: info.append(' Overwriting: no tiles') return '\n'.join(info) def format_cleanup_task(task): info = [] info.append(' %s:' % (task.md['name'], )) if task.coverage is False: info.append(" Empty coverage given for this task") info.append(" Skipped") return '\n'.join(info) info.append(" Cleaning up cache '%s' with grid '%s' in %s" % ( task.md['cache_name'], task.md['grid_name'], task.grid.srs.srs_code)) if task.coverage: info.append(' Limited to coverage in: %s (EPSG:4326)' % (format_bbox(task.coverage.extent.llbbox), )) else: info.append(' Complete grid: %s (EPSG:4326)' % (format_bbox(map_extent_from_grid(task.grid).llbbox), )) info.append(' Levels: %s' % (task.levels, )) if task.remove_timestamp: info.append(' Remove: tiles older than %s' % datetime.fromtimestamp(task.remove_timestamp)) else: info.append(' Remove: all tiles') return '\n'.join(info) mapproxy-1.11.0/mapproxy/service/000077500000000000000000000000001320454472400167755ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/service/__init__.py000066400000000000000000000012061320454472400211050ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. mapproxy-1.11.0/mapproxy/service/base.py000066400000000000000000000032341320454472400202630ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Service handler (WMS, TMS, etc.). """ from mapproxy.exception import RequestError class Server(object): names = tuple() request_parser = lambda x: None request_methods = () def handle(self, req): try: parsed_req = self.parse_request(req) handler = getattr(self, parsed_req.request_handler_name) return handler(parsed_req) except RequestError as e: return e.render() def parse_request(self, req): return self.request_parser(req) def decorate_img(self, image, service, layers, environ, query_extent): """ Callback that allows the ImageSource associated with a response to be modified before it is returned. The callback is passed the ImageSource instance and must return a valid ImageSource """ if 'mapproxy.decorate_img' in environ: image = environ['mapproxy.decorate_img']( image, service, layers, environ=environ, query_extent=query_extent) return image mapproxy-1.11.0/mapproxy/service/demo.py000066400000000000000000000266151320454472400203050ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Demo service handler """ from __future__ import division import os import pkg_resources import mimetypes from collections import defaultdict from xml.sax.saxutils import escape from mapproxy.config.config import base_config from mapproxy.compat import PY2 from mapproxy.exception import RequestError from mapproxy.service.base import Server from mapproxy.response import Response from mapproxy.srs import SRS, get_epsg_num from mapproxy.layer import SRSConditional, CacheMapLayer, ResolutionConditional from mapproxy.source.wms import WMSSource if PY2: import urllib2 else: from urllib import request as urllib2 from mapproxy.template import template_loader, bunch env = {'bunch': bunch} get_template = template_loader(__name__, 'templates', namespace=env) def static_filename(name): if base_config().template_dir: return os.path.join(base_config().template_dir, name) else: return pkg_resources.resource_filename(__name__, os.path.join('templates', name)) class DemoServer(Server): names = ('demo',) def __init__(self, layers, md, request_parser=None, tile_layers=None, srs=None, image_formats=None, services=None, restful_template=None): Server.__init__(self) self.layers = layers self.tile_layers = tile_layers or {} self.md = md self.image_formats = image_formats filter_image_format = [] for format in self.image_formats: if 'image/jpeg' == format or 'image/png' == format: filter_image_format.append(format) self.image_formats = filter_image_format self.srs = srs self.services = services or [] self.restful_template = restful_template def handle(self, req): if req.path.startswith('/demo/static/'): filename = req.path.lstrip('/') filename = static_filename(filename) if not os.path.isfile(filename): return Response('file not found', content_type='text/plain', status=404) type, encoding = mimetypes.guess_type(filename) return Response(open(filename, 'rb'), content_type=type) # we don't authorize the static files (css, js) # since they are not confidential try: authorized = self.authorized_demo(req.environ) except RequestError as ex: return ex.render() if not authorized: return Response('forbidden', content_type='text/plain', status=403) if 'wms_layer' in req.args: demo = self._render_wms_template('demo/wms_demo.html', req) elif 'tms_layer' in req.args: demo = self._render_tms_template('demo/tms_demo.html', req) elif 'wmts_layer' in req.args: demo = self._render_wmts_template('demo/wmts_demo.html', req) elif 'wms_capabilities' in req.args: url = '%s/service?REQUEST=GetCapabilities'%(req.script_url) capabilities = urllib2.urlopen(url) demo = self._render_capabilities_template('demo/capabilities_demo.html', capabilities, 'WMS', url) elif 'wmsc_capabilities' in req.args: url = '%s/service?REQUEST=GetCapabilities&tiled=true'%(req.script_url) capabilities = urllib2.urlopen(url) demo = self._render_capabilities_template('demo/capabilities_demo.html', capabilities, 'WMS-C', url) elif 'wmts_capabilities_kvp' in req.args: url = '%s/service?REQUEST=GetCapabilities&SERVICE=WMTS' % (req.script_url) capabilities = urllib2.urlopen(url) demo = self._render_capabilities_template('demo/capabilities_demo.html', capabilities, 'WMTS', url) elif 'wmts_capabilities' in req.args: url = '%s/wmts/1.0.0/WMTSCapabilities.xml' % (req.script_url) capabilities = urllib2.urlopen(url) demo = self._render_capabilities_template('demo/capabilities_demo.html', capabilities, 'WMTS', url) elif 'tms_capabilities' in req.args: if 'layer' in req.args and 'srs' in req.args: # prevent dir traversal (seems it's not possible with urllib2, but better safe then sorry) layer = req.args['layer'].replace('..', '') srs = req.args['srs'].replace('..', '') url = '%s/tms/1.0.0/%s/%s'%(req.script_url, layer, srs) else: url = '%s/tms/1.0.0/'%(req.script_url) capabilities = urllib2.urlopen(url) demo = self._render_capabilities_template('demo/capabilities_demo.html', capabilities, 'TMS', url) elif req.path == '/demo/': demo = self._render_template('demo/demo.html') else: resp = Response('', status=301) resp.headers['Location'] = req.script_url.rstrip('/') + '/demo/' return resp return Response(demo, content_type='text/html') def layer_srs(self, layer): """ Return a list tuples with title and name of all SRS for the layer. The title of SRS that are native to the layer are suffixed with a '*'. """ cached_srs = [] for map_layer in layer.map_layers: # TODO unify map_layers interface if isinstance(map_layer, SRSConditional): for srs_key in map_layer.srs_map.keys(): cached_srs.append(srs_key.srs_code) elif isinstance(map_layer, CacheMapLayer): cached_srs.append(map_layer.grid.srs.srs_code) elif isinstance(map_layer, ResolutionConditional): cached_srs.append(map_layer.srs.srs_code) elif isinstance(map_layer, WMSSource): if map_layer.supported_srs: for supported_srs in map_layer.supported_srs: cached_srs.append(supported_srs.srs_code) uncached_srs = [] for srs_code in self.srs: if srs_code not in cached_srs: uncached_srs.append(srs_code) sorted_cached_srs = sorted(cached_srs, key=lambda srs: get_epsg_num(srs)) sorted_uncached_srs = sorted(uncached_srs, key=lambda srs: get_epsg_num(srs)) sorted_cached_srs = [(s + '*', s) for s in sorted_cached_srs] sorted_uncached_srs = [(s, s) for s in sorted_uncached_srs] return sorted_cached_srs + sorted_uncached_srs def _render_template(self, template): template = get_template(template, default_inherit="demo/static.html") tms_tile_layers = defaultdict(list) for layer in self.tile_layers: name = self.tile_layers[layer].md.get('name') tms_tile_layers[name].append(self.tile_layers[layer]) wmts_layers = tms_tile_layers.copy() return template.substitute(layers=self.layers, formats=self.image_formats, srs=self.srs, layer_srs=self.layer_srs, tms_layers=tms_tile_layers, wmts_layers=wmts_layers, services=self.services) def _render_wms_template(self, template, req): template = get_template(template, default_inherit="demo/static.html") layer = self.layers[req.args['wms_layer']] srs = escape(req.args['srs']) bbox = layer.extent.bbox_for(SRS(srs)) width = bbox[2] - bbox[0] height = bbox[3] - bbox[1] min_res = max(width/256, height/256) return template.substitute(layer=layer, image_formats=self.image_formats, format=escape(req.args['format']), srs=srs, layer_srs=self.layer_srs, bbox=bbox, res=min_res) def _render_tms_template(self, template, req): template = get_template(template, default_inherit="demo/static.html") for layer in self.tile_layers.values(): if (layer.name == req.args['tms_layer'] and layer.grid.srs.srs_code == req.args['srs']): tile_layer = layer break resolutions = tile_layer.grid.tile_sets res = [] for level, resolution in resolutions: res.append(resolution) if tile_layer.grid.srs.is_latlong: units = 'degree' else: units = 'm' if tile_layer.grid.profile == 'local': add_res_to_options = True else: add_res_to_options = False return template.substitute(layer=tile_layer, srs=escape(req.args['srs']), format=escape(req.args['format']), resolutions=res, units=units, add_res_to_options=add_res_to_options, all_tile_layers=self.tile_layers) def _render_wmts_template(self, template, req): template = get_template(template, default_inherit="demo/static.html") for layer in self.tile_layers.values(): if (layer.name == req.args['wmts_layer'] and layer.grid.srs.srs_code == req.args['srs']): wmts_layer = layer break restful_url = self.restful_template.replace('{Layer}', wmts_layer.name, 1) if '{Format}' in restful_url: restful_url = restful_url.replace('{Format}', wmts_layer.format) if wmts_layer.grid.srs.is_latlong: units = 'degree' else: units = 'm' return template.substitute(layer=wmts_layer, matrix_set=wmts_layer.grid.name, format=escape(req.args['format']), srs=escape(req.args['srs']), resolutions=wmts_layer.grid.resolutions, units=units, all_tile_layers=self.tile_layers, restful_url=restful_url) def _render_capabilities_template(self, template, xmlfile, service, url): template = get_template(template, default_inherit="demo/static.html") return template.substitute(capabilities = xmlfile, service = service, url = url) def authorized_demo(self, environ): if 'mapproxy.authorize' in environ: result = environ['mapproxy.authorize']('demo', [], environ=environ) if result['authorized'] == 'unauthenticated': raise RequestError('unauthorized', status=401) if result['authorized'] == 'full': return True return False return True mapproxy-1.11.0/mapproxy/service/kml.py000066400000000000000000000300761320454472400201400ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import re from mapproxy.response import Response from mapproxy.exception import RequestError, PlainExceptionHandler from mapproxy.service.base import Server from mapproxy.request.tile import TileRequest from mapproxy.srs import SRS from mapproxy.util.coverage import load_limited_to class KMLRequest(TileRequest): """ Class for TMS-like KML requests. """ request_handler_name = 'map' req_prefix = '/kml' tile_req_re = re.compile(r'''^(?P/kml)/ (?P[^/]+)/ ((?P[^/]+)/)? (?P-?\d+)/ (?P-?\d+)/ (?P-?\d+)\.(?P\w+)''', re.VERBOSE) def __init__(self, request): TileRequest.__init__(self, request) if self.format == 'kml': self.request_handler_name = 'kml' @property def exception_handler(self): return PlainExceptionHandler() class KMLInitRequest(TileRequest): """ Class for TMS-like KML requests. """ request_handler_name = 'map' req_prefix = '/kml' tile_req_re = re.compile(r'''^(?P/kml)/ (?P[^/]+) (/(?P[^/]+))? /?$ ''', re.VERBOSE) def __init__(self, request): self.http = request self.tile = (0, 0, 0) self.format = 'kml' self.request_handler_name = 'kml' self._init_request() @property def exception_handler(self): return PlainExceptionHandler() def kml_request(req): if KMLInitRequest.tile_req_re.match(req.path): return KMLInitRequest(req) else: return KMLRequest(req) class KMLServer(Server): """ OGC KML 2.2 Server """ names = ('kml',) request_parser = staticmethod(kml_request) request_methods = ('map', 'kml') def __init__(self, layers, md, max_tile_age=None, use_dimension_layers=False): Server.__init__(self) self.layers = layers self.md = md self.max_tile_age = max_tile_age self.use_dimension_layers = use_dimension_layers def map(self, map_request): """ :return: the requested tile """ # force 'sw' origin for kml map_request.origin = 'sw' layer = self.layer(map_request) limit_to = self.authorize_tile_layer(layer, map_request) tile = layer.render(map_request, coverage=limit_to) tile_format = getattr(tile, 'format', map_request.format) resp = Response(tile.as_buffer(), content_type='image/' + tile_format) resp.cache_headers(tile.timestamp, etag_data=(tile.timestamp, tile.size), max_age=self.max_tile_age) resp.make_conditional(map_request.http) return resp def authorize_tile_layer(self, tile_layer, request): if 'mapproxy.authorize' in request.http.environ: if request.tile: query_extent = (tile_layer.grid.srs.srs_code, tile_layer.tile_bbox(request, use_profiles=request.use_profiles)) else: query_extent = None # for layer capabilities result = request.http.environ['mapproxy.authorize']('kml', [tile_layer.name], query_extent=query_extent, environ=request.http.environ) if result['authorized'] == 'unauthenticated': raise RequestError('unauthorized', status=401) if result['authorized'] == 'full': return if result['authorized'] == 'partial': if result['layers'].get(tile_layer.name, {}).get('tile', False) == True: limited_to = result['layers'][tile_layer.name].get('limited_to') if not limited_to: limited_to = result.get('limited_to') if limited_to: return load_limited_to(limited_to) else: return None raise RequestError('forbidden', status=403) def _internal_layer(self, tile_request): if '_layer_spec' in tile_request.dimensions: name = tile_request.layer + '_' + tile_request.dimensions['_layer_spec'] else: name = tile_request.layer if name in self.layers: return self.layers[name] if name + '_EPSG4326' in self.layers: return self.layers[name + '_EPSG4326'] if name + '_EPSG900913' in self.layers: return self.layers[name + '_EPSG900913'] return None def _internal_dimension_layer(self, tile_request): key = (tile_request.layer, tile_request.dimensions.get('_layer_spec')) return self.layers.get(key) def layer(self, tile_request): if self.use_dimension_layers: internal_layer = self._internal_dimension_layer(tile_request) else: internal_layer = self._internal_layer(tile_request) if internal_layer is None: raise RequestError('unknown layer: ' + tile_request.layer, request=tile_request) return internal_layer def kml(self, map_request): """ :return: the rendered KML response """ # force 'sw' origin for kml map_request.origin = 'sw' layer = self.layer(map_request) self.authorize_tile_layer(layer, map_request) tile_coord = map_request.tile initial_level = False if tile_coord[2] == 0: initial_level = True bbox = self._tile_wgs_bbox(map_request, layer, limit=True) if bbox is None: raise RequestError('The requested tile is outside the bounding box ' 'of the tile map.', request=map_request) tile = SubTile(tile_coord, bbox) subtiles = self._get_subtiles(map_request, layer) tile_size = layer.grid.tile_size[0] url = map_request.http.script_url.rstrip('/') result = KMLRenderer().render(tile=tile, subtiles=subtiles, layer=layer, url=url, name=map_request.layer, format=layer.format, name_path=layer.md['name_path'], initial_level=initial_level, tile_size=tile_size) resp = Response(result, content_type='application/vnd.google-earth.kml+xml') resp.cache_headers(etag_data=(result,), max_age=self.max_tile_age) resp.make_conditional(map_request.http) return resp def _get_subtiles(self, tile_request, layer): """ Create four `SubTile` for the next level of `tile`. """ tile = tile_request.tile bbox = layer.tile_bbox(tile_request, use_profiles=tile_request.use_profiles, limit=True) level = layer.grid.internal_tile_coord((tile[0], tile[1], tile[2]+1), use_profiles=False)[2] bbox_, tile_grid_, tiles = layer.grid.get_affected_level_tiles(bbox, level) subtiles = [] for coord in tiles: if coord is None: continue sub_bbox = layer.grid.tile_bbox(coord) if sub_bbox is not None: # only add subtiles where the lower left corner is in the bbox # to prevent subtiles to appear in multiple KML docs DELTA = -1.0/10e6 if (sub_bbox[0] - bbox[0]) > DELTA and (sub_bbox[1] - bbox[1]) > DELTA: sub_bbox_wgs = self._tile_bbox_to_wgs(sub_bbox, layer.grid) coord = layer.grid.external_tile_coord(coord, use_profiles=False) if layer.grid.origin not in ('ll', 'sw', None): coord = layer.grid.flip_tile_coord(coord) subtiles.append(SubTile(coord, sub_bbox_wgs)) return subtiles def _tile_wgs_bbox(self, tile_request, layer, limit=False): bbox = layer.tile_bbox(tile_request, use_profiles=tile_request.use_profiles, limit=limit) if bbox is None: return None return self._tile_bbox_to_wgs(bbox, layer.grid) def _tile_bbox_to_wgs(self, src_bbox, grid): bbox = grid.srs.transform_bbox_to(SRS(4326), src_bbox, with_points=4) if grid.srs == SRS(900913): bbox = list(bbox) if abs(src_bbox[1] - -20037508.342789244) < 0.1: bbox[1] = -90.0 if abs(src_bbox[3] - 20037508.342789244) < 0.1: bbox[3] = 90.0 return bbox def check_map_request(self, map_request): if map_request.layer not in self.layers: raise RequestError('unknown layer: ' + map_request.layer, request=map_request) class SubTile(object): """ Contains the ``bbox`` and ``coord`` of a sub tile. """ def __init__(self, coord, bbox): self.coord = coord self.bbox = bbox class KMLRenderer(object): header = """ %(layer_name)s %(north)f%(south)f %(east)f%(west)f """ network_link = """ %(layer_name)s - %(coord)s %(north)f%(south)f %(east)f%(west)f %(min_lod)d -1 %(href)s onRegion """ ground_overlay = """ %(coord)s %(north)f%(south)f %(east)f%(west)f %(min_lod)d %(max_lod)d 8 8 %(level)d %(href)s %(north)f%(south)f %(east)f%(west)f """ footer = """ """ def render(self, tile, subtiles, layer, url, name, name_path, format, initial_level, tile_size): response = [] response.append(self.header % dict(east=tile.bbox[2], south=tile.bbox[1], west=tile.bbox[0], north=tile.bbox[3], layer_name=name)) name_path = '/'.join(name_path) for subtile in subtiles: kml_href = '%s/kml/%s/%d/%d/%d.kml' % (url, name_path, subtile.coord[2], subtile.coord[0], subtile.coord[1]) response.append(self.network_link % dict(east=subtile.bbox[2], south=subtile.bbox[1], west=subtile.bbox[0], north=subtile.bbox[3], min_lod=tile_size/2, href=kml_href, layer_name=name, coord=subtile.coord)) for subtile in subtiles: tile_href = '%s/kml/%s/%d/%d/%d.%s' % ( url, name_path, subtile.coord[2], subtile.coord[0], subtile.coord[1], layer.format) response.append(self.ground_overlay % dict(east=subtile.bbox[2], south=subtile.bbox[1], west=subtile.bbox[0], north=subtile.bbox[3], coord=subtile.coord, min_lod=tile_size/2, max_lod=tile_size*3, href=tile_href, level=subtile.coord[2])) response.append(self.footer) return ''.join(response)mapproxy-1.11.0/mapproxy/service/ows.py000066400000000000000000000025151320454472400201620ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Wrapper service handler for all OWS services (/service?). """ class OWSServer(object): """ Wraps all OWS services (/service?, /ows?, /wms?, /wmts?) and dispatches requests based on the ``services`` query argument. """ def __init__(self, services): self.names = ['service', 'ows'] self.services = {} for service in services: if service.service == 'wms' and 'wms' not in self.names: self.names.append('wms') self.services[service.service] = service def handle(self, req): service = req.args.get('service', 'wms').lower() assert service in self.services return self.services[service].handle(req) mapproxy-1.11.0/mapproxy/service/template_helper.py000066400000000000000000000030771320454472400225300ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from cgi import escape from mapproxy.template import bunch __all__ = ['escape', 'indent', 'bunch', 'wms100format', 'wms100info_format', 'wms111metadatatype'] def indent(text, n=2): return '\n'.join(' '*n + line for line in text.split('\n')) def wms100format(format): """ >>> wms100format('image/png') 'PNG' >>> wms100format('image/GeoTIFF') """ _mime_class, sub_type = format.split('/') sub_type = sub_type.upper() if sub_type in ['PNG', 'TIFF', 'GIF', 'JPEG']: return sub_type else: return None def wms100info_format(format): """ >>> wms100info_format('text/html') 'MIME' >>> wms100info_format('application/vnd.ogc.gml') 'GML.1' """ if format in ('application/vnd.ogc.gml', 'text/xml'): return 'GML.1' return 'MIME' def wms111metadatatype(type): if type == 'ISO19115:2003': return 'TC211' if type == 'FGDC:1998': return 'FGDC' mapproxy-1.11.0/mapproxy/service/templates/000077500000000000000000000000001320454472400207735ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/service/templates/demo/000077500000000000000000000000001320454472400217175ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/service/templates/demo/capabilities_demo.html000066400000000000000000000007111320454472400262410ustar00rootroot00000000000000{{py: import cgi import textwrap wrapper = textwrap.TextWrapper(replace_whitespace=False, width=90, break_long_words=False) menu_title = "Capabilities" jscript_openlayers = None jscript_functions = None }}

{{service}} GetCapabilities

{{url}}
{{for line in capabilities}}
{{cgi.escape(wrapper.fill(line.decode('utf8')))}}
{{endfor}}
            
mapproxy-1.11.0/mapproxy/service/templates/demo/demo.html000066400000000000000000000175331320454472400235420ustar00rootroot00000000000000{{py: from mapproxy.compat import PY2 if PY2: from urllib import quote, quote_plus else: from urllib.parse import quote, quote_plus from mapproxy.version import version def strip(s): return s.split('/')[1] def replace(s): return s.replace(':','') menu_title=None jscript_openlayers=None }} {{def jscript_functions}} {{enddef}}

About

MapProxy Version {{version}}

WMS

{{if 'wms' in services}}
Capabilities document (download as xml) (view as html)
{{if 'wms_111' in services }} {{for layer in layers.values()}} {{for loop, format in looper(formats)}} {{endfor}} {{endfor}}
Layer Coordinate-System Image-Format
{{layer.name}}
{{format | strip}}

Coordinate systems marked with * are supported without reprojection.

{{else}}
The demo service only supports WMS 1.1.1. Enable 1.1.1 to see a list of your configured layers.
{{endif}} {{else}}
This service is not available with the current configuration.
{{endif}}

WMS-C

{{if 'wms' in services}}
Capabilities document (download as xml) (view as html)
{{else}}
This service is not available with the current configuration.
{{endif}}

WMTS

{{if 'wmts' in services}} {{if 'wmts_kvp' in services}}
KVP capabilities document (download as xml) (view as html)
{{endif}} {{if 'wmts_restful' in services}}
RESTFul capabilities document (download as xml) (view as html)
{{endif}} {{for wmts_layer_name, wmts_layers in wmts_layers.items()}} {{for loop, layer in looper(wmts_layers)}} {{if not loop.first}} {{endif}} {{if layer.grid.supports_access_with_origin('nw')}} {{endif}} {{endfor}} {{endfor}}
Layer Coordinate-System Image-Format
{{wmts_layer_name}}
{{layer.grid.srs.srs_code}} {{layer.format}} {{else}} {{layer.grid.name}} not compatible with WMTS
{{else}}
This service is not available with the current configuration.
{{endif}}

TMS

{{if 'tms' in services}}
Capabilities document (download as xml) (view as html)
{{for tms_layer_name, tms_layers in tms_layers.items()}} {{for loop, layer in looper(tms_layers)}} {{if not loop.first}} {{endif}} {{if layer.grid.supports_access_with_origin('sw')}} {{else}} {{endif}} {{endfor}} {{endfor}}
Layer Coordinate-System Image-Format Layer Capabilities
{{tms_layer_name}}
{{layer.grid.srs.srs_code}} {{layer.format}} click here {{layer.grid.name}} not compatible with TMS
{{else}}
This service is not available with the current configuration.
{{endif}} mapproxy-1.11.0/mapproxy/service/templates/demo/openlayers-demo.cfg000066400000000000000000000004201320454472400254770ustar00rootroot00000000000000[first] [last] [include] OpenLayers/Control.js OpenLayers/Map.js OpenLayers/Control/Attribution.js OpenLayers/Control/Navigation.js OpenLayers/Control/PanZoom.js OpenLayers/Projection.js OpenLayers/Layer/WMS.js OpenLayers/Layer/TMS.js OpenLayers/Layer/WMTS.js [exclude]mapproxy-1.11.0/mapproxy/service/templates/demo/static.html000066400000000000000000000017061320454472400241000ustar00rootroot00000000000000 MapProxy Demo {{if jscript_openlayers !=None }} {{else}} {{endif}}
{{self.body}}
{{jscript_openlayers}} {{jscript_functions}} mapproxy-1.11.0/mapproxy/service/templates/demo/static/000077500000000000000000000000001320454472400232065ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/service/templates/demo/static/OpenLayers.js000066400000000000000000005000741320454472400256330ustar00rootroot00000000000000/* OpenLayers.js -- OpenLayers Map Viewer Library Copyright (c) 2006-2012 by OpenLayers Contributors Published under the 2-clause BSD license. See http://openlayers.org/dev/license.txt for the full text of the license, and http://openlayers.org/dev/authors.txt for full list of contributors. Includes compressed code under the following licenses: (For uncompressed versions of the code used, please see the OpenLayers Github repository: ) */ /** * Contains XMLHttpRequest.js * Copyright 2007 Sergey Ilinsky (http://www.ilinsky.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 */ /** * OpenLayers.Util.pagePosition is based on Yahoo's getXY method, which is * Copyright (c) 2006, Yahoo! Inc. * All rights reserved. * * Redistribution and use of this software in source and binary forms, with or * without modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * * Neither the name of Yahoo! Inc. nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission of Yahoo! Inc. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ var OpenLayers={VERSION_NUMBER:"Release 2.12",singleFile:true,_getScriptLocation:(function(){var r=new RegExp("(^|(.*?\\/))(OpenLayers[^\\/]*?\\.js)(\\?|$)"),s=document.getElementsByTagName('script'),src,m,l="";for(var i=0,len=s.length;i1){var newArgs=[C,P].concat(Array.prototype.slice.call(arguments).slice(1,len-1),F);OpenLayers.inherit.apply(null,newArgs);}else{C.prototype=F;} return C;};OpenLayers.inherit=function(C,P){var F=function(){};F.prototype=P.prototype;C.prototype=new F;var i,l,o;for(i=2,l=arguments.length;i0?duration:Number.POSITIVE_INFINITY;var id=++counter;var start=+new Date;loops[id]=function(){if(loops[id]&&+new Date-start<=duration){callback();if(loops[id]){requestFrame(loops[id],element);}}else{delete loops[id];}};requestFrame(loops[id],element);return id;} function stop(id){delete loops[id];} return{isNative:isNative,requestFrame:requestFrame,start:start,stop:stop};})(window);OpenLayers.Tween=OpenLayers.Class({easing:null,begin:null,finish:null,duration:null,callbacks:null,time:null,animationId:null,playing:false,initialize:function(easing){this.easing=(easing)?easing:OpenLayers.Easing.Expo.easeOut;},start:function(begin,finish,duration,options){this.playing=true;this.begin=begin;this.finish=finish;this.duration=duration;this.callbacks=options.callbacks;this.time=0;OpenLayers.Animation.stop(this.animationId);this.animationId=null;if(this.callbacks&&this.callbacks.start){this.callbacks.start.call(this,this.begin);} this.animationId=OpenLayers.Animation.start(OpenLayers.Function.bind(this.play,this));},stop:function(){if(!this.playing){return;} if(this.callbacks&&this.callbacks.done){this.callbacks.done.call(this,this.finish);} OpenLayers.Animation.stop(this.animationId);this.animationId=null;this.playing=false;},play:function(){var value={};for(var i in this.begin){var b=this.begin[i];var f=this.finish[i];if(b==null||f==null||isNaN(b)||isNaN(f)){throw new TypeError('invalid value for Tween');} var c=f-b;value[i]=this.easing.apply(this,[this.time,b,c,this.duration]);} this.time++;if(this.callbacks&&this.callbacks.eachStep){this.callbacks.eachStep.call(this,value);} if(this.time>this.duration){this.stop();}},CLASS_NAME:"OpenLayers.Tween"});OpenLayers.Easing={CLASS_NAME:"OpenLayers.Easing"};OpenLayers.Easing.Linear={easeIn:function(t,b,c,d){return c*t/d+b;},easeOut:function(t,b,c,d){return c*t/d+b;},easeInOut:function(t,b,c,d){return c*t/d+b;},CLASS_NAME:"OpenLayers.Easing.Linear"};OpenLayers.Easing.Expo={easeIn:function(t,b,c,d){return(t==0)?b:c*Math.pow(2,10*(t/d-1))+b;},easeOut:function(t,b,c,d){return(t==d)?b+c:c*(-Math.pow(2,-10*t/d)+1)+b;},easeInOut:function(t,b,c,d){if(t==0)return b;if(t==d)return b+c;if((t/=d/2)<1)return c/2*Math.pow(2,10*(t-1))+b;return c/2*(-Math.pow(2,-10*--t)+2)+b;},CLASS_NAME:"OpenLayers.Easing.Expo"};OpenLayers.Easing.Quad={easeIn:function(t,b,c,d){return c*(t/=d)*t+b;},easeOut:function(t,b,c,d){return-c*(t/=d)*(t-2)+b;},easeInOut:function(t,b,c,d){if((t/=d/2)<1)return c/2*t*t+b;return-c/2*((--t)*(t-2)-1)+b;},CLASS_NAME:"OpenLayers.Easing.Quad"};OpenLayers.String={startsWith:function(str,sub){return(str.indexOf(sub)==0);},contains:function(str,sub){return(str.indexOf(sub)!=-1);},trim:function(str){return str.replace(/^\s\s*/,'').replace(/\s\s*$/,'');},camelize:function(str){var oStringList=str.split('-');var camelizedString=oStringList[0];for(var i=1,len=oStringList.length;i0){fig=parseFloat(num.toPrecision(sig));} return fig;},format:function(num,dec,tsep,dsep){dec=(typeof dec!="undefined")?dec:0;tsep=(typeof tsep!="undefined")?tsep:OpenLayers.Number.thousandsSeparator;dsep=(typeof dsep!="undefined")?dsep:OpenLayers.Number.decimalSeparator;if(dec!=null){num=parseFloat(num.toFixed(dec));} var parts=num.toString().split(".");if(parts.length==1&&dec==null){dec=0;} var integer=parts[0];if(tsep){var thousands=/(-?[0-9]+)([0-9]{3})/;while(thousands.test(integer)){integer=integer.replace(thousands,"$1"+tsep+"$2");}} var str;if(dec==0){str=integer;}else{var rem=parts.length>1?parts[1]:"0";if(dec!=null){rem=rem+new Array(dec-rem.length+1).join("0");} str=integer+dsep+rem;} return str;}};OpenLayers.Function={bind:function(func,object){var args=Array.prototype.slice.apply(arguments,[2]);return function(){var newArgs=args.concat(Array.prototype.slice.apply(arguments,[0]));return func.apply(object,newArgs);};},bindAsEventListener:function(func,object){return function(event){return func.call(object,event||window.event);};},False:function(){return false;},True:function(){return true;},Void:function(){}};OpenLayers.Array={filter:function(array,callback,caller){var selected=[];if(Array.prototype.filter){selected=array.filter(callback,caller);}else{var len=array.length;if(typeof callback!="function"){throw new TypeError();} for(var i=0;ithis.right)){this.right=bounds.right;} if((this.top==null)||(bounds.top>this.top)){this.top=bounds.top;}}}},containsLonLat:function(ll,options){if(typeof options==="boolean"){options={inclusive:options};} options=options||{};var contains=this.contains(ll.lon,ll.lat,options.inclusive),worldBounds=options.worldBounds;if(worldBounds&&!contains){var worldWidth=worldBounds.getWidth();var worldCenterX=(worldBounds.left+worldBounds.right)/2;var worldsAway=Math.round((ll.lon-worldCenterX)/worldWidth);contains=this.containsLonLat({lon:ll.lon-worldsAway*worldWidth,lat:ll.lat},{inclusive:options.inclusive});} return contains;},containsPixel:function(px,inclusive){return this.contains(px.x,px.y,inclusive);},contains:function(x,y,inclusive){if(inclusive==null){inclusive=true;} if(x==null||y==null){return false;} x=OpenLayers.Util.toFloat(x);y=OpenLayers.Util.toFloat(y);var contains=false;if(inclusive){contains=((x>=this.left)&&(x<=this.right)&&(y>=this.bottom)&&(y<=this.top));}else{contains=((x>this.left)&&(xthis.bottom)&&(y=self.bottom)&&(bounds.bottom<=self.top))||((self.bottom>=bounds.bottom)&&(self.bottom<=bounds.top)));var inTop=(((bounds.top>=self.bottom)&&(bounds.top<=self.top))||((self.top>bounds.bottom)&&(self.top=self.left)&&(bounds.left<=self.right))||((self.left>=bounds.left)&&(self.left<=bounds.right)));var inRight=(((bounds.right>=self.left)&&(bounds.right<=self.right))||((self.right>=bounds.left)&&(self.right<=bounds.right)));intersects=((inBottom||inTop)&&(inLeft||inRight));} if(options.worldBounds&&!intersects){var world=options.worldBounds;var width=world.getWidth();var selfCrosses=!world.containsBounds(self);var boundsCrosses=!world.containsBounds(bounds);if(selfCrosses&&!boundsCrosses){bounds=bounds.add(-width,0);intersects=self.intersectsBounds(bounds,{inclusive:options.inclusive});}else if(boundsCrosses&&!selfCrosses){self=self.add(-width,0);intersects=bounds.intersectsBounds(self,{inclusive:options.inclusive});}} return intersects;},containsBounds:function(bounds,partial,inclusive){if(partial==null){partial=false;} if(inclusive==null){inclusive=true;} var bottomLeft=this.contains(bounds.left,bounds.bottom,inclusive);var bottomRight=this.contains(bounds.right,bounds.bottom,inclusive);var topLeft=this.contains(bounds.left,bounds.top,inclusive);var topRight=this.contains(bounds.right,bounds.top,inclusive);return(partial)?(bottomLeft||bottomRight||topLeft||topRight):(bottomLeft&&bottomRight&&topLeft&&topRight);},determineQuadrant:function(lonlat){var quadrant="";var center=this.getCenterLonLat();quadrant+=(lonlat.lat=maxExtent.right&&newBounds.right>maxExtent.right){newBounds=newBounds.add(-width,0);} var newLeft=newBounds.left+leftTolerance;if(newLeftmaxExtent.left&&newBounds.right-rightTolerance>maxExtent.right){newBounds=newBounds.add(-width,0);}} return newBounds;},CLASS_NAME:"OpenLayers.Bounds"});OpenLayers.Bounds.fromString=function(str,reverseAxisOrder){var bounds=str.split(",");return OpenLayers.Bounds.fromArray(bounds,reverseAxisOrder);};OpenLayers.Bounds.fromArray=function(bbox,reverseAxisOrder){return reverseAxisOrder===true?new OpenLayers.Bounds(bbox[1],bbox[0],bbox[3],bbox[2]):new OpenLayers.Bounds(bbox[0],bbox[1],bbox[2],bbox[3]);};OpenLayers.Bounds.fromSize=function(size){return new OpenLayers.Bounds(0,size.h,size.w,0);};OpenLayers.Bounds.oppositeQuadrant=function(quadrant){var opp="";opp+=(quadrant.charAt(0)=='t')?'b':'t';opp+=(quadrant.charAt(1)=='l')?'r':'l';return opp;};OpenLayers.Element={visible:function(element){return OpenLayers.Util.getElement(element).style.display!='none';},toggle:function(){for(var i=0,len=arguments.length;imaxExtent.right){newLonLat.lon-=maxExtent.getWidth();}} return newLonLat;},CLASS_NAME:"OpenLayers.LonLat"});OpenLayers.LonLat.fromString=function(str){var pair=str.split(",");return new OpenLayers.LonLat(pair[0],pair[1]);};OpenLayers.LonLat.fromArray=function(arr){var gotArr=OpenLayers.Util.isArray(arr),lon=gotArr&&arr[0],lat=gotArr&&arr[1];return new OpenLayers.LonLat(lon,lat);};OpenLayers.Pixel=OpenLayers.Class({x:0.0,y:0.0,initialize:function(x,y){this.x=parseFloat(x);this.y=parseFloat(y);},toString:function(){return("x="+this.x+",y="+this.y);},clone:function(){return new OpenLayers.Pixel(this.x,this.y);},equals:function(px){var equals=false;if(px!=null){equals=((this.x==px.x&&this.y==px.y)||(isNaN(this.x)&&isNaN(this.y)&&isNaN(px.x)&&isNaN(px.y)));} return equals;},distanceTo:function(px){return Math.sqrt(Math.pow(this.x-px.x,2)+ Math.pow(this.y-px.y,2));},add:function(x,y){if((x==null)||(y==null)){throw new TypeError('Pixel.add cannot receive null values');} return new OpenLayers.Pixel(this.x+x,this.y+y);},offset:function(px){var newPx=this.clone();if(px){newPx=this.add(px.x,px.y);} return newPx;},CLASS_NAME:"OpenLayers.Pixel"});OpenLayers.Size=OpenLayers.Class({w:0.0,h:0.0,initialize:function(w,h){this.w=parseFloat(w);this.h=parseFloat(h);},toString:function(){return("w="+this.w+",h="+this.h);},clone:function(){return new OpenLayers.Size(this.w,this.h);},equals:function(sz){var equals=false;if(sz!=null){equals=((this.w==sz.w&&this.h==sz.h)||(isNaN(this.w)&&isNaN(this.h)&&isNaN(sz.w)&&isNaN(sz.h)));} return equals;},CLASS_NAME:"OpenLayers.Size"});OpenLayers.Console={log:function(){},debug:function(){},info:function(){},warn:function(){},error:function(){},userError:function(error){alert(error);},assert:function(){},dir:function(){},dirxml:function(){},trace:function(){},group:function(){},groupEnd:function(){},time:function(){},timeEnd:function(){},profile:function(){},profileEnd:function(){},count:function(){},CLASS_NAME:"OpenLayers.Console"};(function(){var scripts=document.getElementsByTagName("script");for(var i=0,len=scripts.length;i=0;i--){if(array[i]==item){array.splice(i,1);}} return array;};OpenLayers.Util.indexOf=function(array,obj){if(typeof array.indexOf=="function"){return array.indexOf(obj);}else{for(var i=0,len=array.length;i=0.0&&parseFloat(opacity)<1.0){element.style.filter='alpha(opacity='+(opacity*100)+')';element.style.opacity=opacity;}else if(parseFloat(opacity)==1.0){element.style.filter='';element.style.opacity='';}};OpenLayers.Util.createDiv=function(id,px,sz,imgURL,position,border,overflow,opacity){var dom=document.createElement('div');if(imgURL){dom.style.backgroundImage='url('+imgURL+')';} if(!id){id=OpenLayers.Util.createUniqueID("OpenLayersDiv");} if(!position){position="absolute";} OpenLayers.Util.modifyDOMElement(dom,id,px,sz,position,border,overflow,opacity);return dom;};OpenLayers.Util.createImage=function(id,px,sz,imgURL,position,border,opacity,delayDisplay){var image=document.createElement("img");if(!id){id=OpenLayers.Util.createUniqueID("OpenLayersDiv");} if(!position){position="relative";} OpenLayers.Util.modifyDOMElement(image,id,px,sz,position,border,null,opacity);if(delayDisplay){image.style.display="none";function display(){image.style.display="";OpenLayers.Event.stopObservingElement(image);} OpenLayers.Event.observe(image,"load",display);OpenLayers.Event.observe(image,"error",display);} image.style.alt=id;image.galleryImg="no";if(imgURL){image.src=imgURL;} return image;};OpenLayers.IMAGE_RELOAD_ATTEMPTS=0;OpenLayers.Util.alphaHackNeeded=null;OpenLayers.Util.alphaHack=function(){if(OpenLayers.Util.alphaHackNeeded==null){var arVersion=navigator.appVersion.split("MSIE");var version=parseFloat(arVersion[1]);var filter=false;try{filter=!!(document.body.filters);}catch(e){} OpenLayers.Util.alphaHackNeeded=(filter&&(version>=5.5)&&(version<7));} return OpenLayers.Util.alphaHackNeeded;};OpenLayers.Util.modifyAlphaImageDiv=function(div,id,px,sz,imgURL,position,border,sizing,opacity){OpenLayers.Util.modifyDOMElement(div,id,px,sz,position,null,null,opacity);var img=div.childNodes[0];if(imgURL){img.src=imgURL;} OpenLayers.Util.modifyDOMElement(img,div.id+"_innerImage",null,sz,"relative",border);if(OpenLayers.Util.alphaHack()){if(div.style.display!="none"){div.style.display="inline-block";} if(sizing==null){sizing="scale";} div.style.filter="progid:DXImageTransform.Microsoft"+".AlphaImageLoader(src='"+img.src+"', "+"sizingMethod='"+sizing+"')";if(parseFloat(div.style.opacity)>=0.0&&parseFloat(div.style.opacity)<1.0){div.style.filter+=" alpha(opacity="+div.style.opacity*100+")";} img.style.filter="alpha(opacity=0)";}};OpenLayers.Util.createAlphaImageDiv=function(id,px,sz,imgURL,position,border,sizing,opacity,delayDisplay){var div=OpenLayers.Util.createDiv();var img=OpenLayers.Util.createImage(null,null,null,null,null,null,null,delayDisplay);img.className="olAlphaImg";div.appendChild(img);OpenLayers.Util.modifyAlphaImageDiv(div,id,px,sz,imgURL,position,border,sizing,opacity);return div;};OpenLayers.Util.upperCaseObject=function(object){var uObject={};for(var key in object){uObject[key.toUpperCase()]=object[key];} return uObject;};OpenLayers.Util.applyDefaults=function(to,from){to=to||{};var fromIsEvt=typeof window.Event=="function"&&from instanceof window.Event;for(var key in from){if(to[key]===undefined||(!fromIsEvt&&from.hasOwnProperty&&from.hasOwnProperty(key)&&!to.hasOwnProperty(key))){to[key]=from[key];}} if(!fromIsEvt&&from&&from.hasOwnProperty&&from.hasOwnProperty('toString')&&!to.hasOwnProperty('toString')){to.toString=from.toString;} return to;};OpenLayers.Util.getParameterString=function(params){var paramsArray=[];for(var key in params){var value=params[key];if((value!=null)&&(typeof value!='function')){var encodedValue;if(typeof value=='object'&&value.constructor==Array){var encodedItemArray=[];var item;for(var itemIndex=0,len=value.length;itemIndex1e-12&&--iterLimit>0){var sinLambda=Math.sin(lambda),cosLambda=Math.cos(lambda);var sinSigma=Math.sqrt((cosU2*sinLambda)*(cosU2*sinLambda)+ (cosU1*sinU2-sinU1*cosU2*cosLambda)*(cosU1*sinU2-sinU1*cosU2*cosLambda));if(sinSigma==0){return 0;} var cosSigma=sinU1*sinU2+cosU1*cosU2*cosLambda;var sigma=Math.atan2(sinSigma,cosSigma);var alpha=Math.asin(cosU1*cosU2*sinLambda/sinSigma);var cosSqAlpha=Math.cos(alpha)*Math.cos(alpha);var cos2SigmaM=cosSigma-2*sinU1*sinU2/cosSqAlpha;var C=f/16*cosSqAlpha*(4+f*(4-3*cosSqAlpha));lambdaP=lambda;lambda=L+(1-C)*f*Math.sin(alpha)*(sigma+C*sinSigma*(cos2SigmaM+C*cosSigma*(-1+2*cos2SigmaM*cos2SigmaM)));} if(iterLimit==0){return NaN;} var uSq=cosSqAlpha*(a*a-b*b)/(b*b);var A=1+uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)));var B=uSq/1024*(256+uSq*(-128+uSq*(74-47*uSq)));var deltaSigma=B*sinSigma*(cos2SigmaM+B/4*(cosSigma*(-1+2*cos2SigmaM*cos2SigmaM)- B/6*cos2SigmaM*(-3+4*sinSigma*sinSigma)*(-3+4*cos2SigmaM*cos2SigmaM)));var s=b*A*(sigma-deltaSigma);var d=s.toFixed(3)/1000;return d;};OpenLayers.Util.destinationVincenty=function(lonlat,brng,dist){var u=OpenLayers.Util;var ct=u.VincentyConstants;var a=ct.a,b=ct.b,f=ct.f;var lon1=lonlat.lon;var lat1=lonlat.lat;var s=dist;var alpha1=u.rad(brng);var sinAlpha1=Math.sin(alpha1);var cosAlpha1=Math.cos(alpha1);var tanU1=(1-f)*Math.tan(u.rad(lat1));var cosU1=1/Math.sqrt((1+tanU1*tanU1)),sinU1=tanU1*cosU1;var sigma1=Math.atan2(tanU1,cosAlpha1);var sinAlpha=cosU1*sinAlpha1;var cosSqAlpha=1-sinAlpha*sinAlpha;var uSq=cosSqAlpha*(a*a-b*b)/(b*b);var A=1+uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)));var B=uSq/1024*(256+uSq*(-128+uSq*(74-47*uSq)));var sigma=s/(b*A),sigmaP=2*Math.PI;while(Math.abs(sigma-sigmaP)>1e-12){var cos2SigmaM=Math.cos(2*sigma1+sigma);var sinSigma=Math.sin(sigma);var cosSigma=Math.cos(sigma);var deltaSigma=B*sinSigma*(cos2SigmaM+B/4*(cosSigma*(-1+2*cos2SigmaM*cos2SigmaM)- B/6*cos2SigmaM*(-3+4*sinSigma*sinSigma)*(-3+4*cos2SigmaM*cos2SigmaM)));sigmaP=sigma;sigma=s/(b*A)+deltaSigma;} var tmp=sinU1*sinSigma-cosU1*cosSigma*cosAlpha1;var lat2=Math.atan2(sinU1*cosSigma+cosU1*sinSigma*cosAlpha1,(1-f)*Math.sqrt(sinAlpha*sinAlpha+tmp*tmp));var lambda=Math.atan2(sinSigma*sinAlpha1,cosU1*cosSigma-sinU1*sinSigma*cosAlpha1);var C=f/16*cosSqAlpha*(4+f*(4-3*cosSqAlpha));var L=lambda-(1-C)*f*sinAlpha*(sigma+C*sinSigma*(cos2SigmaM+C*cosSigma*(-1+2*cos2SigmaM*cos2SigmaM)));var revAz=Math.atan2(sinAlpha,-tmp);return new OpenLayers.LonLat(lon1+u.deg(L),u.deg(lat2));};OpenLayers.Util.getParameters=function(url){url=(url===null||url===undefined)?window.location.href:url;var paramsString="";if(OpenLayers.String.contains(url,'?')){var start=url.indexOf('?')+1;var end=OpenLayers.String.contains(url,"#")?url.indexOf('#'):url.length;paramsString=url.substring(start,end);} var parameters={};var pairs=paramsString.split(/[&;]/);for(var i=0,len=pairs.length;i1.0)?(1.0/scale):scale;return normScale;};OpenLayers.Util.getResolutionFromScale=function(scale,units){var resolution;if(scale){if(units==null){units="degrees";} var normScale=OpenLayers.Util.normalizeScale(scale);resolution=1/(normScale*OpenLayers.INCHES_PER_UNIT[units]*OpenLayers.DOTS_PER_INCH);} return resolution;};OpenLayers.Util.getScaleFromResolution=function(resolution,units){if(units==null){units="degrees";} var scale=resolution*OpenLayers.INCHES_PER_UNIT[units]*OpenLayers.DOTS_PER_INCH;return scale;};OpenLayers.Util.pagePosition=function(forElement){var pos=[0,0];var viewportElement=OpenLayers.Util.getViewportElement();if(!forElement||forElement==window||forElement==viewportElement){return pos;} var BUGGY_GECKO_BOX_OBJECT=OpenLayers.IS_GECKO&&document.getBoxObjectFor&&OpenLayers.Element.getStyle(forElement,'position')=='absolute'&&(forElement.style.top==''||forElement.style.left=='');var parent=null;var box;if(forElement.getBoundingClientRect){box=forElement.getBoundingClientRect();var scrollTop=viewportElement.scrollTop;var scrollLeft=viewportElement.scrollLeft;pos[0]=box.left+scrollLeft;pos[1]=box.top+scrollTop;}else if(document.getBoxObjectFor&&!BUGGY_GECKO_BOX_OBJECT){box=document.getBoxObjectFor(forElement);var vpBox=document.getBoxObjectFor(viewportElement);pos[0]=box.screenX-vpBox.screenX;pos[1]=box.screenY-vpBox.screenY;}else{pos[0]=forElement.offsetLeft;pos[1]=forElement.offsetTop;parent=forElement.offsetParent;if(parent!=forElement){while(parent){pos[0]+=parent.offsetLeft;pos[1]+=parent.offsetTop;parent=parent.offsetParent;}} var browser=OpenLayers.BROWSER_NAME;if(browser=="opera"||(browser=="safari"&&OpenLayers.Element.getStyle(forElement,'position')=='absolute')){pos[1]-=document.body.offsetTop;} parent=forElement.offsetParent;while(parent&&parent!=document.body){pos[0]-=parent.scrollLeft;if(browser!="opera"||parent.tagName!='TR'){pos[1]-=parent.scrollTop;} parent=parent.offsetParent;}} return pos;};OpenLayers.Util.getViewportElement=function(){var viewportElement=arguments.callee.viewportElement;if(viewportElement==undefined){viewportElement=(OpenLayers.BROWSER_NAME=="msie"&&document.compatMode!='CSS1Compat')?document.body:document.documentElement;arguments.callee.viewportElement=viewportElement;} return viewportElement;};OpenLayers.Util.isEquivalentUrl=function(url1,url2,options){options=options||{};OpenLayers.Util.applyDefaults(options,{ignoreCase:true,ignorePort80:true,ignoreHash:true});var urlObj1=OpenLayers.Util.createUrlObject(url1,options);var urlObj2=OpenLayers.Util.createUrlObject(url2,options);for(var key in urlObj1){if(key!=="args"){if(urlObj1[key]!=urlObj2[key]){return false;}}} for(var key in urlObj1.args){if(urlObj1.args[key]!=urlObj2.args[key]){return false;} delete urlObj2.args[key];} for(var key in urlObj2.args){return false;} return true;};OpenLayers.Util.createUrlObject=function(url,options){options=options||{};if(!(/^\w+:\/\//).test(url)){var loc=window.location;var port=loc.port?":"+loc.port:"";var fullUrl=loc.protocol+"//"+loc.host.split(":").shift()+port;if(url.indexOf("/")===0){url=fullUrl+url;}else{var parts=loc.pathname.split("/");parts.pop();url=fullUrl+parts.join("/")+"/"+url;}} if(options.ignoreCase){url=url.toLowerCase();} var a=document.createElement('a');a.href=url;var urlObject={};urlObject.host=a.host.split(":").shift();urlObject.protocol=a.protocol;if(options.ignorePort80){urlObject.port=(a.port=="80"||a.port=="0")?"":a.port;}else{urlObject.port=(a.port==""||a.port=="0")?"80":a.port;} urlObject.hash=(options.ignoreHash||a.hash==="#")?"":a.hash;var queryString=a.search;if(!queryString){var qMark=url.indexOf("?");queryString=(qMark!=-1)?url.substr(qMark):"";} urlObject.args=OpenLayers.Util.getParameters(queryString);urlObject.pathname=(a.pathname.charAt(0)=="/")?a.pathname:"/"+a.pathname;return urlObject;};OpenLayers.Util.removeTail=function(url){var head=null;var qMark=url.indexOf("?");var hashMark=url.indexOf("#");if(qMark==-1){head=(hashMark!=-1)?url.substr(0,hashMark):url;}else{head=(hashMark!=-1)?url.substr(0,Math.min(qMark,hashMark)):url.substr(0,qMark);} return head;};OpenLayers.IS_GECKO=(function(){var ua=navigator.userAgent.toLowerCase();return ua.indexOf("webkit")==-1&&ua.indexOf("gecko")!=-1;})();OpenLayers.CANVAS_SUPPORTED=(function(){var elem=document.createElement('canvas');return!!(elem.getContext&&elem.getContext('2d'));})();OpenLayers.BROWSER_NAME=(function(){var name="";var ua=navigator.userAgent.toLowerCase();if(ua.indexOf("opera")!=-1){name="opera";}else if(ua.indexOf("msie")!=-1){name="msie";}else if(ua.indexOf("safari")!=-1){name="safari";}else if(ua.indexOf("mozilla")!=-1){if(ua.indexOf("firefox")!=-1){name="firefox";}else{name="mozilla";}} return name;})();OpenLayers.Util.getBrowserName=function(){return OpenLayers.BROWSER_NAME;};OpenLayers.Util.getRenderedDimensions=function(contentHTML,size,options){var w,h;var container=document.createElement("div");container.style.visibility="hidden";var containerElement=(options&&options.containerElement)?options.containerElement:document.body;var parentHasPositionAbsolute=false;var superContainer=null;var parent=containerElement;while(parent&&parent.tagName.toLowerCase()!="body"){var parentPosition=OpenLayers.Element.getStyle(parent,"position");if(parentPosition=="absolute"){parentHasPositionAbsolute=true;break;}else if(parentPosition&&parentPosition!="static"){break;} parent=parent.parentNode;} if(parentHasPositionAbsolute&&(containerElement.clientHeight===0||containerElement.clientWidth===0)){superContainer=document.createElement("div");superContainer.style.visibility="hidden";superContainer.style.position="absolute";superContainer.style.overflow="visible";superContainer.style.width=document.body.clientWidth+"px";superContainer.style.height=document.body.clientHeight+"px";superContainer.appendChild(container);} container.style.position="absolute";if(size){if(size.w){w=size.w;container.style.width=w+"px";}else if(size.h){h=size.h;container.style.height=h+"px";}} if(options&&options.displayClass){container.className=options.displayClass;} var content=document.createElement("div");content.innerHTML=contentHTML;content.style.overflow="visible";if(content.childNodes){for(var i=0,l=content.childNodes.length;i=60){coordinateseconds-=60;coordinateminutes+=1;if(coordinateminutes>=60){coordinateminutes-=60;coordinatedegrees+=1;}} if(coordinatedegrees<10){coordinatedegrees="0"+coordinatedegrees;} var str=coordinatedegrees+"\u00B0";if(dmsOption.indexOf('dm')>=0){if(coordinateminutes<10){coordinateminutes="0"+coordinateminutes;} str+=coordinateminutes+"'";if(dmsOption.indexOf('dms')>=0){if(coordinateseconds<10){coordinateseconds="0"+coordinateseconds;} str+=coordinateseconds+'"';}} if(axis=="lon"){str+=coordinate<0?OpenLayers.i18n("W"):OpenLayers.i18n("E");}else{str+=coordinate<0?OpenLayers.i18n("S"):OpenLayers.i18n("N");} return str;};OpenLayers.Event={observers:false,KEY_SPACE:32,KEY_BACKSPACE:8,KEY_TAB:9,KEY_RETURN:13,KEY_ESC:27,KEY_LEFT:37,KEY_UP:38,KEY_RIGHT:39,KEY_DOWN:40,KEY_DELETE:46,element:function(event){return event.target||event.srcElement;},isSingleTouch:function(event){return event.touches&&event.touches.length==1;},isMultiTouch:function(event){return event.touches&&event.touches.length>1;},isLeftClick:function(event){return(((event.which)&&(event.which==1))||((event.button)&&(event.button==1)));},isRightClick:function(event){return(((event.which)&&(event.which==3))||((event.button)&&(event.button==2)));},stop:function(event,allowDefault){if(!allowDefault){if(event.preventDefault){event.preventDefault();}else{event.returnValue=false;}} if(event.stopPropagation){event.stopPropagation();}else{event.cancelBubble=true;}},findElement:function(event,tagName){var element=OpenLayers.Event.element(event);while(element.parentNode&&(!element.tagName||(element.tagName.toUpperCase()!=tagName.toUpperCase()))){element=element.parentNode;} return element;},observe:function(elementParam,name,observer,useCapture){var element=OpenLayers.Util.getElement(elementParam);useCapture=useCapture||false;if(name=='keypress'&&(navigator.appVersion.match(/Konqueror|Safari|KHTML/)||element.attachEvent)){name='keydown';} if(!this.observers){this.observers={};} if(!element._eventCacheID){var idPrefix="eventCacheID_";if(element.id){idPrefix=element.id+"_"+idPrefix;} element._eventCacheID=OpenLayers.Util.createUniqueID(idPrefix);} var cacheID=element._eventCacheID;if(!this.observers[cacheID]){this.observers[cacheID]=[];} this.observers[cacheID].push({'element':element,'name':name,'observer':observer,'useCapture':useCapture});if(element.addEventListener){element.addEventListener(name,observer,useCapture);}else if(element.attachEvent){element.attachEvent('on'+name,observer);}},stopObservingElement:function(elementParam){var element=OpenLayers.Util.getElement(elementParam);var cacheID=element._eventCacheID;this._removeElementObservers(OpenLayers.Event.observers[cacheID]);},_removeElementObservers:function(elementObservers){if(elementObservers){for(var i=elementObservers.length-1;i>=0;i--){var entry=elementObservers[i];var args=new Array(entry.element,entry.name,entry.observer,entry.useCapture);var removed=OpenLayers.Event.stopObserving.apply(this,args);}}},stopObserving:function(elementParam,name,observer,useCapture){useCapture=useCapture||false;var element=OpenLayers.Util.getElement(elementParam);var cacheID=element._eventCacheID;if(name=='keypress'){if(navigator.appVersion.match(/Konqueror|Safari|KHTML/)||element.detachEvent){name='keydown';}} var foundEntry=false;var elementObservers=OpenLayers.Event.observers[cacheID];if(elementObservers){var i=0;while(!foundEntry&&i=0;--i){map(mercator[i],geographic);} for(i=geographic.length-1;i>=0;--i){map(geographic[i],mercator);}})();OpenLayers.Map=OpenLayers.Class({Z_INDEX_BASE:{BaseLayer:100,Overlay:325,Feature:725,Popup:750,Control:1000},id:null,fractionalZoom:false,events:null,allOverlays:false,div:null,dragging:false,size:null,viewPortDiv:null,layerContainerOrigin:null,layerContainerDiv:null,layers:null,controls:null,popups:null,baseLayer:null,center:null,resolution:null,zoom:0,panRatio:1.5,options:null,tileSize:null,projection:"EPSG:4326",units:null,resolutions:null,maxResolution:null,minResolution:null,maxScale:null,minScale:null,maxExtent:null,minExtent:null,restrictedExtent:null,numZoomLevels:16,theme:null,displayProjection:null,fallThrough:true,panTween:null,eventListeners:null,panMethod:OpenLayers.Easing.Expo.easeOut,panDuration:50,paddingForPopups:null,minPx:null,maxPx:null,initialize:function(div,options){if(arguments.length===1&&typeof div==="object"){options=div;div=options&&options.div;} this.tileSize=new OpenLayers.Size(OpenLayers.Map.TILE_WIDTH,OpenLayers.Map.TILE_HEIGHT);this.paddingForPopups=new OpenLayers.Bounds(15,15,15,15);this.theme=OpenLayers._getScriptLocation()+'theme/default/style.css';this.options=OpenLayers.Util.extend({},options);OpenLayers.Util.extend(this,options);var projCode=this.projection instanceof OpenLayers.Projection?this.projection.projCode:this.projection;OpenLayers.Util.applyDefaults(this,OpenLayers.Projection.defaults[projCode]);if(this.maxExtent&&!(this.maxExtent instanceof OpenLayers.Bounds)){this.maxExtent=new OpenLayers.Bounds(this.maxExtent);} if(this.minExtent&&!(this.minExtent instanceof OpenLayers.Bounds)){this.minExtent=new OpenLayers.Bounds(this.minExtent);} if(this.restrictedExtent&&!(this.restrictedExtent instanceof OpenLayers.Bounds)){this.restrictedExtent=new OpenLayers.Bounds(this.restrictedExtent);} if(this.center&&!(this.center instanceof OpenLayers.LonLat)){this.center=new OpenLayers.LonLat(this.center);} this.layers=[];this.id=OpenLayers.Util.createUniqueID("OpenLayers.Map_");this.div=OpenLayers.Util.getElement(div);if(!this.div){this.div=document.createElement("div");this.div.style.height="1px";this.div.style.width="1px";} OpenLayers.Element.addClass(this.div,'olMap');var id=this.id+"_OpenLayers_ViewPort";this.viewPortDiv=OpenLayers.Util.createDiv(id,null,null,null,"relative",null,"hidden");this.viewPortDiv.style.width="100%";this.viewPortDiv.style.height="100%";this.viewPortDiv.className="olMapViewport";this.div.appendChild(this.viewPortDiv);this.events=new OpenLayers.Events(this,this.viewPortDiv,null,this.fallThrough,{includeXY:true});id=this.id+"_OpenLayers_Container";this.layerContainerDiv=OpenLayers.Util.createDiv(id);this.layerContainerDiv.style.width='100px';this.layerContainerDiv.style.height='100px';this.layerContainerDiv.style.zIndex=this.Z_INDEX_BASE['Popup']-1;this.viewPortDiv.appendChild(this.layerContainerDiv);this.updateSize();if(this.eventListeners instanceof Object){this.events.on(this.eventListeners);} if(parseFloat(navigator.appVersion.split("MSIE")[1])<9){this.events.register("resize",this,this.updateSize);}else{this.updateSizeDestroy=OpenLayers.Function.bind(this.updateSize,this);OpenLayers.Event.observe(window,'resize',this.updateSizeDestroy);} if(this.theme){var addNode=true;var nodes=document.getElementsByTagName('link');for(var i=0,len=nodes.length;i=0;--i){this.controls[i].destroy();} this.controls=null;} if(this.layers!=null){for(var i=this.layers.length-1;i>=0;--i){this.layers[i].destroy(false);} this.layers=null;} if(this.viewPortDiv){this.div.removeChild(this.viewPortDiv);} this.viewPortDiv=null;if(this.eventListeners){this.events.un(this.eventListeners);this.eventListeners=null;} this.events.destroy();this.events=null;this.options=null;},setOptions:function(options){var updatePxExtent=this.minPx&&options.restrictedExtent!=this.restrictedExtent;OpenLayers.Util.extend(this,options);updatePxExtent&&this.moveTo(this.getCachedCenter(),this.zoom,{forceZoomChange:true});},getTileSize:function(){return this.tileSize;},getBy:function(array,property,match){var test=(typeof match.test=="function");var found=OpenLayers.Array.filter(this[array],function(item){return item[property]==match||(test&&match.test(item[property]));});return found;},getLayersBy:function(property,match){return this.getBy("layers",property,match);},getLayersByName:function(match){return this.getLayersBy("name",match);},getLayersByClass:function(match){return this.getLayersBy("CLASS_NAME",match);},getControlsBy:function(property,match){return this.getBy("controls",property,match);},getControlsByClass:function(match){return this.getControlsBy("CLASS_NAME",match);},getLayer:function(id){var foundLayer=null;for(var i=0,len=this.layers.length;ithis.layers.length){idx=this.layers.length;} if(base!=idx){this.layers.splice(base,1);this.layers.splice(idx,0,layer);for(var i=0,len=this.layers.length;i=0;--i){this.removePopup(this.popups[i]);}} popup.map=this;this.popups.push(popup);var popupDiv=popup.draw();if(popupDiv){popupDiv.style.zIndex=this.Z_INDEX_BASE['Popup']+ this.popups.length;this.layerContainerDiv.appendChild(popupDiv);}},removePopup:function(popup){OpenLayers.Util.removeItem(this.popups,popup);if(popup.div){try{this.layerContainerDiv.removeChild(popup.div);} catch(e){}} popup.map=null;},getSize:function(){var size=null;if(this.size!=null){size=this.size.clone();} return size;},updateSize:function(){var newSize=this.getCurrentSize();if(newSize&&!isNaN(newSize.h)&&!isNaN(newSize.w)){this.events.clearMouseCache();var oldSize=this.getSize();if(oldSize==null){this.size=oldSize=newSize;} if(!newSize.equals(oldSize)){this.size=newSize;for(var i=0,len=this.layers.length;i=this.minPx.x+xRestriction?Math.round(dx):0;dy=y<=this.maxPx.y-yRestriction&&y>=this.minPx.y+yRestriction?Math.round(dy):0;if(dx||dy){if(!this.dragging){this.dragging=true;this.events.triggerEvent("movestart");} this.center=null;if(dx){this.layerContainerDiv.style.left=parseInt(this.layerContainerDiv.style.left)-dx+"px";this.minPx.x-=dx;this.maxPx.x-=dx;} if(dy){this.layerContainerDiv.style.top=parseInt(this.layerContainerDiv.style.top)-dy+"px";this.minPx.y-=dy;this.maxPx.y-=dy;} var layer,i,len;for(i=0,len=this.layers.length;imaxResolution){for(var i=zoom|0,ii=resolutions.length;ithis.restrictedExtent.getWidth()){lonlat=new OpenLayers.LonLat(maxCenter.lon,lonlat.lat);}else if(extent.leftthis.restrictedExtent.right){lonlat=lonlat.add(this.restrictedExtent.right- extent.right,0);} if(extent.getHeight()>this.restrictedExtent.getHeight()){lonlat=new OpenLayers.LonLat(lonlat.lon,maxCenter.lat);}else if(extent.bottomthis.restrictedExtent.top){lonlat=lonlat.add(0,this.restrictedExtent.top- extent.top);}}} var zoomChanged=forceZoomChange||((this.isValidZoomLevel(zoom))&&(zoom!=this.getZoom()));var centerChanged=(this.isValidLonLat(lonlat))&&(!lonlat.equals(this.center));if(zoomChanged||centerChanged||dragging){dragging||this.events.triggerEvent("movestart");if(centerChanged){if(!zoomChanged&&this.center){this.centerLayerContainer(lonlat);} this.center=lonlat.clone();} var res=zoomChanged?this.getResolutionForZoom(zoom):this.getResolution();if(zoomChanged||this.layerContainerOrigin==null){this.layerContainerOrigin=this.getCachedCenter();this.layerContainerDiv.style.left="0px";this.layerContainerDiv.style.top="0px";var maxExtent=this.getMaxExtent({restricted:true});var maxExtentCenter=maxExtent.getCenterLonLat();var lonDelta=this.center.lon-maxExtentCenter.lon;var latDelta=maxExtentCenter.lat-this.center.lat;var extentWidth=Math.round(maxExtent.getWidth()/res);var extentHeight=Math.round(maxExtent.getHeight()/res);this.minPx={x:(this.size.w-extentWidth)/2-lonDelta/res,y:(this.size.h-extentHeight)/2-latDelta/res};this.maxPx={x:this.minPx.x+Math.round(maxExtent.getWidth()/res),y:this.minPx.y+Math.round(maxExtent.getHeight()/res)};} if(zoomChanged){this.zoom=zoom;this.resolution=res;} var bounds=this.getExtent();if(this.baseLayer.visibility){this.baseLayer.moveTo(bounds,zoomChanged,options.dragging);options.dragging||this.baseLayer.events.triggerEvent("moveend",{zoomChanged:zoomChanged});} bounds=this.baseLayer.getExtent();for(var i=this.layers.length-1;i>=0;--i){var layer=this.layers[i];if(layer!==this.baseLayer&&!layer.isBaseLayer){var inRange=layer.calculateInRange();if(layer.inRange!=inRange){layer.inRange=inRange;if(!inRange){layer.display(false);} this.events.triggerEvent("changelayer",{layer:layer,property:"visibility"});} if(inRange&&layer.visibility){layer.moveTo(bounds,zoomChanged,options.dragging);options.dragging||layer.events.triggerEvent("moveend",{zoomChanged:zoomChanged});}}} this.events.triggerEvent("move");dragging||this.events.triggerEvent("moveend");if(zoomChanged){for(var i=0,len=this.popups.length;i=0)&&(zoomLevel0){resolution=this.layers[0].getResolution();} return resolution;},getUnits:function(){var units=null;if(this.baseLayer!=null){units=this.baseLayer.units;} return units;},getScale:function(){var scale=null;if(this.baseLayer!=null){var res=this.getResolution();var units=this.baseLayer.units;scale=OpenLayers.Util.getScaleFromResolution(res,units);} return scale;},getZoomForExtent:function(bounds,closest){var zoom=null;if(this.baseLayer!=null){zoom=this.baseLayer.getZoomForExtent(bounds,closest);} return zoom;},getResolutionForZoom:function(zoom){var resolution=null;if(this.baseLayer){resolution=this.baseLayer.getResolutionForZoom(zoom);} return resolution;},getZoomForResolution:function(resolution,closest){var zoom=null;if(this.baseLayer!=null){zoom=this.baseLayer.getZoomForResolution(resolution,closest);} return zoom;},zoomTo:function(zoom){if(this.isValidZoomLevel(zoom)){this.setCenter(null,zoom);}},zoomIn:function(){this.zoomTo(this.getZoom()+1);},zoomOut:function(){this.zoomTo(this.getZoom()-1);},zoomToExtent:function(bounds,closest){if(!(bounds instanceof OpenLayers.Bounds)){bounds=new OpenLayers.Bounds(bounds);} var center=bounds.getCenterLonLat();if(this.baseLayer.wrapDateLine){var maxExtent=this.getMaxExtent();bounds=bounds.clone();while(bounds.right=0){this.initResolutions();if(reinitialize&&this.map.baseLayer===this){this.map.setCenter(this.map.getCenter(),this.map.getZoomForResolution(resolution),false,true);this.map.events.triggerEvent("changebaselayer",{layer:this});} break;}}}},onMapResize:function(){},redraw:function(){var redrawn=false;if(this.map){this.inRange=this.calculateInRange();var extent=this.getExtent();if(extent&&this.inRange&&this.visibility){var zoomChanged=true;this.moveTo(extent,zoomChanged,false);this.events.triggerEvent("moveend",{"zoomChanged":zoomChanged});redrawn=true;}} return redrawn;},moveTo:function(bounds,zoomChanged,dragging){var display=this.visibility;if(!this.isBaseLayer){display=display&&this.inRange;} this.display(display);},moveByPx:function(dx,dy){},setMap:function(map){if(this.map==null){this.map=map;this.maxExtent=this.maxExtent||this.map.maxExtent;this.minExtent=this.minExtent||this.map.minExtent;this.projection=this.projection||this.map.projection;if(typeof this.projection=="string"){this.projection=new OpenLayers.Projection(this.projection);} this.units=this.projection.getUnits()||this.units||this.map.units;this.initResolutions();if(!this.isBaseLayer){this.inRange=this.calculateInRange();var show=((this.visibility)&&(this.inRange));this.div.style.display=show?"":"none";} this.setTileSize();}},afterAdd:function(){},removeMap:function(map){},getImageSize:function(bounds){return(this.imageSize||this.tileSize);},setTileSize:function(size){var tileSize=(size)?size:((this.tileSize)?this.tileSize:this.map.getTileSize());this.tileSize=tileSize;if(this.gutter){this.imageSize=new OpenLayers.Size(tileSize.w+(2*this.gutter),tileSize.h+(2*this.gutter));}},getVisibility:function(){return this.visibility;},setVisibility:function(visibility){if(visibility!=this.visibility){this.visibility=visibility;this.display(visibility);this.redraw();if(this.map!=null){this.map.events.triggerEvent("changelayer",{layer:this,property:"visibility"});} this.events.triggerEvent("visibilitychanged");}},display:function(display){if(display!=(this.div.style.display!="none")){this.div.style.display=(display&&this.calculateInRange())?"block":"none";}},calculateInRange:function(){var inRange=false;if(this.alwaysInRange){inRange=true;}else{if(this.map){var resolution=this.map.getResolution();inRange=((resolution>=this.minResolution)&&(resolution<=this.maxResolution));}} return inRange;},setIsBaseLayer:function(isBaseLayer){if(isBaseLayer!=this.isBaseLayer){this.isBaseLayer=isBaseLayer;if(this.map!=null){this.map.events.triggerEvent("changebaselayer",{layer:this});}}},initResolutions:function(){var i,len,p;var props={},alwaysInRange=true;for(i=0,len=this.RESOLUTION_PROPERTIES.length;i=resolution){highRes=res;lowZoom=i;} if(res<=resolution){lowRes=res;highZoom=i;break;}} var dRes=highRes-lowRes;if(dRes>0){zoom=lowZoom+((highRes-resolution)/dRes);}else{zoom=lowZoom;}}else{var diff;var minDiff=Number.POSITIVE_INFINITY;for(i=0,len=this.resolutions.length;iminDiff){break;} minDiff=diff;}else{if(this.resolutions[i]=0&&row=0;i--){serverResolution=this.serverResolutions[i];if(serverResolution>resolution){resolution=serverResolution;break;}} if(i===-1){throw'no appropriate resolution in serverResolutions';}} return resolution;},getServerZoom:function(){var resolution=this.getServerResolution();return this.serverResolutions?OpenLayers.Util.indexOf(this.serverResolutions,resolution):this.map.getZoomForResolution(resolution)+(this.zoomOffset||0);},transformDiv:function(scale){this.div.style.width=100*scale+'%';this.div.style.height=100*scale+'%';var size=this.map.getSize();var lcX=parseInt(this.map.layerContainerDiv.style.left,10);var lcY=parseInt(this.map.layerContainerDiv.style.top,10);var x=(lcX-(size.w/2.0))*(scale-1);var y=(lcY-(size.h/2.0))*(scale-1);this.div.style.left=x+'%';this.div.style.top=y+'%';},getResolutionScale:function(){return parseInt(this.div.style.width,10)/100;},applyBackBuffer:function(resolution){if(this.backBufferTimerId!==null){this.removeBackBuffer();} var backBuffer=this.backBuffer;if(!backBuffer){backBuffer=this.createBackBuffer();if(!backBuffer){return;} this.div.insertBefore(backBuffer,this.div.firstChild);this.backBuffer=backBuffer;var topLeftTileBounds=this.grid[0][0].bounds;this.backBufferLonLat={lon:topLeftTileBounds.left,lat:topLeftTileBounds.top};this.backBufferResolution=this.gridResolution;} var style=backBuffer.style;var ratio=this.backBufferResolution/resolution;style.width=100*ratio+'%';style.height=100*ratio+'%';var position=this.getViewPortPxFromLonLat(this.backBufferLonLat,resolution);var leftOffset=parseInt(this.map.layerContainerDiv.style.left,10);var topOffset=parseInt(this.map.layerContainerDiv.style.top,10);backBuffer.style.left=Math.round(position.x-leftOffset)+'%';backBuffer.style.top=Math.round(position.y-topOffset)+'%';},createBackBuffer:function(){var backBuffer;if(this.grid.length>0){backBuffer=document.createElement('div');backBuffer.id=this.div.id+'_bb';backBuffer.className='olBackBuffer';backBuffer.style.position='absolute';backBuffer.style.width='100%';backBuffer.style.height='100%';for(var i=0,lenI=this.grid.length;i=bounds.bottom-tilelat*this.buffer)||rowidx-tileSize.w*(buffer-1)){this.shiftColumn(true);}else if(tlViewPort.x<-tileSize.w*buffer){this.shiftColumn(false);}else if(tlViewPort.y>-tileSize.h*(buffer-1)){this.shiftRow(true);}else if(tlViewPort.y<-tileSize.h*buffer){this.shiftRow(false);}else{break;}}},shiftRow:function(prepend){var modelRowIndex=(prepend)?0:(this.grid.length-1);var grid=this.grid;var modelRow=grid[modelRowIndex];var resolution=this.getServerResolution();var deltaY=(prepend)?-this.tileSize.h:this.tileSize.h;var deltaLat=resolution*-deltaY;var row=(prepend)?grid.pop():grid.shift();for(var i=0,len=modelRow.length;irows){var row=this.grid.pop();for(i=0,l=row.length;icolumns){var row=this.grid[i];var tile=row.pop();this.destroyTile(tile);}}},onMapResize:function(){if(this.singleTile){this.clearGrid();this.setTileSize();}},getTileBounds:function(viewPortPx){var maxExtent=this.maxExtent;var resolution=this.getResolution();var tileMapWidth=resolution*this.tileSize.w;var tileMapHeight=resolution*this.tileSize.h;var mapPoint=this.getLonLatFromViewPortPx(viewPortPx);var tileLeft=maxExtent.left+(tileMapWidth*Math.floor((mapPoint.lon- maxExtent.left)/tileMapWidth));var tileBottom=maxExtent.bottom+(tileMapHeight*Math.floor((mapPoint.lat- maxExtent.bottom)/tileMapHeight));return new OpenLayers.Bounds(tileLeft,tileBottom,tileLeft+tileMapWidth,tileBottom+tileMapHeight);},CLASS_NAME:"OpenLayers.Layer.Grid"});OpenLayers.Control=OpenLayers.Class({id:null,map:null,div:null,type:null,allowSelection:false,displayClass:"",title:"",autoActivate:false,active:null,handler:null,eventListeners:null,events:null,initialize:function(options){this.displayClass=this.CLASS_NAME.replace("OpenLayers.","ol").replace(/\./g,"");OpenLayers.Util.extend(this,options);this.events=new OpenLayers.Events(this);if(this.eventListeners instanceof Object){this.events.on(this.eventListeners);} if(this.id==null){this.id=OpenLayers.Util.createUniqueID(this.CLASS_NAME+"_");}},destroy:function(){if(this.events){if(this.eventListeners){this.events.un(this.eventListeners);} this.events.destroy();this.events=null;} this.eventListeners=null;if(this.handler){this.handler.destroy();this.handler=null;} if(this.handlers){for(var key in this.handlers){if(this.handlers.hasOwnProperty(key)&&typeof this.handlers[key].destroy=="function"){this.handlers[key].destroy();}} this.handlers=null;} if(this.map){this.map.removeControl(this);this.map=null;} this.div=null;},setMap:function(map){this.map=map;if(this.handler){this.handler.setMap(map);}},draw:function(px){if(this.div==null){this.div=OpenLayers.Util.createDiv(this.id);this.div.className=this.displayClass;if(!this.allowSelection){this.div.className+=" olControlNoSelect";this.div.setAttribute("unselectable","on",0);this.div.onselectstart=OpenLayers.Function.False;} if(this.title!=""){this.div.title=this.title;}} if(px!=null){this.position=px.clone();} this.moveTo(this.position);return this.div;},moveTo:function(px){if((px!=null)&&(this.div!=null)){this.div.style.left=px.x+"px";this.div.style.top=px.y+"px";}},activate:function(){if(this.active){return false;} if(this.handler){this.handler.activate();} this.active=true;if(this.map){OpenLayers.Element.addClass(this.map.viewPortDiv,this.displayClass.replace(/ /g,"")+"Active");} this.events.triggerEvent("activate");return true;},deactivate:function(){if(this.active){if(this.handler){this.handler.deactivate();} this.active=false;if(this.map){OpenLayers.Element.removeClass(this.map.viewPortDiv,this.displayClass.replace(/ /g,"")+"Active");} this.events.triggerEvent("deactivate");return true;} return false;},CLASS_NAME:"OpenLayers.Control"});OpenLayers.Control.TYPE_BUTTON=1;OpenLayers.Control.TYPE_TOGGLE=2;OpenLayers.Control.TYPE_TOOL=3;OpenLayers.Control.Attribution=OpenLayers.Class(OpenLayers.Control,{separator:", ",template:"${layers}",destroy:function(){this.map.events.un({"removelayer":this.updateAttribution,"addlayer":this.updateAttribution,"changelayer":this.updateAttribution,"changebaselayer":this.updateAttribution,scope:this});OpenLayers.Control.prototype.destroy.apply(this,arguments);},draw:function(){OpenLayers.Control.prototype.draw.apply(this,arguments);this.map.events.on({'changebaselayer':this.updateAttribution,'changelayer':this.updateAttribution,'addlayer':this.updateAttribution,'removelayer':this.updateAttribution,scope:this});this.updateAttribution();return this.div;},updateAttribution:function(){var attributions=[];if(this.map&&this.map.layers){for(var i=0,len=this.map.layers.length;i=this.down.xy.distanceTo(evt.xy);if(passes&&this.touch&&this.down.touches.length===this.last.touches.length){for(var i=0,ii=this.down.touches.length;ithis.pixelTolerance){passes=false;break;}}}} return passes;},getTouchDistance:function(from,to){return Math.sqrt(Math.pow(from.clientX-to.clientX,2)+ Math.pow(from.clientY-to.clientY,2));},passesDblclickTolerance:function(evt){var passes=true;if(this.down&&this.first){passes=this.down.xy.distanceTo(this.first.xy)<=this.dblclickTolerance;} return passes;},clearTimer:function(){if(this.timerId!=null){window.clearTimeout(this.timerId);this.timerId=null;} if(this.rightclickTimerId!=null){window.clearTimeout(this.rightclickTimerId);this.rightclickTimerId=null;}},delayedCall:function(evt){this.timerId=null;if(evt){this.callback("click",[evt]);}},getEventInfo:function(evt){var touches;if(evt.touches){var len=evt.touches.length;touches=new Array(len);var touch;for(var i=0;i=0;--i){this.target.register(this.events[i],this,this.buttonClick,{extension:true});}},destroy:function(){for(var i=this.events.length-1;i>=0;--i){this.target.unregister(this.events[i],this,this.buttonClick);} delete this.target;},getPressedButton:function(element){var depth=3,button;do{if(OpenLayers.Element.hasClass(element,"olButton")){button=element;break;} element=element.parentNode;}while(--depth>0&&element);return button;},buttonClick:function(evt){var propagate=true,element=OpenLayers.Event.element(evt);if(element&&(OpenLayers.Event.isLeftClick(evt)||!~evt.type.indexOf("mouse"))){var button=this.getPressedButton(element);if(button){if(evt.type==="keydown"){switch(evt.keyCode){case OpenLayers.Event.KEY_RETURN:case OpenLayers.Event.KEY_SPACE:this.target.triggerEvent("buttonclick",{buttonElement:button});OpenLayers.Event.stop(evt);propagate=false;break;}}else if(this.startEvt){if(this.completeRegEx.test(evt.type)){var pos=OpenLayers.Util.pagePosition(button);this.target.triggerEvent("buttonclick",{buttonElement:button,buttonXY:{x:this.startEvt.clientX-pos[0],y:this.startEvt.clientY-pos[1]}});} if(this.cancelRegEx.test(evt.type)){delete this.startEvt;} OpenLayers.Event.stop(evt);propagate=false;} if(this.startRegEx.test(evt.type)){this.startEvt=evt;OpenLayers.Event.stop(evt);propagate=false;}}else{delete this.startEvt;}} return propagate;}});OpenLayers.Handler.Drag=OpenLayers.Class(OpenLayers.Handler,{started:false,stopDown:true,dragging:false,touch:false,last:null,start:null,lastMoveEvt:null,oldOnselectstart:null,interval:0,timeoutId:null,documentDrag:false,documentEvents:null,initialize:function(control,callbacks,options){OpenLayers.Handler.prototype.initialize.apply(this,arguments);if(this.documentDrag===true){var me=this;this._docMove=function(evt){me.mousemove({xy:{x:evt.clientX,y:evt.clientY},element:document});};this._docUp=function(evt){me.mouseup({xy:{x:evt.clientX,y:evt.clientY}});};}},dragstart:function(evt){var propagate=true;this.dragging=false;if(this.checkModifiers(evt)&&(OpenLayers.Event.isLeftClick(evt)||OpenLayers.Event.isSingleTouch(evt))){this.started=true;this.start=evt.xy;this.last=evt.xy;OpenLayers.Element.addClass(this.map.viewPortDiv,"olDragDown");this.down(evt);this.callback("down",[evt.xy]);OpenLayers.Event.stop(evt);if(!this.oldOnselectstart){this.oldOnselectstart=document.onselectstart?document.onselectstart:OpenLayers.Function.True;} document.onselectstart=OpenLayers.Function.False;propagate=!this.stopDown;}else{this.started=false;this.start=null;this.last=null;} return propagate;},dragmove:function(evt){this.lastMoveEvt=evt;if(this.started&&!this.timeoutId&&(evt.xy.x!=this.last.x||evt.xy.y!=this.last.y)){if(this.documentDrag===true&&this.documentEvents){if(evt.element===document){this.adjustXY(evt);this.setEvent(evt);}else{this.removeDocumentEvents();}} if(this.interval>0){this.timeoutId=setTimeout(OpenLayers.Function.bind(this.removeTimeout,this),this.interval);} this.dragging=true;this.move(evt);this.callback("move",[evt.xy]);if(!this.oldOnselectstart){this.oldOnselectstart=document.onselectstart;document.onselectstart=OpenLayers.Function.False;} this.last=evt.xy;} return true;},dragend:function(evt){if(this.started){if(this.documentDrag===true&&this.documentEvents){this.adjustXY(evt);this.removeDocumentEvents();} var dragged=(this.start!=this.last);this.started=false;this.dragging=false;OpenLayers.Element.removeClass(this.map.viewPortDiv,"olDragDown");this.up(evt);this.callback("up",[evt.xy]);if(dragged){this.callback("done",[evt.xy]);} document.onselectstart=this.oldOnselectstart;} return true;},down:function(evt){},move:function(evt){},up:function(evt){},out:function(evt){},mousedown:function(evt){return this.dragstart(evt);},touchstart:function(evt){if(!this.touch){this.touch=true;this.map.events.un({mousedown:this.mousedown,mouseup:this.mouseup,mousemove:this.mousemove,click:this.click,scope:this});} return this.dragstart(evt);},mousemove:function(evt){return this.dragmove(evt);},touchmove:function(evt){return this.dragmove(evt);},removeTimeout:function(){this.timeoutId=null;if(this.dragging){this.mousemove(this.lastMoveEvt);}},mouseup:function(evt){return this.dragend(evt);},touchend:function(evt){evt.xy=this.last;return this.dragend(evt);},mouseout:function(evt){if(this.started&&OpenLayers.Util.mouseLeft(evt,this.map.viewPortDiv)){if(this.documentDrag===true){this.addDocumentEvents();}else{var dragged=(this.start!=this.last);this.started=false;this.dragging=false;OpenLayers.Element.removeClass(this.map.viewPortDiv,"olDragDown");this.out(evt);this.callback("out",[]);if(dragged){this.callback("done",[evt.xy]);} if(document.onselectstart){document.onselectstart=this.oldOnselectstart;}}} return true;},click:function(evt){return(this.start==this.last);},activate:function(){var activated=false;if(OpenLayers.Handler.prototype.activate.apply(this,arguments)){this.dragging=false;activated=true;} return activated;},deactivate:function(){var deactivated=false;if(OpenLayers.Handler.prototype.deactivate.apply(this,arguments)){this.touch=false;this.started=false;this.dragging=false;this.start=null;this.last=null;deactivated=true;OpenLayers.Element.removeClass(this.map.viewPortDiv,"olDragDown");} return deactivated;},adjustXY:function(evt){var pos=OpenLayers.Util.pagePosition(this.map.viewPortDiv);evt.xy.x-=pos[0];evt.xy.y-=pos[1];},addDocumentEvents:function(){OpenLayers.Element.addClass(document.body,"olDragDown");this.documentEvents=true;OpenLayers.Event.observe(document,"mousemove",this._docMove);OpenLayers.Event.observe(document,"mouseup",this._docUp);},removeDocumentEvents:function(){OpenLayers.Element.removeClass(document.body,"olDragDown");this.documentEvents=false;OpenLayers.Event.stopObserving(document,"mousemove",this._docMove);OpenLayers.Event.stopObserving(document,"mouseup",this._docUp);},CLASS_NAME:"OpenLayers.Handler.Drag"});OpenLayers.Handler.Box=OpenLayers.Class(OpenLayers.Handler,{dragHandler:null,boxDivClassName:'olHandlerBoxZoomBox',boxOffsets:null,initialize:function(control,callbacks,options){OpenLayers.Handler.prototype.initialize.apply(this,arguments);this.dragHandler=new OpenLayers.Handler.Drag(this,{down:this.startBox,move:this.moveBox,out:this.removeBox,up:this.endBox},{keyMask:this.keyMask});},destroy:function(){OpenLayers.Handler.prototype.destroy.apply(this,arguments);if(this.dragHandler){this.dragHandler.destroy();this.dragHandler=null;}},setMap:function(map){OpenLayers.Handler.prototype.setMap.apply(this,arguments);if(this.dragHandler){this.dragHandler.setMap(map);}},startBox:function(xy){this.callback("start",[]);this.zoomBox=OpenLayers.Util.createDiv('zoomBox',{x:-9999,y:-9999});this.zoomBox.className=this.boxDivClassName;this.zoomBox.style.zIndex=this.map.Z_INDEX_BASE["Popup"]-1;this.map.viewPortDiv.appendChild(this.zoomBox);OpenLayers.Element.addClass(this.map.viewPortDiv,"olDrawBox");},moveBox:function(xy){var startX=this.dragHandler.start.x;var startY=this.dragHandler.start.y;var deltaX=Math.abs(startX-xy.x);var deltaY=Math.abs(startY-xy.y);var offset=this.getBoxOffsets();this.zoomBox.style.width=(deltaX+offset.width+1)+"px";this.zoomBox.style.height=(deltaY+offset.height+1)+"px";this.zoomBox.style.left=(xy.x5||Math.abs(this.dragHandler.start.y-end.y)>5){var start=this.dragHandler.start;var top=Math.min(start.y,end.y);var bottom=Math.max(start.y,end.y);var left=Math.min(start.x,end.x);var right=Math.max(start.x,end.x);result=new OpenLayers.Bounds(left,bottom,right,top);}else{result=this.dragHandler.start.clone();} this.removeBox();this.callback("done",[result]);},removeBox:function(){this.map.viewPortDiv.removeChild(this.zoomBox);this.zoomBox=null;this.boxOffsets=null;OpenLayers.Element.removeClass(this.map.viewPortDiv,"olDrawBox");},activate:function(){if(OpenLayers.Handler.prototype.activate.apply(this,arguments)){this.dragHandler.activate();return true;}else{return false;}},deactivate:function(){if(OpenLayers.Handler.prototype.deactivate.apply(this,arguments)){if(this.dragHandler.deactivate()){if(this.zoomBox){this.removeBox();}} return true;}else{return false;}},getBoxOffsets:function(){if(!this.boxOffsets){var testDiv=document.createElement("div");testDiv.style.position="absolute";testDiv.style.border="1px solid black";testDiv.style.width="3px";document.body.appendChild(testDiv);var w3cBoxModel=testDiv.clientWidth==3;document.body.removeChild(testDiv);var left=parseInt(OpenLayers.Element.getStyle(this.zoomBox,"border-left-width"));var right=parseInt(OpenLayers.Element.getStyle(this.zoomBox,"border-right-width"));var top=parseInt(OpenLayers.Element.getStyle(this.zoomBox,"border-top-width"));var bottom=parseInt(OpenLayers.Element.getStyle(this.zoomBox,"border-bottom-width"));this.boxOffsets={left:left,right:right,top:top,bottom:bottom,width:w3cBoxModel===false?left+right:0,height:w3cBoxModel===false?top+bottom:0};} return this.boxOffsets;},CLASS_NAME:"OpenLayers.Handler.Box"});OpenLayers.Control.ZoomBox=OpenLayers.Class(OpenLayers.Control,{type:OpenLayers.Control.TYPE_TOOL,out:false,keyMask:null,alwaysZoom:false,draw:function(){this.handler=new OpenLayers.Handler.Box(this,{done:this.zoomBox},{keyMask:this.keyMask});},zoomBox:function(position){if(position instanceof OpenLayers.Bounds){var bounds;if(!this.out){var minXY=this.map.getLonLatFromPixel({x:position.left,y:position.bottom});var maxXY=this.map.getLonLatFromPixel({x:position.right,y:position.top});bounds=new OpenLayers.Bounds(minXY.lon,minXY.lat,maxXY.lon,maxXY.lat);}else{var pixWidth=Math.abs(position.right-position.left);var pixHeight=Math.abs(position.top-position.bottom);var zoomFactor=Math.min((this.map.size.h/pixHeight),(this.map.size.w/pixWidth));var extent=this.map.getExtent();var center=this.map.getLonLatFromPixel(position.getCenterPixel());var xmin=center.lon-(extent.getWidth()/2)*zoomFactor;var xmax=center.lon+(extent.getWidth()/2)*zoomFactor;var ymin=center.lat-(extent.getHeight()/2)*zoomFactor;var ymax=center.lat+(extent.getHeight()/2)*zoomFactor;bounds=new OpenLayers.Bounds(xmin,ymin,xmax,ymax);} var lastZoom=this.map.getZoom();this.map.zoomToExtent(bounds);if(lastZoom==this.map.getZoom()&&this.alwaysZoom==true){this.map.zoomTo(lastZoom+(this.out?-1:1));}}else{if(!this.out){this.map.setCenter(this.map.getLonLatFromPixel(position),this.map.getZoom()+1);}else{this.map.setCenter(this.map.getLonLatFromPixel(position),this.map.getZoom()-1);}}},CLASS_NAME:"OpenLayers.Control.ZoomBox"});OpenLayers.Control.DragPan=OpenLayers.Class(OpenLayers.Control,{type:OpenLayers.Control.TYPE_TOOL,panned:false,interval:1,documentDrag:false,kinetic:null,enableKinetic:false,kineticInterval:10,draw:function(){if(this.enableKinetic){var config={interval:this.kineticInterval};if(typeof this.enableKinetic==="object"){config=OpenLayers.Util.extend(config,this.enableKinetic);} this.kinetic=new OpenLayers.Kinetic(config);} this.handler=new OpenLayers.Handler.Drag(this,{"move":this.panMap,"done":this.panMapDone,"down":this.panMapStart},{interval:this.interval,documentDrag:this.documentDrag});},panMapStart:function(){if(this.kinetic){this.kinetic.begin();}},panMap:function(xy){if(this.kinetic){this.kinetic.update(xy);} this.panned=true;this.map.pan(this.handler.last.x-xy.x,this.handler.last.y-xy.y,{dragging:true,animate:false});},panMapDone:function(xy){if(this.panned){var res=null;if(this.kinetic){res=this.kinetic.end(xy);} this.map.pan(this.handler.last.x-xy.x,this.handler.last.y-xy.y,{dragging:!!res,animate:false});if(res){var self=this;this.kinetic.move(res,function(x,y,end){self.map.pan(x,y,{dragging:!end,animate:false});});} this.panned=false;}},CLASS_NAME:"OpenLayers.Control.DragPan"});OpenLayers.Handler.MouseWheel=OpenLayers.Class(OpenLayers.Handler,{wheelListener:null,mousePosition:null,interval:0,delta:0,cumulative:true,initialize:function(control,callbacks,options){OpenLayers.Handler.prototype.initialize.apply(this,arguments);this.wheelListener=OpenLayers.Function.bindAsEventListener(this.onWheelEvent,this);},destroy:function(){OpenLayers.Handler.prototype.destroy.apply(this,arguments);this.wheelListener=null;},onWheelEvent:function(e){if(!this.map||!this.checkModifiers(e)){return;} var overScrollableDiv=false;var overLayerDiv=false;var overMapDiv=false;var elem=OpenLayers.Event.element(e);while((elem!=null)&&!overMapDiv&&!overScrollableDiv){if(!overScrollableDiv){try{if(elem.currentStyle){overflow=elem.currentStyle["overflow"];}else{var style=document.defaultView.getComputedStyle(elem,null);var overflow=style.getPropertyValue("overflow");} overScrollableDiv=(overflow&&(overflow=="auto")||(overflow=="scroll"));}catch(err){}} if(!overLayerDiv){for(var i=0,len=this.map.layers.length;i=1.3&&!params.EXCEPTIONS){params.EXCEPTIONS="INIMAGE";} newArguments.push(name,url,params,options);OpenLayers.Layer.Grid.prototype.initialize.apply(this,newArguments);OpenLayers.Util.applyDefaults(this.params,OpenLayers.Util.upperCaseObject(this.DEFAULT_PARAMS));if(!this.noMagic&&this.params.TRANSPARENT&&this.params.TRANSPARENT.toString().toLowerCase()=="true"){if((options==null)||(!options.isBaseLayer)){this.isBaseLayer=false;} if(this.params.FORMAT=="image/jpeg"){this.params.FORMAT=OpenLayers.Util.alphaHack()?"image/gif":"image/png";}}},clone:function(obj){if(obj==null){obj=new OpenLayers.Layer.WMS(this.name,this.url,this.params,this.getOptions());} obj=OpenLayers.Layer.Grid.prototype.clone.apply(this,[obj]);return obj;},reverseAxisOrder:function(){var projCode=this.projection.getCode();return parseFloat(this.params.VERSION)>=1.3&&!!(this.yx[projCode]||OpenLayers.Projection.defaults[projCode].yx);},getURL:function(bounds){bounds=this.adjustBounds(bounds);var imageSize=this.getImageSize();var newParams={};var reverseAxisOrder=this.reverseAxisOrder();newParams.BBOX=this.encodeBBOX?bounds.toBBOX(null,reverseAxisOrder):bounds.toArray(reverseAxisOrder);newParams.WIDTH=imageSize.w;newParams.HEIGHT=imageSize.h;var requestString=this.getFullRequestString(newParams);return requestString;},mergeNewParams:function(newParams){var upperParams=OpenLayers.Util.upperCaseObject(newParams);var newArguments=[upperParams];return OpenLayers.Layer.Grid.prototype.mergeNewParams.apply(this,newArguments);},getFullRequestString:function(newParams,altUrl){var mapProjection=this.map.getProjectionObject();var projectionCode=this.projection&&this.projection.equals(mapProjection)?this.projection.getCode():mapProjection.getCode();var value=(projectionCode=="none")?null:projectionCode;if(parseFloat(this.params.VERSION)>=1.3){this.params.CRS=value;}else{this.params.SRS=value;} if(typeof this.params.TRANSPARENT=="boolean"){newParams.TRANSPARENT=this.params.TRANSPARENT?"TRUE":"FALSE";} return OpenLayers.Layer.Grid.prototype.getFullRequestString.apply(this,arguments);},CLASS_NAME:"OpenLayers.Layer.WMS"});OpenLayers.Control.PanZoom=OpenLayers.Class(OpenLayers.Control,{slideFactor:50,slideRatio:null,buttons:null,position:null,initialize:function(options){this.position=new OpenLayers.Pixel(OpenLayers.Control.PanZoom.X,OpenLayers.Control.PanZoom.Y);OpenLayers.Control.prototype.initialize.apply(this,arguments);},destroy:function(){if(this.map){this.map.events.unregister("buttonclick",this,this.onButtonClick);} this.removeButtons();this.buttons=null;this.position=null;OpenLayers.Control.prototype.destroy.apply(this,arguments);},setMap:function(map){OpenLayers.Control.prototype.setMap.apply(this,arguments);this.map.events.register("buttonclick",this,this.onButtonClick);},draw:function(px){OpenLayers.Control.prototype.draw.apply(this,arguments);px=this.position;this.buttons=[];var sz={w:18,h:18};var centered=new OpenLayers.Pixel(px.x+sz.w/2,px.y);this._addButton("panup","north-mini.png",centered,sz);px.y=centered.y+sz.h;this._addButton("panleft","west-mini.png",px,sz);this._addButton("panright","east-mini.png",px.add(sz.w,0),sz);this._addButton("pandown","south-mini.png",centered.add(0,sz.h*2),sz);this._addButton("zoomin","zoom-plus-mini.png",centered.add(0,sz.h*3+5),sz);this._addButton("zoomworld","zoom-world-mini.png",centered.add(0,sz.h*4+5),sz);this._addButton("zoomout","zoom-minus-mini.png",centered.add(0,sz.h*5+5),sz);return this.div;},_addButton:function(id,img,xy,sz){var imgLocation=OpenLayers.Util.getImageLocation(img);var btn=OpenLayers.Util.createAlphaImageDiv(this.id+"_"+id,xy,sz,imgLocation,"absolute");btn.style.cursor="pointer";this.div.appendChild(btn);btn.action=id;btn.className="olButton";this.buttons.push(btn);return btn;},_removeButton:function(btn){this.div.removeChild(btn);OpenLayers.Util.removeItem(this.buttons,btn);},removeButtons:function(){for(var i=this.buttons.length-1;i>=0;--i){this._removeButton(this.buttons[i]);}},onButtonClick:function(evt){var btn=evt.buttonElement;switch(btn.action){case"panup":this.map.pan(0,-this.getSlideFactor("h"));break;case"pandown":this.map.pan(0,this.getSlideFactor("h"));break;case"panleft":this.map.pan(-this.getSlideFactor("w"),0);break;case"panright":this.map.pan(this.getSlideFactor("w"),0);break;case"zoomin":this.map.zoomIn();break;case"zoomout":this.map.zoomOut();break;case"zoomworld":this.map.zoomToMaxExtent();break;}},getSlideFactor:function(dim){return this.slideRatio?this.map.getSize()[dim]*this.slideRatio:this.slideFactor;},CLASS_NAME:"OpenLayers.Control.PanZoom"});OpenLayers.Control.PanZoom.X=4;OpenLayers.Control.PanZoom.Y=4;OpenLayers.Layer.TMS=OpenLayers.Class(OpenLayers.Layer.Grid,{serviceVersion:"1.0.0",layername:null,type:null,isBaseLayer:true,tileOrigin:null,serverResolutions:null,zoomOffset:0,initialize:function(name,url,options){var newArguments=[];newArguments.push(name,url,{},options);OpenLayers.Layer.Grid.prototype.initialize.apply(this,newArguments);},clone:function(obj){if(obj==null){obj=new OpenLayers.Layer.TMS(this.name,this.url,this.getOptions());} obj=OpenLayers.Layer.Grid.prototype.clone.apply(this,[obj]);return obj;},getURL:function(bounds){bounds=this.adjustBounds(bounds);var res=this.getServerResolution();var x=Math.round((bounds.left-this.tileOrigin.lon)/(res*this.tileSize.w));var y=Math.round((bounds.bottom-this.tileOrigin.lat)/(res*this.tileSize.h));var z=this.getServerZoom();var path=this.serviceVersion+"/"+this.layername+"/"+z+"/"+x+"/"+y+"."+this.type;var url=this.url;if(OpenLayers.Util.isArray(url)){url=this.selectUrl(path,url);} return url+path;},setMap:function(map){OpenLayers.Layer.Grid.prototype.setMap.apply(this,arguments);if(!this.tileOrigin){this.tileOrigin=new OpenLayers.LonLat(this.map.maxExtent.left,this.map.maxExtent.bottom);}},CLASS_NAME:"OpenLayers.Layer.TMS"});OpenLayers.Layer.WMTS=OpenLayers.Class(OpenLayers.Layer.Grid,{isBaseLayer:true,version:"1.0.0",requestEncoding:"KVP",url:null,layer:null,matrixSet:null,style:null,format:"image/jpeg",tileOrigin:null,tileFullExtent:null,formatSuffix:null,matrixIds:null,dimensions:null,params:null,zoomOffset:0,serverResolutions:null,formatSuffixMap:{"image/png":"png","image/png8":"png","image/png24":"png","image/png32":"png","png":"png","image/jpeg":"jpg","image/jpg":"jpg","jpeg":"jpg","jpg":"jpg"},matrix:null,initialize:function(config){var required={url:true,layer:true,style:true,matrixSet:true};for(var prop in required){if(!(prop in config)){throw new Error("Missing property '"+prop+"' in layer configuration.");}} config.params=OpenLayers.Util.upperCaseObject(config.params);var args=[config.name,config.url,config.params,config];OpenLayers.Layer.Grid.prototype.initialize.apply(this,args);if(!this.formatSuffix){this.formatSuffix=this.formatSuffixMap[this.format]||this.format.split("/").pop();} if(this.matrixIds){var len=this.matrixIds.length;if(len&&typeof this.matrixIds[0]==="string"){var ids=this.matrixIds;this.matrixIds=new Array(len);for(var i=0;i=0;--i){dimension=dimensions[i];context[dimension]=params[dimension.toUpperCase()];}} url=OpenLayers.String.format(template,context);}else{var path=this.version+"/"+this.layer+"/"+this.style+"/";if(dimensions){for(var i=0;iɛ<;L@ }k;/emIENDB`mapproxy-1.11.0/mapproxy/service/templates/demo/static/img/south-mini.png000066400000000000000000000007411320454472400265660ustar00rootroot00000000000000PNG  IHDRVΎWsBIT|dtEXtSoftwarewww.inkscape.org<sIDAT8͒KQ+AP4 9 4PA@CsA 9fKRQh _"!Cg;ys ΅m nNNq8:Wx5U Ba{;:<-#LE/XRt:։it(?Yt۠+8ׁan_P8fqq~\.q:4qλ ȍ |;ívcـ$|XtX`fڀ٤h;V C3G"OT ю%Xw-SR5 :J;̢t@DPz L6FåT` 6I͐?Aʏ+΀IENDB`mapproxy-1.11.0/mapproxy/service/templates/demo/static/img/west-mini.png000066400000000000000000000007051320454472400264060ustar00rootroot00000000000000PNG  IHDRVΎWsBIT|dtEXtSoftwarewww.inkscape.org<WIDAT8KBQ?! CH5)QPj%s\\(A RCsCQPPD6 I`,y|{ι+ !PKQ!`CE%dA]$s`sQ Eɱ>pO9.[$%@fOOD$6f4mOy7h֎9l8Ld\IENDB`mapproxy-1.11.0/mapproxy/service/templates/demo/static/img/zoom-plus-mini.png000066400000000000000000000007511320454472400273720ustar00rootroot00000000000000PNG  IHDRVΎWsBIT|dtEXtSoftwarewww.inkscape.org<{IDAT8;OAF dU,,D숍hLIcAgkZ+-4PcԊ4 fZ.,,l2ͽgwvF(_6% E8zEJ'9DJHp ~# /=%tDJ VT*NNP *X >@qRx$zb1a`Ya/,xhR9mi9ϻh*,:@41MM%h"fJ&st )E((Uȷ^89`k딋AX;D7n `yyyMS}ʃEdIENDB`mapproxy-1.11.0/mapproxy/service/templates/demo/static/img/zoom-world-mini.png000066400000000000000000000020601320454472400275310ustar00rootroot00000000000000PNG  IHDRVΎWsBIT|dtEXtSoftwarewww.inkscape.org<IDAT8m[L^JBKZ XBRJ]D31`LJ LHFd(e\ =|;G7[X :23Ch0e849{hrPxRQ5'31ӧxebbb>jر f۽5(ddT35{1Uz5Ip/UO 300˾}ܹF^8G62< 55Xr)eeVΞ"#TΝ3z<;R8~X,JK|y6ۯ\`40u"ΝETݵr)99Q^n%33턇ٟ I)(ciI`qq I||b1p^/^dllh,J'&@GDmD2owa68 zZ[G0HC EETT@\zro\H++iitvc0pZ2.EffVeܺ-vJOďɤz%8kT\.%??+W ĉ_046"DL?Z7Jz 66YGQW o<<45Aii76nn0t<ٌoHD>D6Bqq;NettH5 ܸ5~~^,- kge٬X9|8xx`ttLµkzPrrZ\~b S 66.^LB'1p|+cc}>uu;Dw '%% [X^(/ӧÜ9Ӳ7& MiFZVW]dgnz#@,3;Lsmm#_qpdrIENDB`mapproxy-1.11.0/mapproxy/service/templates/demo/static/logo.png000066400000000000000000000025301320454472400246540ustar00rootroot00000000000000PNG  IHDR47CiEiCCPICC ProfilexTkA6n"Zkx"IYhE6bk Ed3In6&*Ezd/JZE(ޫ(b-nL~7}ov r4 Ril|Bj A4%UN$As{z[V{wwҶ@G*q Y<ߡ)t9Nyx+=Y"|@5-MS%@H8qR>׋infObN~N>! ?F?aĆ=5`5_M'Tq. VJp8dasZHOLn}&wVQygE0  HPEaP@<14r?#{2u$jtbDA{6=Q<("qCA*Oy\V;噹sM^|vWGyz?W15s-_̗)UKuZ17ߟl;=..s7VgjHUO^gc)1&v!.K `m)m$``/]?[xF QT*d4o(/lșmSqens}nk~8X<R5 vz)Ӗ9R,bRPCRR%eKUbvؙn9BħJeRR~NցoEx pHYs  IDAThYN1mW?x^׿/pU>B' x5$/ %l&nu:od; DLW„BW4:Nw_dUKP޼ c{}4 0U $ٛ7a;RmPt)gBuΐQ_!Lf τBWbR(9$"uolI dd{.P gӟû/|@b<-ބv[>Vݽ7H\VeBR7N}6נ k[jcV(DUtX!=!6:Gޟ>('}ZV{&9qTfBi?bdpt]-4@Rci0r\:!V;pKogW:Nw}mkK9o㷙-J|"`Bm*Dg=p&n`_`mG5bt.&cB:4 $F+PnXN!`mE {H6j2]1213)';dYIENDB`mapproxy-1.11.0/mapproxy/service/templates/demo/static/site.css000066400000000000000000000037121320454472400246670ustar00rootroot00000000000000 body { background-color: #EEEEEC; font-family: Verdana, sans-serif; font-size: 12px; line-height: 1.3em; width: 70%; min-width: 700px; margin: 1em auto; color: #2e3436; } div#box { background-color: #fefefe; border: solid #2e3436 1px; } div#menu, div#content, div#footer { padding: 2em; } div#header { padding: 0.5em 2em; position: relative; background-color: #fefefe; } div#header a { text-decoration: none; color: inherit; } div#header img { padding-left: 0.7em; display: inline; } div#header h1 { font-size: 32px; position: absolute; display: inline; top: 5px; left: 2.8em; } div#content { margin-left: 10px; } div#content h1, div#content h2, div#content h3, div#content h4, div#content h5, div#content h6 { /* font-family: Georgia, 'Trebuchet MS', Verdana, sans-serif;*/ border-bottom: 0; margin: 1.2em 0em 0.5em -10px; padding: .3em 0 .1em 10px; } div#content h1, div#content h2, div#content h3 { border-bottom: 1px dashed #ccc; } p { margin-top: 0.5em; margin-right: 5%; } div#menu { background-color: #3A3740; padding: 0.6em 0 0.6em 2.6em; } #menu span { background-color: #3A3740; font-weight: bold; padding: 0.6em; } #menu span.current { background-color: #666; } #menu a { color: #fefefc; text-decoration: none; } div#footer { color: gray; text-align: center; font-size: small; } div#footer a { color: gray; text-decoration: none; } pre { border: dotted black 1px; background: #eeeeec; font-size: small; padding: 1em; margin-right: 5%; } div#map { border: 1px solid black; width: 95%; height: 600px; background-color: #EEEEEC; } .code { border: dotted black 1px; background: #eeeeec; font-size: small; padding: 1em; } th { padding-right: 1.5em; } td.value{ padding: 0 2em; } .capabilities { padding: 1em; } div.capabilities span{ padding-right:1.5em; } mapproxy-1.11.0/mapproxy/service/templates/demo/static/theme/000077500000000000000000000000001320454472400243105ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/service/templates/demo/static/theme/default/000077500000000000000000000000001320454472400257345ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/service/templates/demo/static/theme/default/framedCloud.css000066400000000000000000000000001320454472400306610ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/service/templates/demo/static/theme/default/google.css000066400000000000000000000004571320454472400277300ustar00rootroot00000000000000.olLayerGoogleCopyright { right: 3px; bottom: 2px; left: auto; } .olLayerGoogleV3.olLayerGoogleCopyright { bottom: 0px; right: 0px !important; } .olLayerGooglePoweredBy { left: 2px; bottom: 2px; } .olLayerGoogleV3.olLayerGooglePoweredBy { bottom: 0px !important; } mapproxy-1.11.0/mapproxy/service/templates/demo/static/theme/default/ie6-style.css000066400000000000000000000003241320454472400302660ustar00rootroot00000000000000.olControlZoomPanel div { background-image: url(img/zoom-panel-NOALPHA.png); } .olControlPanPanel div { background-image: url(img/pan-panel-NOALPHA.png); } .olControlEditingToolbar { width: 200px; } mapproxy-1.11.0/mapproxy/service/templates/demo/static/theme/default/style.css000066400000000000000000000232441320454472400276130ustar00rootroot00000000000000div.olMap { z-index: 0; padding: 0 !important; margin: 0 !important; cursor: default; } div.olMapViewport { text-align: left; } div.olLayerDiv { -moz-user-select: none; -khtml-user-select: none; } .olLayerGoogleCopyright { left: 2px; bottom: 2px; } .olLayerGoogleV3.olLayerGoogleCopyright { right: auto !important; } .olLayerGooglePoweredBy { left: 2px; bottom: 15px; } .olLayerGoogleV3.olLayerGooglePoweredBy { bottom: 15px !important; } .olControlAttribution { font-size: smaller; right: 3px; bottom: 4.5em; position: absolute; display: block; } .olControlScale { right: 3px; bottom: 3em; display: block; position: absolute; font-size: smaller; } .olControlScaleLine { display: block; position: absolute; left: 10px; bottom: 15px; font-size: xx-small; } .olControlScaleLineBottom { border: solid 2px black; border-bottom: none; margin-top:-2px; text-align: center; } .olControlScaleLineTop { border: solid 2px black; border-top: none; text-align: center; } .olControlPermalink { right: 3px; bottom: 1.5em; display: block; position: absolute; font-size: smaller; } div.olControlMousePosition { bottom: 0; right: 3px; display: block; position: absolute; font-family: Arial; font-size: smaller; } .olControlOverviewMapContainer { position: absolute; bottom: 0; right: 0; } .olControlOverviewMapElement { padding: 10px 18px 10px 10px; background-color: #00008B; -moz-border-radius: 1em 0 0 0; } .olControlOverviewMapMinimizeButton, .olControlOverviewMapMaximizeButton { height: 18px; width: 18px; right: 0; bottom: 80px; cursor: pointer; } .olControlOverviewMapExtentRectangle { overflow: hidden; background-image: url("img/blank.gif"); cursor: move; border: 2px dotted red; } .olControlOverviewMapRectReplacement { overflow: hidden; cursor: move; background-image: url("img/overview_replacement.gif"); background-repeat: no-repeat; background-position: center; } .olLayerGeoRSSDescription { float:left; width:100%; overflow:auto; font-size:1.0em; } .olLayerGeoRSSClose { float:right; color:gray; font-size:1.2em; margin-right:6px; font-family:sans-serif; } .olLayerGeoRSSTitle { float:left;font-size:1.2em; } .olPopupContent { padding:5px; overflow: auto; } .olControlNavigationHistory { background-image: url("img/navigation_history.png"); background-repeat: no-repeat; width: 24px; height: 24px; } .olControlNavigationHistoryPreviousItemActive { background-position: 0 0; } .olControlNavigationHistoryPreviousItemInactive { background-position: 0 -24px; } .olControlNavigationHistoryNextItemActive { background-position: -24px 0; } .olControlNavigationHistoryNextItemInactive { background-position: -24px -24px; } div.olControlSaveFeaturesItemActive { background-image: url(img/save_features_on.png); background-repeat: no-repeat; background-position: 0 1px; } div.olControlSaveFeaturesItemInactive { background-image: url(img/save_features_off.png); background-repeat: no-repeat; background-position: 0 1px; } .olHandlerBoxZoomBox { border: 2px solid red; position: absolute; background-color: white; opacity: 0.50; font-size: 1px; filter: alpha(opacity=50); } .olHandlerBoxSelectFeature { border: 2px solid blue; position: absolute; background-color: white; opacity: 0.50; font-size: 1px; filter: alpha(opacity=50); } .olControlPanPanel { top: 10px; left: 5px; } .olControlPanPanel div { background-image: url(img/pan-panel.png); height: 18px; width: 18px; cursor: pointer; position: absolute; } .olControlPanPanel .olControlPanNorthItemInactive { top: 0; left: 9px; background-position: 0 0; } .olControlPanPanel .olControlPanSouthItemInactive { top: 36px; left: 9px; background-position: 18px 0; } .olControlPanPanel .olControlPanWestItemInactive { position: absolute; top: 18px; left: 0; background-position: 0 18px; } .olControlPanPanel .olControlPanEastItemInactive { top: 18px; left: 18px; background-position: 18px 18px; } .olControlZoomPanel { top: 71px; left: 14px; } .olControlZoomPanel div { background-image: url(img/zoom-panel.png); position: absolute; height: 18px; width: 18px; cursor: pointer; } .olControlZoomPanel .olControlZoomInItemInactive { top: 0; left: 0; background-position: 0 0; } .olControlZoomPanel .olControlZoomToMaxExtentItemInactive { top: 18px; left: 0; background-position: 0 -18px; } .olControlZoomPanel .olControlZoomOutItemInactive { top: 36px; left: 0; background-position: 0 18px; } /* * When a potential text is bigger than the image it move the image * with some headers (closes #3154) */ .olControlPanZoomBar div { font-size: 1px; } .olPopupCloseBox { background: url("img/close.gif") no-repeat; cursor: pointer; } .olFramedCloudPopupContent { padding: 5px; overflow: auto; } .olControlNoSelect { -moz-user-select: none; -khtml-user-select: none; } .olImageLoadError { display: none !important; } /** * Cursor styles */ .olCursorWait { cursor: wait; } .olDragDown { cursor: move; } .olDrawBox { cursor: crosshair; } .olControlDragFeatureOver { cursor: move; } .olControlDragFeatureActive.olControlDragFeatureOver.olDragDown { cursor: -moz-grabbing; } /** * Layer switcher */ .olControlLayerSwitcher { position: absolute; top: 25px; right: 0; width: 20em; font-family: sans-serif; font-weight: bold; margin-top: 3px; margin-left: 3px; margin-bottom: 3px; font-size: smaller; color: white; background-color: transparent; } .olControlLayerSwitcher .layersDiv { padding-top: 5px; padding-left: 10px; padding-bottom: 5px; padding-right: 10px; background-color: darkblue; } .olControlLayerSwitcher .layersDiv .baseLbl, .olControlLayerSwitcher .layersDiv .dataLbl { margin-top: 3px; margin-left: 3px; margin-bottom: 3px; } .olControlLayerSwitcher .layersDiv .baseLayersDiv, .olControlLayerSwitcher .layersDiv .dataLayersDiv { padding-left: 10px; } .olControlLayerSwitcher .maximizeDiv, .olControlLayerSwitcher .minimizeDiv { width: 18px; height: 18px; top: 5px; right: 0; cursor: pointer; } .olBingAttribution { color: #DDD; } .olBingAttribution.road { color: #333; } .olGoogleAttribution.hybrid, .olGoogleAttribution.satellite { color: #EEE; } .olGoogleAttribution { color: #333; } span.olGoogleAttribution a { color: #77C; } span.olGoogleAttribution.hybrid a, span.olGoogleAttribution.satellite a { color: #EEE; } /** * Editing and navigation icons. * (using the editing_tool_bar.png sprint image) */ .olControlNavToolbar , .olControlEditingToolbar { margin: 5px 5px 0 0; } .olControlNavToolbar div, .olControlEditingToolbar div { background-image: url("img/editing_tool_bar.png"); background-repeat: no-repeat; margin: 0 0 5px 5px; width: 24px; height: 22px; cursor: pointer } /* positions */ .olControlEditingToolbar { right: 0; top: 0; } .olControlNavToolbar { top: 295px; left: 9px; } /* layouts */ .olControlEditingToolbar div { float: right; } /* individual controls */ .olControlNavToolbar .olControlNavigationItemInactive, .olControlEditingToolbar .olControlNavigationItemInactive { background-position: -103px -1px; } .olControlNavToolbar .olControlNavigationItemActive , .olControlEditingToolbar .olControlNavigationItemActive { background-position: -103px -24px; } .olControlNavToolbar .olControlZoomBoxItemInactive { background-position: -128px -1px; } .olControlNavToolbar .olControlZoomBoxItemActive { background-position: -128px -24px; } .olControlEditingToolbar .olControlDrawFeaturePointItemInactive { background-position: -77px -1px; } .olControlEditingToolbar .olControlDrawFeaturePointItemActive { background-position: -77px -24px; } .olControlEditingToolbar .olControlDrawFeaturePathItemInactive { background-position: -51px -1px; } .olControlEditingToolbar .olControlDrawFeaturePathItemActive { background-position: -51px -24px; } .olControlEditingToolbar .olControlDrawFeaturePolygonItemInactive{ background-position: -26px -1px; } .olControlEditingToolbar .olControlDrawFeaturePolygonItemActive { background-position: -26px -24px; } div.olControlZoom { position: absolute; top: 8px; left: 8px; background: rgba(255,255,255,0.4); border-radius: 4px; padding: 2px; } div.olControlZoom a { display: block; margin: 1px; padding: 0; color: white; font-size: 18px; font-family: 'Lucida Grande', Verdana, Geneva, Lucida, Arial, Helvetica, sans-serif; font-weight: bold; text-decoration: none; text-align: center; height: 22px; width:22px; line-height: 19px; background: #130085; /* fallback for IE - IE6 requires background shorthand*/ background: rgba(0, 60, 136, 0.5); filter: alpha(opacity=80); } div.olControlZoom a:hover { background: #130085; /* fallback for IE */ background: rgba(0, 60, 136, 0.7); filter: alpha(opacity=100); } @media only screen and (max-width: 600px) { div.olControlZoom a:hover { background: rgba(0, 60, 136, 0.5); } } a.olControlZoomIn { border-radius: 4px 4px 0 0; } a.olControlZoomOut { border-radius: 0 0 4px 4px; } /** * Animations */ .olLayerGrid .olTileImage { -webkit-transition: opacity 0.2s linear; -moz-transition: opacity 0.2s linear; -o-transition: opacity 0.2s linear; transition: opacity 0.2s linear; } mapproxy-1.11.0/mapproxy/service/templates/demo/tms_demo.html000066400000000000000000000063441320454472400244230ustar00rootroot00000000000000{{py: import cgi import textwrap wrapper = textwrap.TextWrapper(replace_whitespace=False, width=90, break_long_words=False) def approx_bbox(layer, srs): from mapproxy.srs import SRS extent = layer.md['extent'].bbox_for(SRS(srs)) return ', '.join(map(lambda x: '%.2f' % x, extent)) menu_title= "TMS %s %s"%(layer.name, srs) jscript_functions=None }} {{def jscript_openlayers}} {{enddef}}

Openlayers Client - Layer {{layer.name}}

Coordinate SystemImage format
{{format}}

Bounding Box

{{', '.join(str(s) for s in layer.grid.bbox)}}

Level and Resolutions

{{for level, res in layer.grid.tile_sets}} {{endfor}}
LevelResolution
{{level}}{{res}}

JavaScript code

{{for line in jscript_openlayers().split('\n')}}
{{cgi.escape(wrapper.fill(line))}}
{{endfor}}
            
mapproxy-1.11.0/mapproxy/service/templates/demo/wms_demo.html000066400000000000000000000060131320454472400244170ustar00rootroot00000000000000{{py: import cgi import textwrap wrapper = textwrap.TextWrapper(replace_whitespace=False, width=90, break_long_words=False) def strip(s): return s.split('/')[1] menu_title = "WMS %s %s" % (layer.name,srs) jscript_functions=None }} {{def jscript_openlayers}} {{enddef}}

Openlayers Client - Layer {{layer.name}}

Coordinate System Image format

JavaScript code

{{for line in jscript_openlayers().split('\n')}}
{{cgi.escape(wrapper.fill(line))}}
{{endfor}}
            
mapproxy-1.11.0/mapproxy/service/templates/demo/wmts_demo.html000066400000000000000000000056051320454472400246110ustar00rootroot00000000000000{{py: import cgi import textwrap wrapper = textwrap.TextWrapper(replace_whitespace=False, width=90, break_long_words=False) def approx_bbox(layer, srs): from mapproxy.srs import SRS extent = layer.md['extent'].bbox_for(SRS(srs)) return ', '.join(map(lambda x: '%.2f' % x, extent)) menu_title= "WMTS %s %s"%(layer.name, srs) jscript_functions=None }} {{def jscript_openlayers}} {{enddef}}

Openlayers Client - Layer {{layer.name}}

Coordinate SystemImage format
{{layer.format}}

Bounding Box

{{', '.join(str(s) for s in layer.grid.bbox)}}

JavaScript code

{{for line in jscript_openlayers().split('\n')}}
{{cgi.escape(wrapper.fill(line))}}
{{endfor}}
            
mapproxy-1.11.0/mapproxy/service/templates/tms_capabilities.xml000066400000000000000000000007171320454472400250360ustar00rootroot00000000000000 {{service.title}} {{service.abstract}} {{for layer in layers.values()}} {{endfor}} mapproxy-1.11.0/mapproxy/service/templates/tms_exception.xml000066400000000000000000000001451320454472400243760ustar00rootroot00000000000000 {{exception}} mapproxy-1.11.0/mapproxy/service/templates/tms_root_resource.xml000066400000000000000000000003431320454472400252720ustar00rootroot00000000000000 {{if service}} {{endif}} mapproxy-1.11.0/mapproxy/service/templates/tms_tilemap_capabilities.xml000066400000000000000000000012661320454472400265510ustar00rootroot00000000000000 {{layer.title}} {{layer.grid.srs_name}} {{for level, res in layer.grid.tile_sets }} {{endfor}} mapproxy-1.11.0/mapproxy/service/templates/wms100capabilities.xml000066400000000000000000000054271320454472400251260ustar00rootroot00000000000000 ]> OGC:WMS {{service.title}} {{service.abstract}} {{if service.online_resource}} {{service.online_resource}} {{else}} {{service.url}} {{endif}} {{service.get('fees', 'none')}} {{service.get('access_constraints', 'none')}} {{for format in formats}} {{if wms100format(format)}} <{{wms100format(format)}}/> {{endif}} {{endfor}} {{for format in info_formats}} {{if wms100info_format(format)}} <{{wms100info_format(format)}}/> {{endif}} {{endfor}} {{def layer_capabilities(layer, with_srs)}} {{if layer.name}} {{ layer.name }} {{endif}} {{ layer.title }} {{for s in srs}}{{s}} {{endfor}} {{py: extent = layer_llbbox(layer)}} {{for srs_code, bbox in layer_srs_bbox(layer)}} {{endfor}} {{if layer.is_active and layer.has_legend and layer.legend_url}} {{endif}} {{if layer.res_range}} {{py: max_scale, min_scale = layer.res_range.scale_hint()}} {{endif}} {{for layer in layer.layers}} {{layer_capabilities(layer, False)|indent}} {{endfor}}
{{enddef}} {{layer_capabilities(layers, True)}} mapproxy-1.11.0/mapproxy/service/templates/wms100exception.xml000066400000000000000000000001221320454472400244560ustar00rootroot00000000000000 {{exception}} mapproxy-1.11.0/mapproxy/service/templates/wms110capabilities.xml000066400000000000000000000125041320454472400251210ustar00rootroot00000000000000 {{else}} {{endif}} ]> OGC:WMS {{service.title}} {{service.abstract}} {{if service.online_resource}} {{else}} {{endif}} {{if service.contact}} {{py:service.contact = bunch(default='', **service.contact)}} {{service.contact.person}} {{service.contact.organization}} {{service.contact.position}} {{service.contact.get('address_type', 'postal')}}
{{service.contact.address}}
{{service.contact.city}} {{service.contact.state}} {{service.contact.postcode}} {{service.contact.country}}
{{service.contact.phone}} {{service.contact.fax}} {{service.contact.email}}
{{endif}} {{service.get('fees', 'none')}} {{service.get('access_constraints', 'none')}}
application/vnd.ogc.wms_xml {{for format in formats}} {{format}} {{endfor}} {{for format in info_formats}} {{format}} {{endfor}} application/vnd.ogc.se_xml application/vnd.ogc.se_inimage application/vnd.ogc.se_blank {{if tile_layers}} {{for layer in tile_layers}} {{layer.grid.srs_name}} {{for level, res in layer.grid.tile_sets}}{{res}} {{endfor}} {{layer.grid.tile_size[0]}} {{layer.grid.tile_size[1]}} {{layer.md['format']}} {{layer.name}} {{endfor}} {{endif}} {{def layer_capabilities(layer, with_srs)}} {{if layer.name}} {{ layer.name }} {{endif}} {{ layer.title }} {{if with_srs}} {{for s in srs}}{{s}} {{endfor}} {{endif}} {{py: extent = layer_llbbox(layer)}} {{for srs_code, bbox in layer_srs_bbox(layer)}} {{endfor}} {{if layer.is_active and layer.has_legend and layer.legend_url}} {{endif}} {{if layer.res_range}} {{py: max_scale, min_scale = layer.res_range.scale_hint()}} {{endif}} {{for layer in layer.layers}} {{layer_capabilities(layer, False)|indent}} {{endfor}}
{{enddef}} {{layer_capabilities(layers, True)}} mapproxy-1.11.0/mapproxy/service/templates/wms110exception.xml000066400000000000000000000004431320454472400244650ustar00rootroot00000000000000 {{exception}} mapproxy-1.11.0/mapproxy/service/templates/wms111capabilities.xml000066400000000000000000000144531320454472400251270ustar00rootroot00000000000000 {{else}} {{endif}} ]> OGC:WMS {{service.title}} {{service.abstract}} {{if service.online_resource}} {{else}} {{endif}} {{if service.contact}} {{py:service.contact = bunch(default='', **service.contact)}} {{service.contact.person}} {{service.contact.organization}} {{service.contact.position}} {{service.contact.get('address_type', 'postal')}}
{{service.contact.address}}
{{service.contact.city}} {{service.contact.state}} {{service.contact.postcode}} {{service.contact.country}}
{{service.contact.phone}} {{service.contact.fax}} {{service.contact.email}}
{{endif}} {{service.get('fees', 'none')}} {{service.get('access_constraints', 'none')}}
application/vnd.ogc.wms_xml {{for format in formats}} {{format}} {{endfor}} {{for format in info_formats}} {{format}} {{endfor}} {{if service.has_legend}} {{for format in formats}} {{format}} {{endfor}} {{endif}} application/vnd.ogc.se_xml application/vnd.ogc.se_inimage application/vnd.ogc.se_blank {{if tile_layers}} {{for layer in tile_layers}} {{layer.grid.srs.srs_code}} {{for level, res in layer.grid.tile_sets}}{{res}} {{endfor}} {{layer.grid.tile_size[0]}} {{layer.grid.tile_size[1]}} {{layer.md['format']}} {{layer.name}} {{endfor}} {{endif}} {{def layer_capabilities(layer, with_srs)}} {{if layer.name}} {{ layer.name }} {{endif}} {{ layer.title }} {{if layer.md and 'abstract' in layer.md}} {{ layer.md['abstract'] }} {{endif}} {{if with_srs}} {{for s in srs}} {{s}} {{endfor}} {{endif}} {{py: extent = layer_llbbox(layer)}} {{for srs_code, bbox in layer_srs_bbox(layer)}} {{endfor}} {{py: md = bunch(default='', **layer.md)}} {{if md.metadata}} {{for data in md.metadata}} {{py: data = bunch(default='', **data)}} {{if wms111metadatatype(data.type)}} {{if data.format}} {{data.format}} {{endif}} {{endif}} {{endfor}} {{endif}} {{if layer.is_active and layer.has_legend and layer.legend_url}} {{endif}} {{if layer.res_range}} {{py: max_scale, min_scale = layer.res_range.scale_hint()}} {{endif}} {{for layer in layer.layers}} {{layer_capabilities(layer, False)|indent}} {{endfor}}
{{enddef}} {{layer_capabilities(layers, True)}} mapproxy-1.11.0/mapproxy/service/templates/wms111exception.xml000066400000000000000000000004431320454472400244660ustar00rootroot00000000000000 {{exception}} mapproxy-1.11.0/mapproxy/service/templates/wms130capabilities.xml000066400000000000000000000312141320454472400251220ustar00rootroot00000000000000 WMS {{service.title}} {{service.abstract}} {{if service.keyword_list and len(service.keyword_list) > 0}} {{for list in service.keyword_list}} {{py: kw=bunch(default='', **list)}} {{for keyword in kw.keywords}} {{keyword}} {{endfor}} {{endfor}} {{endif}}{{if service.online_resource}} {{else}} {{endif}} {{if service.contact}} {{py:service.contact = bunch(default='', **service.contact)}} {{service.contact.person}} {{service.contact.organization}} {{service.contact.position}} {{service.contact.get('address_type', 'postal')}}
{{service.contact.address}}
{{service.contact.city}} {{service.contact.state}} {{service.contact.postcode}} {{service.contact.country}}
{{service.contact.phone}} {{service.contact.fax}} {{service.contact.email}}
{{endif}} {{service.get('fees', 'none')}} {{service.get('access_constraints', 'none')}}
text/xml {{for format in formats}} {{format}} {{endfor}} {{for format in info_formats}} {{format}} {{endfor}} {{if service.has_legend}} {{for format in formats}} {{format}} {{endfor}} {{endif}} XML INIMAGE BLANK {{if inspire_md}} {{def inspire_dates(config)}} {{if 'date_of_publication' in config}} {{config['date_of_publication']}} {{endif}} {{if 'date_of_creation' in config}} {{config['date_of_creation']}} {{endif}} {{if 'date_of_last_revision' in config}} {{config['date_of_last_revision']}} {{endif}} {{enddef}} {{if inspire_md.type == 'linked'}} {{ inspire_md.metadata_url.url | escape }} {{ inspire_md.metadata_url.media_type }} {{ inspire_md.languages.default }} {{ inspire_md.languages.default }} {{else}} {{for rl in inspire_md.resource_locators}} {{ rl['url'] | escape }} {{ rl['media_type'] }} {{endfor}} service {{ inspire_dates(inspire_md.temporal_reference) }} {{for c in inspire_md.conformities}} {{c['title']}} {{ inspire_dates(c) }} {{for uri in c.get('uris', [])}} {{uri | escape}} {{endfor}} {{for rl in c.get('resource_locators')}} {{ rl['url'] | escape }} {{ rl['media_type'] }} {{endfor}} {{c['degree']}} {{endfor}} {{for c in inspire_md.metadata_points_of_contact}} {{c['organisation_name']}} {{c['email']}} {{endfor}} {{inspire_md.metadata_date}} view {{for k in inspire_md.mandatory_keywords}} {{ k }} {{endfor}} {{for k in inspire_md.get('keywords', [])}} {{ k['title']}} {{ inspire_dates(k) }} {{for uri in k.get('uris', [])}} {{uri | escape}} {{endfor}} {{for rl in k.get('resource_locators', [])}} {{ rl['url'] | escape }} {{ rl['media_type'] }} {{endfor}} {{ k['keyword_value']}} {{endfor}} {{ inspire_md.languages.default }} {{ inspire_md.languages.default }} {{if inspire_md.metadata_url}} {{ inspire_md.metadata_url.url | escape }} {{ inspire_md.metadata_url.media_type }} {{endif}} {{endif}} {{endif}} {{def layer_capabilities(layer, with_srs)}} {{py: md = bunch(default='', **layer.md)}} {{if layer.name}} {{ layer.name }} {{endif}} {{ layer.title }} {{if md.abstract}} {{md.abstract}} {{endif}} {{if md.keyword_list and len(md.keyword_list) > 0}} {{for list in md.keyword_list}} {{py: kw=bunch(default='', **list)}} {{for keyword in kw.keywords}} {{keyword}} {{endfor}} {{endfor}} {{endif}} {{if with_srs}} {{for s in srs}} {{s}} {{endfor}} {{endif}} {{py: extent = layer_llbbox(layer)}} {{ extent[0] }} {{ extent[2] }} {{ extent[1] }} {{ extent[3] }} {{for srs_code, bbox in layer_srs_bbox(layer, epsg_axis_order=True)}} {{endfor}} {{if md.attribution}} {{py: attribution = bunch(default='', **md.attribution)}} {{if attribution.title}} {{attribution.title}} {{endif}} {{if attribution.url}} {{endif}} {{if attribution.logo}} {{py: logo = bunch(default='', **attribution.logo)}} {{if logo.format}} {{logo.format}} {{endif}} {{if logo.url}} {{endif}} {{endif}} {{endif}} {{if md.identifier}} {{for idf in md.identifier}} {{py: idf=bunch(default='', **idf)}} {{if idf.url}} {{endif}} {{endfor}} {{for idf in md.identifier}} {{py:idf=bunch(default='', **idf)}} {{idf.value}} {{endfor}} {{endif}} {{if md.metadata}}{{ layer_meta_tag('MetadataURL', md.metadata) }}{{endif}} {{if md.data}}{{layer_meta_tag('DataURL', md.data)}}{{endif}} {{if md.feature_list}}{{layer_meta_tag('FeatureListURL', md.feature_list)}}{{endif}} {{if layer.is_active and layer.has_legend and layer.legend_url}} {{endif}} {{if layer.res_range}} {{py: min_scale, max_scale = layer.res_range.scale_denominator()}} {{if min_scale}}{{min_scale}}{{endif}} {{if max_scale}}{{max_scale}}{{endif}} {{endif}} {{for layer in layer.layers}} {{layer_capabilities(layer, False)|indent}} {{endfor}}
{{enddef}} {{def layer_meta_tag(tag, config)}} {{for data in config}} {{py: data=bunch(default='', **data)}} <{{tag}}{{if data.type}} type="{{data.type|escape}}"{{endif}}> {{if data.format}} {{data.format}} {{endif}} {{endfor}} {{enddef}} {{layer_capabilities(layers, True)}} mapproxy-1.11.0/mapproxy/service/templates/wms130exception.xml000066400000000000000000000006101320454472400244630ustar00rootroot00000000000000 {{exception}} mapproxy-1.11.0/mapproxy/service/templates/wmts100capabilities.xml000066400000000000000000000124111320454472400253010ustar00rootroot00000000000000 {{service.title}} {{service.abstract}} {{if service.keyword_list and len(service.keyword_list) > 0}} {{for list in service.keyword_list}} {{py: kw=bunch(default='', **list)}} {{for keyword in kw.keywords}} {{keyword}} {{endfor}} {{endfor}} {{endif}} OGC WMTS 1.0.0 {{service.get('fees', 'none')}} {{service.get('access_constraints', 'none')}} {{if service.contact}} {{py:service.contact = bunch(default='', **service.contact)}} {{service.contact.organization}} {{service.contact.person}} {{service.contact.position}} {{service.contact.phone}} {{service.contact.fax}} {{service.contact.organization}} {{service.contact.city}} {{service.contact.postcode}} {{service.contact.country}} {{service.contact.email}} {{endif}} {{if not restful}} KVP KVP {{endif}} {{for layer in layers}} {{layer.title}} {{layer.md['extent'].llbbox[0]}} {{layer.md['extent'].llbbox[1]}} {{layer.md['extent'].llbbox[2]}} {{layer.md['extent'].llbbox[3]}} {{layer.name}} image/{{layer.format}} {{for dimension in layer.dimensions}} {{ dimension_keys[dimension] }} {{ layer.dimensions[dimension].default }} {{for value in layer.dimensions[dimension]}} {{ value }} {{endfor}} {{endfor}} {{for grid in layer.grids}} {{grid.name}} {{endfor}} {{if restful}} {{endif}} {{endfor}} {{for tile_matrix_set in tile_matrix_sets}} {{tile_matrix_set.name}} {{tile_matrix_set.srs_name}} {{for matrix in tile_matrix_set}} {{matrix.identifier}} {{matrix.scale_denom}} {{matrix.topleft[0]}} {{matrix.topleft[1]}} {{matrix.tile_size[0]}} {{matrix.tile_size[1]}} {{matrix.grid_size[0]}} {{matrix.grid_size[1]}} {{endfor}} {{endfor}} {{if restful}} {{endif}} mapproxy-1.11.0/mapproxy/service/templates/wmts100exception.xml000066400000000000000000000007431320454472400246530ustar00rootroot00000000000000 {{exception}} mapproxy-1.11.0/mapproxy/service/tile.py000066400000000000000000000440361320454472400203130ustar00rootroot00000000000000# -:- encoding: utf-8 -:- # This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import math import time from mapproxy.compat import iteritems, itervalues from mapproxy.response import Response from mapproxy.exception import RequestError from mapproxy.service.base import Server from mapproxy.request.tile import tile_request from mapproxy.request.base import split_mime_type from mapproxy.layer import map_extent_from_grid from mapproxy.source import SourceError from mapproxy.srs import SRS from mapproxy.grid import default_bboxs from mapproxy.image import BlankImageSource from mapproxy.image.opts import ImageOptions from mapproxy.image.mask import mask_image_source_from_coverage from mapproxy.util.ext.odict import odict from mapproxy.util.coverage import load_limited_to import logging log = logging.getLogger(__name__) from mapproxy.template import template_loader, bunch get_template = template_loader(__name__, 'templates') class TileServer(Server): """ A Tile Server. Supports strict TMS and non-TMS requests. The difference is the support for profiles. The our internal tile cache starts with one tile at the first level (like KML, etc.), but the global-geodetic and global-mercator start with two and four tiles. The ``tile_request`` should set ``use_profiles`` accordingly (eg. False if first level is one tile) """ names = ('tiles', 'tms') request_parser = staticmethod(tile_request) request_methods = ('map', 'tms_capabilities, tms_root_resource') template_file = 'tms_capabilities.xml' layer_template_file = 'tms_tilemap_capabilities.xml' root_resource_template_file = 'tms_root_resource.xml' def __init__(self, layers, md, max_tile_age=None, use_dimension_layers=False, origin=None): Server.__init__(self) self.layers = layers self.md = md self.max_tile_age = max_tile_age self.use_dimension_layers = use_dimension_layers self.origin = origin def map(self, tile_request): """ :return: the requested tile :rtype: Response """ if self.origin and not tile_request.origin: tile_request.origin = self.origin layer, limit_to = self.layer(tile_request) def decorate_img(image): query_extent = (layer.grid.srs.srs_code, layer.tile_bbox(tile_request, use_profiles=tile_request.use_profiles)) return self.decorate_img(image, 'tms', [layer.name], tile_request.http.environ, query_extent) tile = layer.render(tile_request, use_profiles=tile_request.use_profiles, coverage=limit_to, decorate_img=decorate_img) tile_format = getattr(tile, 'format', tile_request.format) resp = Response(tile.as_buffer(), content_type='image/' + tile_format) if tile.cacheable: resp.cache_headers(tile.timestamp, etag_data=(tile.timestamp, tile.size), max_age=self.max_tile_age) else: resp.cache_headers(no_cache=True) resp.make_conditional(tile_request.http) return resp def _internal_layer(self, tile_request): if '_layer_spec' in tile_request.dimensions: name = tile_request.layer + '_' + tile_request.dimensions['_layer_spec'] else: name = tile_request.layer if name in self.layers: return self.layers[name] if name + '_EPSG900913' in self.layers: return self.layers[name + '_EPSG900913'] if name + '_EPSG4326' in self.layers: return self.layers[name + '_EPSG4326'] return None def _internal_dimension_layer(self, tile_request): key = (tile_request.layer, tile_request.dimensions.get('_layer_spec')) return self.layers.get(key) def layer(self, tile_request): if self.use_dimension_layers: internal_layer = self._internal_dimension_layer(tile_request) else: internal_layer = self._internal_layer(tile_request) if internal_layer is None: raise RequestError('unknown layer: ' + tile_request.layer, request=tile_request) limit_to = self.authorize_tile_layer(internal_layer, tile_request) return internal_layer, limit_to def authorize_tile_layer(self, tile_layer, request): if 'mapproxy.authorize' in request.http.environ: if request.tile: query_extent = (tile_layer.grid.srs.srs_code, tile_layer.tile_bbox(request, use_profiles=request.use_profiles)) else: query_extent = None # for layer capabilities result = request.http.environ['mapproxy.authorize']('tms', [tile_layer.name], query_extent=query_extent, environ=request.http.environ) if result['authorized'] == 'unauthenticated': raise RequestError('unauthorized', status=401) if result['authorized'] == 'full': return if result['authorized'] == 'partial': if result['layers'].get(tile_layer.name, {}).get('tile', False) == True: limited_to = result['layers'][tile_layer.name].get('limited_to') if not limited_to: limited_to = result.get('limited_to') if limited_to: return load_limited_to(limited_to) else: return None raise RequestError('forbidden', status=403) def authorized_tile_layers(self, env): if 'mapproxy.authorize' in env: result = env['mapproxy.authorize']('tms', [l for l in self.layers], query_extent=None, environ=env) if result['authorized'] == 'unauthenticated': raise RequestError('unauthorized', status=401) if result['authorized'] == 'full': return self.layers if result['authorized'] == 'none': raise RequestError('forbidden', status=403) allowed_layers = odict() for layer in itervalues(self.layers): if result['layers'].get(layer.name, {}).get('tile', False) == True: allowed_layers[layer.name] = layer return allowed_layers else: return self.layers def tms_capabilities(self, tms_request): """ :return: the rendered tms capabilities :rtype: Response """ service = self._service_md(tms_request) if hasattr(tms_request, 'layer'): layer, limit_to = self.layer(tms_request) result = self._render_layer_template(layer, service) else: layers = self.authorized_tile_layers(tms_request.http.environ) result = self._render_template(layers, service) return Response(result, mimetype='text/xml') def tms_root_resource(self, tms_request): """ :return: root resource with all available versions of the service :rtype: Response """ service = self._service_md(tms_request) result = self._render_root_resource_template(service) return Response(result, mimetype='text/xml') def _service_md(self, map_request): md = dict(self.md) md['url'] = map_request.http.base_url return md def _render_template(self, layers, service): template = get_template(self.template_file) return template.substitute(service=bunch(default='', **service), layers=layers) def _render_layer_template(self, layer, service): template = get_template(self.layer_template_file) return template.substitute(service=bunch(default='', **service), layer=layer) def _render_root_resource_template(self, service): template = get_template(self.root_resource_template_file) return template.substitute(service=bunch(default='', **service)) class TileLayer(object): def __init__(self, name, title, md, tile_manager, dimensions=None): """ :param md: the layer metadata :param tile_manager: the layer tile manager """ self.name = name self.title = title self.md = md self.tile_manager = tile_manager self.dimensions = dimensions self.grid = TileServiceGrid(tile_manager.grid) self.extent = map_extent_from_grid(self.grid) self._empty_tile = None self._mixed_format = True if self.md.get('format', False) == 'mixed' else False self.empty_response_as_png = True @property def bbox(self): return self.grid.bbox @property def srs(self): return self.grid.srs @property def format(self): _mime_class, format, _options = split_mime_type(self.format_mime_type) return format @property def format_mime_type(self): # force png format for capabilities & requests if mixed format if self._mixed_format: return 'image/png' return self.md.get('format', 'image/png') def _internal_tile_coord(self, tile_request, use_profiles=False): tile_coord = self.grid.internal_tile_coord(tile_request.tile, use_profiles) if tile_coord is None: raise RequestError('The requested tile is outside the bounding box' ' of the tile map.', request=tile_request, code='TileOutOfRange') if tile_request.origin == 'nw' and self.grid.origin not in ('ul', 'nw'): tile_coord = self.grid.flip_tile_coord(tile_coord) elif tile_request.origin == 'sw' and self.grid.origin not in ('ll', 'sw', None): tile_coord = self.grid.flip_tile_coord(tile_coord) return tile_coord def empty_response(self): if self.empty_response_as_png: format = 'png' else: format = self.format if not self._empty_tile: img = BlankImageSource(size=self.grid.tile_size, image_opts=ImageOptions(format=format, transparent=True)) self._empty_tile = img.as_buffer().read() return ImageResponse(self._empty_tile, format=format, timestamp=time.time()) def tile_bbox(self, tile_request, use_profiles=False, limit=False): tile_coord = self._internal_tile_coord(tile_request, use_profiles=use_profiles) return self.grid.tile_bbox(tile_coord, limit=limit) def checked_dimensions(self, tile_request): dimensions = {} for dimension, values in iteritems(self.dimensions): value = tile_request.dimensions.get(dimension) if value in values: dimensions[dimension] = value elif not value or value == 'default': dimensions[dimension] = values.default else: raise RequestError('invalid dimension value (%s=%s).' % (dimension, value), request=tile_request, code='InvalidParameterValue') return dimensions def render(self, tile_request, use_profiles=False, coverage=None, decorate_img=None): if tile_request.format != self.format: raise RequestError('invalid format (%s). this tile set only supports (%s)' % (tile_request.format, self.format), request=tile_request, code='InvalidParameterValue') tile_coord = self._internal_tile_coord(tile_request, use_profiles=use_profiles) coverage_intersects = False if coverage: tile_bbox = self.grid.tile_bbox(tile_coord) if coverage.contains(tile_bbox, self.grid.srs): pass elif coverage.intersects(tile_bbox, self.grid.srs): coverage_intersects = True else: return self.empty_response() dimensions = self.checked_dimensions(tile_request) try: with self.tile_manager.session(): tile = self.tile_manager.load_tile_coord(tile_coord, dimensions=dimensions, with_metadata=True) if tile.source is None: return self.empty_response() # Provide the wrapping WSGI app or filter the opportunity to process the # image before it's wrapped up in a response if decorate_img: tile.source = decorate_img(tile.source) if coverage_intersects: if self.empty_response_as_png: format = 'png' image_opts = ImageOptions(transparent=True, format='png') else: format = self.format image_opts = tile.source.image_opts tile.source = mask_image_source_from_coverage( tile.source, tile_bbox, self.grid.srs, coverage, image_opts) return TileResponse(tile, format=format, image_opts=image_opts) format = None if self._mixed_format else tile_request.format return TileResponse(tile, format=format, image_opts=self.tile_manager.image_opts) except SourceError as e: raise RequestError(e.args[0], request=tile_request, internal=True) class ImageResponse(object): """ Response from an image. """ def __init__(self, img, format, timestamp): self.img = img self.timestamp = timestamp self.format = format self.size = 0 self.cacheable = True def as_buffer(self): return self.img class TileResponse(object): """ Response from a Tile. """ def __init__(self, tile, format=None, timestamp=None, image_opts=None): self.tile = tile self.timestamp = tile.timestamp self.size = tile.size self.cacheable = tile.cacheable self._buf = self.tile.source_buffer(format=format, image_opts=image_opts) self.format = format or self._format_from_magic_bytes() def as_buffer(self): return self._buf def _format_from_magic_bytes(self): #read the 2 magic bytes from the buffer magic_bytes = self._buf.read(2) self._buf.seek(0) if magic_bytes == b'\xFF\xD8': return 'jpeg' return 'png' class TileServiceGrid(object): """ Wraps a `TileGrid` and adds some ``TileService`` specific methods. """ def __init__(self, grid): self.grid = grid self.profile = None if self.grid.srs == SRS(900913) and self.grid.bbox == default_bboxs[SRS((900913))]: self.profile = 'global-mercator' self.srs_name = 'OSGEO:41001' # as required by TMS 1.0.0 self._skip_first_level = True elif self.grid.srs == SRS(4326) and self.grid.bbox == default_bboxs[SRS((4326))]: self.profile = 'global-geodetic' self.srs_name = 'EPSG:4326' self._skip_first_level = True else: self.profile = 'local' self.srs_name = self.grid.srs.srs_code self._skip_first_level = False self._skip_odd_level = False res_factor = self.grid.resolutions[0]/self.grid.resolutions[1] if res_factor == math.sqrt(2): self._skip_odd_level = True def internal_level(self, level): """ :return: the internal level """ if self._skip_first_level: level += 1 if self._skip_odd_level: level += 1 if self._skip_odd_level: level *= 2 return level @property def bbox(self): """ :return: the bbox of all tiles of the first level """ first_level = self.internal_level(0) grid_size = self.grid.grid_sizes[first_level] return self.grid._get_bbox([(0, 0, first_level), (grid_size[0]-1, grid_size[1]-1, first_level)]) def __getattr__(self, key): return getattr(self.grid, key) @property def tile_sets(self): """ Get all public tile sets for this layer. :return: the order and resolution of each tile set """ tile_sets = [] num_levels = self.grid.levels start = 0 step = 1 if self._skip_first_level: if self._skip_odd_level: start = 2 else: start = 1 if self._skip_odd_level: step = 2 for order, level in enumerate(range(start, num_levels, step)): tile_sets.append((order, self.grid.resolutions[level])) return tile_sets def internal_tile_coord(self, tile_coord, use_profiles): """ Converts public tile coords to internal tile coords. :param tile_coord: the public tile coord :param use_profiles: True if the tile service supports global profiles (see `mapproxy.core.server.TileServer`) """ x, y, z = tile_coord if int(z) < 0: return None if use_profiles and self._skip_first_level: z += 1 if self._skip_odd_level: z *= 2 return self.grid.limit_tile((x, y, z)) def external_tile_coord(self, tile_coord, use_profiles): """ Converts internal tile coords to external tile coords. :param tile_coord: the internal tile coord :param use_profiles: True if the tile service supports global profiles (see `mapproxy.core.server.TileServer`) """ x, y, z = tile_coord if z < 0: return None if use_profiles and self._skip_first_level: z -= 1 if self._skip_odd_level: z //= 2 return (x, y, z) mapproxy-1.11.0/mapproxy/service/wms.py000066400000000000000000000770351320454472400201710ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010-2014 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ WMS service handler """ from mapproxy.compat import iteritems from mapproxy.compat.itertools import chain from functools import partial from mapproxy.cache.tile import CacheInfo from mapproxy.request.wms import (wms_request, WMS111LegendGraphicRequest, mimetype_from_infotype, infotype_from_mimetype, switch_bbox_epsg_axis_order) from mapproxy.srs import SRS, TransformationError from mapproxy.service.base import Server from mapproxy.response import Response from mapproxy.source import SourceError from mapproxy.exception import RequestError from mapproxy.image import bbox_position_in_image, SubImageSource, BlankImageSource from mapproxy.image.merge import concat_legends, LayerMerger from mapproxy.image.opts import ImageOptions from mapproxy.image.message import attribution_image, message_image from mapproxy.layer import BlankImage, MapQuery, InfoQuery, LegendQuery, MapError, LimitedLayer from mapproxy.layer import MapBBOXError, merge_layer_extents, merge_layer_res_ranges from mapproxy.util import async from mapproxy.util.py import cached_property, reraise from mapproxy.util.coverage import load_limited_to from mapproxy.util.ext.odict import odict from mapproxy.template import template_loader, bunch, recursive_bunch from mapproxy.service import template_helper from mapproxy.layer import DefaultMapExtent, MapExtent get_template = template_loader(__name__, 'templates', namespace=template_helper.__dict__) class PERMIT_ALL_LAYERS(object): pass class WMSServer(Server): service = 'wms' fi_transformers = None def __init__(self, root_layer, md, srs, image_formats, request_parser=None, tile_layers=None, attribution=None, info_types=None, strict=False, on_error='raise', concurrent_layer_renderer=1, max_output_pixels=None, srs_extents=None, max_tile_age=None, versions=None, inspire_md=None, ): Server.__init__(self) self.request_parser = request_parser or partial(wms_request, strict=strict, versions=versions) self.root_layer = root_layer self.layers = root_layer.child_layers() self.tile_layers = tile_layers or {} self.strict = strict self.attribution = attribution self.md = md self.on_error = on_error self.concurrent_layer_renderer = concurrent_layer_renderer self.image_formats = image_formats self.info_types = info_types self.srs = srs self.srs_extents = srs_extents self.max_output_pixels = max_output_pixels self.max_tile_age = max_tile_age self.inspire_md = inspire_md def map(self, map_request): self.check_map_request(map_request) params = map_request.params query = MapQuery(params.bbox, params.size, SRS(params.srs), params.format) if map_request.params.get('tiled', 'false').lower() == 'true': query.tiled_only = True orig_query = query if self.srs_extents and params.srs in self.srs_extents: # limit query to srs_extent if query is larger query_extent = MapExtent(params.bbox, SRS(params.srs)) if not self.srs_extents[params.srs].contains(query_extent): limited_extent = self.srs_extents[params.srs].intersection(query_extent) if not limited_extent: img_opts = self.image_formats[params.format_mime_type].copy() img_opts.bgcolor = params.bgcolor img_opts.transparent = params.transparent img = BlankImageSource(size=params.size, image_opts=img_opts, cacheable=True) return Response(img.as_buffer(), content_type=img_opts.format.mime_type) sub_size, offset, sub_bbox = bbox_position_in_image(params.bbox, params.size, limited_extent.bbox) query = MapQuery(sub_bbox, sub_size, SRS(params.srs), params.format) actual_layers = odict() for layer_name in map_request.params.layers: layer = self.layers[layer_name] # only add if layer renders the query if layer.renders_query(query): # if layer is not transparent and will be rendered, # remove already added (then hidden) layers if layer.is_opaque(query): actual_layers = odict() for layer_name, map_layers in layer.map_layers_for_query(query): actual_layers[layer_name] = map_layers authorized_layers, coverage = self.authorized_layers('map', actual_layers.keys(), map_request.http.environ, query_extent=(query.srs.srs_code, query.bbox)) self.filter_actual_layers(actual_layers, map_request.params.layers, authorized_layers) render_layers = [] for layers in actual_layers.values(): render_layers.extend(layers) self.update_query_with_fwd_params(query, params=params, layers=render_layers) raise_source_errors = True if self.on_error == 'raise' else False renderer = LayerRenderer(render_layers, query, map_request, raise_source_errors=raise_source_errors, concurrent_rendering=self.concurrent_layer_renderer) merger = LayerMerger() renderer.render(merger) if self.attribution and self.attribution.get('text') and not query.tiled_only: merger.add(attribution_image(self.attribution['text'], query.size)) img_opts = self.image_formats[params.format_mime_type].copy() img_opts.bgcolor = params.bgcolor img_opts.transparent = params.transparent result = merger.merge(size=query.size, image_opts=img_opts, bbox=query.bbox, bbox_srs=params.srs, coverage=coverage) if query != orig_query: result = SubImageSource(result, size=orig_query.size, offset=offset, image_opts=img_opts) # Provide the wrapping WSGI app or filter the opportunity to process the # image before it's wrapped up in a response result = self.decorate_img(result, 'wms.map', actual_layers.keys(), map_request.http.environ, (query.srs.srs_code, query.bbox)) try: result_buf = result.as_buffer(img_opts) except IOError as ex: raise RequestError('error while processing image file: %s' % ex, request=map_request) resp = Response(result_buf, content_type=img_opts.format.mime_type) if query.tiled_only and isinstance(result.cacheable, CacheInfo): cache_info = result.cacheable resp.cache_headers(cache_info.timestamp, etag_data=(cache_info.timestamp, cache_info.size), max_age=self.max_tile_age) resp.make_conditional(map_request.http) if not result.cacheable: resp.cache_headers(no_cache=True) return resp def capabilities(self, map_request): # TODO: debug layer # if '__debug__' in map_request.params: # layers = self.layers.values() # else: # layers = [layer for name, layer in iteritems(self.layers) # if name != '__debug__'] if map_request.params.get('tiled', 'false').lower() == 'true': tile_layers = self.tile_layers.values() else: tile_layers = [] service = self._service_md(map_request) root_layer = self.authorized_capability_layers(map_request.http.environ) info_types = ['text', 'html', 'xml'] # defaults if self.info_types: info_types = self.info_types elif self.fi_transformers: info_types = self.fi_transformers.keys() info_formats = [mimetype_from_infotype(map_request.version, info_type) for info_type in info_types] result = Capabilities(service, root_layer, tile_layers, self.image_formats, info_formats, srs=self.srs, srs_extents=self.srs_extents, inspire_md=self.inspire_md, ).render(map_request) return Response(result, mimetype=map_request.mime_type) def featureinfo(self, request): infos = [] self.check_featureinfo_request(request) p = request.params query = InfoQuery(p.bbox, p.size, SRS(p.srs), p.pos, p['info_format'], format=request.params.format or None, feature_count=p.get('feature_count')) actual_layers = odict() for layer_name in request.params.query_layers: layer = self.layers[layer_name] if not layer.queryable: raise RequestError('layer %s is not queryable' % layer_name, request=request) for layer_name, info_layers in layer.info_layers_for_query(query): actual_layers[layer_name] = info_layers authorized_layers, coverage = self.authorized_layers('featureinfo', actual_layers.keys(), request.http.environ, query_extent=(query.srs.srs_code, query.bbox)) self.filter_actual_layers(actual_layers, request.params.layers, authorized_layers) # outside of auth-coverage if coverage and not coverage.contains(query.coord, query.srs): infos = [] else: info_layers = [] for layers in actual_layers.values(): info_layers.extend(layers) for layer in info_layers: info = layer.get_info(query) if info is None: continue infos.append(info) mimetype = None if 'info_format' in request.params: mimetype = request.params.info_format if not infos: return Response('', mimetype=mimetype) if self.fi_transformers: doc = infos[0].combine(infos) if doc.info_type == 'text': resp = doc.as_string() mimetype = 'text/plain' else: if not mimetype: if 'xml' in self.fi_transformers: info_type = 'xml' elif 'html' in self.fi_transformers: info_type = 'html' else: info_type = 'text' mimetype = mimetype_from_infotype(request.version, info_type) else: info_type = infotype_from_mimetype(request.version, mimetype) resp = self.fi_transformers[info_type](doc).as_string() else: mimetype = mimetype_from_infotype(request.version, infos[0].info_type) if len(infos) > 1: resp = infos[0].combine(infos).as_string() else: resp = infos[0].as_string() return Response(resp, mimetype=mimetype) def check_map_request(self, request): if self.max_output_pixels and \ (request.params.size[0] * request.params.size[1]) > self.max_output_pixels: request.prevent_image_exception = True raise RequestError("image size too large", request=request) self.validate_layers(request) request.validate_format(self.image_formats) request.validate_srs(self.srs) def update_query_with_fwd_params(self, query, params, layers): # forward relevant request params into MapQuery.dimensions for layer in layers: if not hasattr(layer, 'fwd_req_params'): continue for p in layer.fwd_req_params: if p in params: query.dimensions[p] = params[p] def check_featureinfo_request(self, request): self.validate_layers(request) request.validate_srs(self.srs) def validate_layers(self, request): query_layers = request.params.query_layers if hasattr(request, 'query_layers') else [] for layer in chain(request.params.layers, query_layers): if layer not in self.layers: raise RequestError('unknown layer: ' + str(layer), code='LayerNotDefined', request=request) def check_legend_request(self, request): if request.params.layer not in self.layers: raise RequestError('unknown layer: ' + request.params.layer, code='LayerNotDefined', request=request) #TODO: If layer not in self.layers raise RequestError def legendgraphic(self, request): legends = [] self.check_legend_request(request) layer = request.params.layer if not self.layers[layer].has_legend: raise RequestError('layer %s has no legend graphic' % layer, request=request) legend = self.layers[layer].legend(request) [legends.append(i) for i in legend if i is not None] result = concat_legends(legends) if 'format' in request.params: mimetype = request.params.format_mime_type else: mimetype = 'image/png' img_opts = self.image_formats[request.params.format_mime_type] return Response(result.as_buffer(img_opts), mimetype=mimetype) def _service_md(self, map_request): md = dict(self.md) md['url'] = map_request.url md['has_legend'] = self.root_layer.has_legend return md def authorized_layers(self, feature, layers, env, query_extent): if 'mapproxy.authorize' in env: result = env['mapproxy.authorize']('wms.' + feature, layers[:], environ=env, query_extent=query_extent) if result['authorized'] == 'unauthenticated': raise RequestError('unauthorized', status=401) if result['authorized'] == 'full': return PERMIT_ALL_LAYERS, None layers = {} if result['authorized'] == 'partial': for layer_name, permissions in iteritems(result['layers']): if permissions.get(feature, False) == True: layers[layer_name] = permissions.get('limited_to') limited_to = result.get('limited_to') if limited_to: coverage = load_limited_to(limited_to) else: coverage = None return layers, coverage else: return PERMIT_ALL_LAYERS, None def filter_actual_layers(self, actual_layers, requested_layers, authorized_layers): if authorized_layers is not PERMIT_ALL_LAYERS: requested_layer_names = set(requested_layers) for layer_name in actual_layers.keys(): if layer_name not in authorized_layers: # check whether layer was requested explicit... if layer_name in requested_layer_names: raise RequestError('forbidden', status=403) # or implicit (part of group layer) else: del actual_layers[layer_name] elif authorized_layers[layer_name] is not None: limited_to = load_limited_to(authorized_layers[layer_name]) actual_layers[layer_name] = [LimitedLayer(lyr, limited_to) for lyr in actual_layers[layer_name]] def authorized_capability_layers(self, env): if 'mapproxy.authorize' in env: result = env['mapproxy.authorize']('wms.capabilities', self.layers.keys(), environ=env) if result['authorized'] == 'unauthenticated': raise RequestError('unauthorized', status=401) if result['authorized'] == 'full': return self.root_layer if result['authorized'] == 'partial': limited_to = result.get('limited_to') if limited_to: coverage = load_limited_to(limited_to) else: coverage = None return FilteredRootLayer(self.root_layer, result['layers'], coverage=coverage) raise RequestError('forbidden', status=403) else: return self.root_layer class FilteredRootLayer(object): def __init__(self, root_layer, permissions, coverage=None): self.root_layer = root_layer self.permissions = permissions self.coverage = coverage def __getattr__(self, name): return getattr(self.root_layer, name) @cached_property def extent(self): layer_name = self.root_layer.name limited_to = self.permissions.get(layer_name, {}).get('limited_to') extent = self.root_layer.extent if limited_to: coverage = load_limited_to(limited_to) limited_coverage = coverage.intersection(extent.bbox, extent.srs) extent = limited_coverage.extent if self.coverage: limited_coverage = self.coverage.intersection(extent.bbox, extent.srs) extent = limited_coverage.extent return extent @property def queryable(self): if not self.root_layer.queryable: return False layer_name = self.root_layer.name if not layer_name or self.permissions.get(layer_name, {}).get('featureinfo', False): return True return False def layer_permitted(self, layer): if not self.permissions.get(layer.name, {}).get('map', False): return False extent = layer.extent limited_to = self.permissions.get(layer.name, {}).get('limited_to') if limited_to: coverage = load_limited_to(limited_to) if not coverage.intersects(extent.bbox, extent.srs): return False if self.coverage: if not self.coverage.intersects(extent.bbox, extent.srs): return False return True @cached_property def layers(self): layers = [] for layer in self.root_layer.layers: if not layer.name or self.layer_permitted(layer): filtered_layer = FilteredRootLayer(layer, self.permissions, self.coverage) if filtered_layer.is_active or filtered_layer.layers: # add filtered_layer only if it is active (no grouping layer) # or if it contains other active layers layers.append(filtered_layer) return layers DEFAULT_EXTENTS = { 'EPSG:3857': DefaultMapExtent(), 'EPSG:4326': DefaultMapExtent(), 'EPSG:900913': DefaultMapExtent(), } def limit_srs_extents(srs_extents, supported_srs): """ Limit srs_extents to supported_srs. """ if srs_extents: srs_extents = srs_extents.copy() else: srs_extents = DEFAULT_EXTENTS.copy() for srs in list(srs_extents.keys()): if srs not in supported_srs: srs_extents.pop(srs) return srs_extents class Capabilities(object): """ Renders WMS capabilities documents. """ def __init__(self, server_md, layers, tile_layers, image_formats, info_formats, srs, srs_extents=None, epsg_axis_order=False, inspire_md=None, ): self.service = server_md self.layers = layers self.tile_layers = tile_layers self.image_formats = image_formats self.info_formats = info_formats self.srs = srs self.srs_extents = limit_srs_extents(srs_extents, srs) self.inspire_md = inspire_md def layer_srs_bbox(self, layer, epsg_axis_order=False): for srs, extent in iteritems(self.srs_extents): # is_default is True when no explicit bbox is defined for this srs # use layer extent if extent.is_default: bbox = layer.extent.bbox_for(SRS(srs)) elif layer.extent.is_default: bbox = extent.bbox_for(SRS(srs)) else: # Use intersection of srs_extent and layer.extent. bbox = extent.intersection(layer.extent).bbox_for(SRS(srs)) if epsg_axis_order: bbox = switch_bbox_epsg_axis_order(bbox, srs) if srs in self.srs: yield srs, bbox # add native srs layer_srs_code = layer.extent.srs.srs_code if layer_srs_code not in self.srs_extents: bbox = layer.extent.bbox if epsg_axis_order: bbox = switch_bbox_epsg_axis_order(bbox, layer_srs_code) if layer_srs_code in self.srs: yield layer_srs_code, bbox def layer_llbbox(self, layer): if 'EPSG:4326' in self.srs_extents: llbbox = self.srs_extents['EPSG:4326'].intersection(layer.extent).llbbox return limit_llbbox(llbbox) return limit_llbbox(layer.extent.llbbox) def render(self, _map_request): return self._render_template(_map_request.capabilities_template) def _render_template(self, template): template = get_template(template) inspire_md = None if self.inspire_md: inspire_md = recursive_bunch(default='', **self.inspire_md) doc = template.substitute(service=bunch(default='', **self.service), layers=self.layers, formats=self.image_formats, info_formats=self.info_formats, srs=self.srs, tile_layers=self.tile_layers, layer_srs_bbox=self.layer_srs_bbox, layer_llbbox=self.layer_llbbox, inspire_md=inspire_md, ) # strip blank lines doc = '\n'.join(l for l in doc.split('\n') if l.rstrip()) return doc def limit_llbbox(bbox): """ Limit the long/lat bounding box to +-180/89.99999999 degrees. Some clients can't handle +-90 north/south, so we subtract a tiny bit. >>> ', '.join('%.6f' % x for x in limit_llbbox((-200,-90.0, 180, 90))) '-180.000000, -89.999999, 180.000000, 89.999999' >>> ', '.join('%.6f' % x for x in limit_llbbox((-20,-9.0, 10, 10))) '-20.000000, -9.000000, 10.000000, 10.000000' """ minx, miny, maxx, maxy = bbox minx = max(-180, minx) miny = max(-89.999999, miny) maxx = min(180, maxx) maxy = min(89.999999, maxy) return minx, miny, maxx, maxy class LayerRenderer(object): def __init__(self, layers, query, request, raise_source_errors=True, concurrent_rendering=1): self.layers = layers self.query = query self.request = request self.raise_source_errors = raise_source_errors self.concurrent_rendering = concurrent_rendering def render(self, layer_merger): render_layers = combined_layers(self.layers, self.query) if not render_layers: return async_pool = async.Pool(size=min(len(render_layers), self.concurrent_rendering)) if self.raise_source_errors: return self._render_raise_exceptions(async_pool, render_layers, layer_merger) else: return self._render_capture_source_errors(async_pool, render_layers, layer_merger) def _render_raise_exceptions(self, async_pool, render_layers, layer_merger): # call _render_layer, raise all exceptions try: for layer_task in async_pool.imap(self._render_layer, render_layers, use_result_objects=True): if layer_task.exception is None: layer, layer_img = layer_task.result if layer_img is not None: layer_merger.add(layer_img, layer.coverage) else: ex = layer_task.exception async_pool.shutdown(True) reraise(ex) except SourceError as ex: raise RequestError(ex.args[0], request=self.request) def _render_capture_source_errors(self, async_pool, render_layers, layer_merger): # call _render_layer, capture SourceError exceptions errors = [] rendered = 0 for layer_task in async_pool.imap(self._render_layer, render_layers, use_result_objects=True): if layer_task.exception is None: layer, layer_img = layer_task.result if layer_img is not None: layer_merger.add(layer_img, layer.coverage) rendered += 1 else: layer_merger.cacheable = False ex = layer_task.exception if isinstance(ex[1], SourceError): errors.append(ex[1].args[0]) else: async_pool.shutdown(True) reraise(ex) if render_layers and not rendered: errors = '\n'.join(errors) raise RequestError('Could not get any sources:\n'+errors, request=self.request) if errors: layer_merger.add(message_image('\n'.join(errors), self.query.size, image_opts=ImageOptions(transparent=True))) def _render_layer(self, layer): try: layer_img = layer.get_map(self.query) if layer_img is not None: layer_img.opacity = layer.opacity return layer, layer_img except SourceError: raise except MapBBOXError: raise RequestError('Request too large or invalid BBOX.', request=self.request) except MapError as e: raise RequestError('Invalid request: %s' % e.args[0], request=self.request) except TransformationError: raise RequestError('Could not transform BBOX: Invalid result.', request=self.request) except BlankImage: return layer, None class WMSLayerBase(object): """ Base class for WMS layer (layer groups and leaf layers). """ "True if layer is an actual layer (not a group only)" is_active = True "list of sublayers" layers = [] "metadata dictionary with tile, name, etc." md = {} "True if .info() is supported" queryable = False "True is .legend() is supported" has_legend = False legend_url = None legend_size = None "resolution range (i.e. ScaleHint) of the layer" res_range = None "MapExtend of the layer" extent = None def map_layers_for_query(self, query): raise NotImplementedError() def legend(self, query): raise NotImplementedError() def info(self, query): raise NotImplementedError() class WMSLayer(WMSLayerBase): """ Class for WMS layers. Combines map, info and legend sources with metadata. """ is_active = True layers = [] def __init__(self, name, title, map_layers, info_layers=[], legend_layers=[], res_range=None, md=None): self.name = name self.title = title self.md = md or {} self.map_layers = map_layers self.info_layers = info_layers self.legend_layers = legend_layers self.extent = merge_layer_extents(map_layers) if res_range is None: res_range = merge_layer_res_ranges(map_layers) self.res_range = res_range self.queryable = True if info_layers else False self.has_legend = True if legend_layers else False def is_opaque(self, query): return any(l.is_opaque(query) for l in self.map_layers) def renders_query(self, query): if self.res_range and not self.res_range.contains(query.bbox, query.size, query.srs): return False return True def map_layers_for_query(self, query): if not self.map_layers: return [] return [(self.name, self.map_layers)] def info_layers_for_query(self, query): if not self.info_layers: return [] return [(self.name, self.info_layers)] def legend(self, request): p = request.params query = LegendQuery(p.format, p.scale) for lyr in self.legend_layers: yield lyr.get_legend(query) @property def legend_size(self): width = 0 height = 0 for layer in self.legend_layers: width = max(layer.size[0], width) height += layer.size[1] return (width, height) @property def legend_url(self): if self.has_legend: req = WMS111LegendGraphicRequest(url='?', param=dict(format='image/png', layer=self.name, sld_version='1.1.0')) return req.complete_url else: return None def child_layers(self): return {self.name: self} class WMSGroupLayer(WMSLayerBase): """ Class for WMS group layers. Groups multiple wms layers, but can also contain a single layer (``this``) that represents this layer. """ def __init__(self, name, title, this, layers, md=None): self.name = name self.title = title self.this = this self.md = md or {} self.is_active = True if this is not None else False self.layers = layers self.has_legend = True if this and this.has_legend or any(l.has_legend for l in layers) else False self.queryable = True if this and this.queryable or any(l.queryable for l in layers) else False all_layers = layers + ([self.this] if self.this else []) self.extent = merge_layer_extents(all_layers) self.res_range = merge_layer_res_ranges(all_layers) def is_opaque(self, query): return any(l.is_opaque(query) for l in self.layers) @property def legend_size(self): return self.this.legend_size @property def legend_url(self): return self.this.legend_url def renders_query(self, query): if self.res_range and not self.res_range.contains(query.bbox, query.size, query.srs): return False return True def map_layers_for_query(self, query): if self.this: return self.this.map_layers_for_query(query) else: layers = [] for layer in self.layers: layers.extend(layer.map_layers_for_query(query)) return layers def info_layers_for_query(self, query): if self.this: return self.this.info_layers_for_query(query) else: layers = [] for layer in self.layers: layers.extend(layer.info_layers_for_query(query)) return layers def child_layers(self): layers = odict() if self.name: layers[self.name] = self for lyr in self.layers: if hasattr(lyr, 'child_layers'): layers.update(lyr.child_layers()) elif lyr.name: layers[lyr.name] = lyr return layers def combined_layers(layers, query): """ Returns a new list of the layers where all adjacent layers are combined if possible. """ if len(layers) <= 1: return layers layers = layers[:] combined_layers = [layers.pop(0)] while layers: current_layer = layers.pop(0) combined = combined_layers[-1].combined_layer(current_layer, query) if combined: # change last layer with combined combined_layers[-1] = combined else: combined_layers.append(current_layer) return combined_layers mapproxy-1.11.0/mapproxy/service/wmts.py000066400000000000000000000270741320454472400203530ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ WMS service handler """ from __future__ import print_function from functools import partial from mapproxy.compat import iteritems, itervalues, iterkeys from mapproxy.request.wmts import ( wmts_request, make_wmts_rest_request_parser, URLTemplateConverter, ) from mapproxy.service.base import Server from mapproxy.response import Response from mapproxy.exception import RequestError from mapproxy.util.coverage import load_limited_to from mapproxy.util.ext.odict import odict from mapproxy.template import template_loader, bunch env = {'bunch': bunch} get_template = template_loader(__name__, 'templates', namespace=env) import logging log = logging.getLogger(__name__) class WMTSServer(Server): service = 'wmts' def __init__(self, layers, md, request_parser=None, max_tile_age=None): Server.__init__(self) self.request_parser = request_parser or wmts_request self.md = md self.max_tile_age = max_tile_age self.layers, self.matrix_sets = self._matrix_sets(layers) self.capabilities_class = Capabilities def _matrix_sets(self, layers): sets = {} layers_grids = odict() for layer in layers.values(): grid = layer.grid if not grid.supports_access_with_origin('nw'): log.warn("skipping layer '%s' for WMTS, grid '%s' of cache '%s' is not compatible with WMTS", layer.name, grid.name, layer.md['cache_name']) continue if grid.name not in sets: try: sets[grid.name] = TileMatrixSet(grid) except AssertionError: continue # TODO layers_grids.setdefault(layer.name, odict())[grid.name] = layer wmts_layers = odict() for layer_name, layers in layers_grids.items(): wmts_layers[layer_name] = WMTSTileLayer(layers) return wmts_layers, sets.values() def capabilities(self, request): service = self._service_md(request) layers = self.authorized_tile_layers(request.http.environ) result = self.capabilities_class(service, layers, self.matrix_sets).render(request) return Response(result, mimetype='application/xml') def tile(self, request): self.check_request(request) tile_layer = self.layers[request.layer][request.tilematrixset] if not request.format: request.format = tile_layer.format self.check_request_dimensions(tile_layer, request) limited_to = self.authorize_tile_layer(tile_layer, request) def decorate_img(image): query_extent = tile_layer.grid.srs.srs_code, tile_layer.tile_bbox(request) return self.decorate_img(image, 'wmts', [tile_layer.name], request.http.environ, query_extent) tile = tile_layer.render(request, coverage=limited_to, decorate_img=decorate_img) # set the content_type to tile.format and not to request.format ( to support mixed_mode) resp = Response(tile.as_buffer(), content_type='image/' + tile.format) resp.cache_headers(tile.timestamp, etag_data=(tile.timestamp, tile.size), max_age=self.max_tile_age) resp.make_conditional(request.http) return resp def authorize_tile_layer(self, tile_layer, request): if 'mapproxy.authorize' in request.http.environ: query_extent = tile_layer.grid.srs.srs_code, tile_layer.tile_bbox(request) result = request.http.environ['mapproxy.authorize']('wmts', [tile_layer.name], query_extent=query_extent, environ=request.http.environ) if result['authorized'] == 'unauthenticated': raise RequestError('unauthorized', status=401) if result['authorized'] == 'full': return if result['authorized'] == 'partial': if result['layers'].get(tile_layer.name, {}).get('tile', False) == True: limited_to = result['layers'][tile_layer.name].get('limited_to') if not limited_to: limited_to = result.get('limited_to') if limited_to: return load_limited_to(limited_to) else: return None raise RequestError('forbidden', status=403) def authorized_tile_layers(self, env): if 'mapproxy.authorize' in env: result = env['mapproxy.authorize']('wmts', [l for l in self.layers], query_extent=None, environ=env) if result['authorized'] == 'unauthenticated': raise RequestError('unauthorized', status=401) if result['authorized'] == 'full': return self.layers.values() if result['authorized'] == 'none': raise RequestError('forbidden', status=403) allowed_layers = [] for layer in itervalues(self.layers): if result['layers'].get(layer.name, {}).get('tile', False) == True: allowed_layers.append(layer) return allowed_layers else: return self.layers.values() def check_request(self, request): request.make_tile_request() if request.layer not in self.layers: raise RequestError('unknown layer: ' + str(request.layer), code='InvalidParameterValue', request=request) if request.tilematrixset not in self.layers[request.layer]: raise RequestError('unknown tilematrixset: ' + str(request.tilematrixset), code='InvalidParameterValue', request=request) def check_request_dimensions(self, tile_layer, request): # allow arbitrary dimensions in KVP service # actual used values are checked later in TileLayer pass def _service_md(self, tile_request): md = dict(self.md) md['url'] = tile_request.url return md class WMTSRestServer(WMTSServer): """ OGC WMTS 1.0.0 RESTful Server """ service = None names = ('wmts',) request_methods = ('tile', 'capabilities') default_template = '/{Layer}/{TileMatrixSet}/{TileMatrix}/{TileCol}/{TileRow}.{Format}' def __init__(self, layers, md, max_tile_age=None, template=None): WMTSServer.__init__(self, layers, md) self.max_tile_age = max_tile_age self.template = template or self.default_template self.url_converter = URLTemplateConverter(self.template) self.request_parser = make_wmts_rest_request_parser(self.url_converter) self.capabilities_class = partial(RestfulCapabilities, url_converter=self.url_converter) def check_request_dimensions(self, tile_layer, request): # check that unknown dimension for this layer are set to default if request.dimensions: for dimension, value in iteritems(request.dimensions): dimension = dimension.lower() if dimension not in tile_layer.dimensions and value != 'default': raise RequestError('unknown dimension: ' + str(dimension), code='InvalidParameterValue', request=request) class Capabilities(object): """ Renders WMS capabilities documents. """ def __init__(self, server_md, layers, matrix_sets): self.service = server_md self.layers = layers self.matrix_sets = matrix_sets def render(self, _map_request): return self._render_template(_map_request.capabilities_template) def template_context(self): return dict(service=bunch(default='', **self.service), restful=False, layers=self.layers, tile_matrix_sets=self.matrix_sets) def _render_template(self, template): template = get_template(template) doc = template.substitute(**self.template_context()) # strip blank lines doc = '\n'.join(l for l in doc.split('\n') if l.rstrip()) return doc class RestfulCapabilities(Capabilities): def __init__(self, server_md, layers, matrix_sets, url_converter): Capabilities.__init__(self, server_md, layers, matrix_sets) self.url_converter = url_converter def template_context(self): return dict(service=bunch(default='', **self.service), restful=True, layers=self.layers, tile_matrix_sets=self.matrix_sets, resource_template=self.url_converter.template, # dimension_key maps lowercase dimensions to the actual # casing from the restful template dimension_keys=dict((k.lower(), k) for k in self.url_converter.dimensions), format_resource_template=format_resource_template, ) def format_resource_template(layer, template, service): # TODO: remove {{Format}} in 1.6 if '{{Format}}' in template: template = template.replace('{{Format}}', layer.format) if '{Format}' in template: template = template.replace('{Format}', layer.format) if '{Layer}' in template: template = template.replace('{Layer}', layer.name) return service.url + template class WMTSTileLayer(object): """ Wrap multiple TileLayers for the same cache but with different grids. """ def __init__(self, layers): self.grids = [lyr.grid for lyr in layers.values()] self.layers = layers self._layer = layers[next(iterkeys(layers))] def __getattr__(self, name): return getattr(self._layer, name) def __contains__(self, gridname): return gridname in self.layers def __getitem__(self, gridname): return self.layers[gridname] from mapproxy.grid import tile_grid # calculated from well-known scale set GoogleCRS84Quad METERS_PER_DEEGREE = 111319.4907932736 def meter_per_unit(srs): if srs.is_latlong: return METERS_PER_DEEGREE return 1 class TileMatrixSet(object): def __init__(self, grid): self.grid = grid self.name = grid.name self.srs_name = grid.srs.srs_code self.tile_matrices = list(self._tile_matrices()) def __iter__(self): return iter(self.tile_matrices) def _tile_matrices(self): for level, res in self.grid.resolutions.iteritems(): origin = self.grid.origin_tile(level, 'ul') bbox = self.grid.tile_bbox(origin) topleft = bbox[0], bbox[3] if self.grid.srs.is_axis_order_ne: topleft = bbox[3], bbox[0] grid_size = self.grid.grid_sizes[level] scale_denom = res / (0.28 / 1000) * meter_per_unit(self.grid.srs) yield bunch( identifier=level, topleft=topleft, grid_size=grid_size, scale_denom=scale_denom, tile_size=self.grid.tile_size, ) if __name__ == '__main__': print(TileMatrixSet(tile_grid(900913)).tile_matrixes()) print(TileMatrixSet(tile_grid(4326, origin='ul')).tile_matrixes()) mapproxy-1.11.0/mapproxy/source/000077500000000000000000000000001320454472400166355ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/source/__init__.py000066400000000000000000000044271320454472400207550ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Map/information sources for layers or tile cache. """ from mapproxy.layer import ( MapLayer, MapExtent, DefaultMapExtent, MapError, MapBBOXError, BlankImage, InfoLayer ) from mapproxy.image.message import message_image from mapproxy.image.opts import ImageOptions from mapproxy.srs import SRS class SourceError(MapError): pass class SourceBBOXError(SourceError, MapBBOXError): pass class InvalidSourceQuery(SourceError): pass class InfoSource(InfoLayer): def get_info(self, query): raise NotImplementedError class LegendSource(object): def get_legend(self, query): raise NotImplementedError class DebugSource(MapLayer): def __init__(self): MapLayer.__init__(self) self.extent = DefaultMapExtent() self.res_range = None def get_map(self, query): bbox = query.bbox w = bbox[2] - bbox[0] h = bbox[3] - bbox[1] res_x = w/query.size[0] res_y = h/query.size[1] debug_info = "bbox: %r\nres: %.8f(%.8f)" % (bbox, res_x, res_y) return message_image(debug_info, size=query.size, image_opts=ImageOptions(transparent=True)) class DummySource(MapLayer): supports_meta_tiles = True """ Source that always returns a blank image. Used internally for 'offline' sources (e.g. seed_only). """ def __init__(self, coverage=None): MapLayer.__init__(self) self.image_opts.transparent = True self.extent = MapExtent((-180, -90, 180, 90), SRS(4326)) self.extent = MapExtent(coverage.bbox, coverage.srs) if coverage else DefaultMapExtent() def get_map(self, query): raise BlankImage() mapproxy-1.11.0/mapproxy/source/arcgis.py000066400000000000000000000025521320454472400204630ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.source.wms import WMSSource, WMSInfoSource import logging log = logging.getLogger('mapproxy.source.arcgis') class ArcGISSource(WMSSource): def __init__(self, client, image_opts=None, coverage=None, res_range=None, supported_srs=None, supported_formats=None): WMSSource.__init__(self, client, image_opts=image_opts, coverage=coverage, res_range=res_range, supported_srs=supported_srs, supported_formats=supported_formats) class ArcGISInfoSource(WMSInfoSource): def __init__(self, client): self.client = client def get_info(self, query): doc = self.client.get_info(query) return docmapproxy-1.11.0/mapproxy/source/error.py000066400000000000000000000026471320454472400203510ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2012 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.image.opts import ImageOptions from mapproxy.image import BlankImageSource class HTTPSourceErrorHandler(object): def __init__(self): self.response_error_codes = {} def add_handler(self, http_code, color, cacheable=False): self.response_error_codes[http_code] = (color, cacheable) def handle(self, status_code, query): color = cacheable = None if status_code in self.response_error_codes: color, cacheable = self.response_error_codes[status_code] elif 'other' in self.response_error_codes: color, cacheable = self.response_error_codes['other'] else: return None transparent = len(color) == 4 image_opts = ImageOptions(bgcolor=color, transparent=transparent) img_source = BlankImageSource(query.size, image_opts, cacheable=cacheable) return img_sourcemapproxy-1.11.0/mapproxy/source/mapnik.py000066400000000000000000000125341320454472400204730ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import import sys import time import threading from mapproxy.grid import tile_grid from mapproxy.image import ImageSource from mapproxy.image.opts import ImageOptions from mapproxy.layer import MapExtent, DefaultMapExtent, BlankImage, MapLayer from mapproxy.source import SourceError from mapproxy.client.log import log_request from mapproxy.util.py import reraise_exception from mapproxy.util.async import run_non_blocking from mapproxy.compat import BytesIO try: import mapnik mapnik except ImportError: try: # for 2.0 alpha/rcs and first 2.0 release import mapnik2 as mapnik except ImportError: mapnik = None # fake 2.0 API for older versions if mapnik and not hasattr(mapnik, 'Box2d'): mapnik.Box2d = mapnik.Envelope import logging log = logging.getLogger(__name__) class MapnikSource(MapLayer): supports_meta_tiles = True def __init__(self, mapfile, layers=None, image_opts=None, coverage=None, res_range=None, lock=None, reuse_map_objects=False, scale_factor=None): MapLayer.__init__(self, image_opts=image_opts) self.mapfile = mapfile self.coverage = coverage self.res_range = res_range self.layers = set(layers) if layers else None self.scale_factor = scale_factor self.lock = lock self._map_objs = {} self._map_objs_lock = threading.Lock() self._cache_map_obj = reuse_map_objects if self.coverage: self.extent = MapExtent(self.coverage.bbox, self.coverage.srs) else: self.extent = DefaultMapExtent() def get_map(self, query): if self.res_range and not self.res_range.contains(query.bbox, query.size, query.srs): raise BlankImage() if self.coverage and not self.coverage.intersects(query.bbox, query.srs): raise BlankImage() try: resp = self.render(query) except RuntimeError as ex: log.error('could not render Mapnik map: %s', ex) reraise_exception(SourceError(ex.args[0]), sys.exc_info()) resp.opacity = self.opacity return resp def render(self, query): mapfile = self.mapfile if '%(webmercator_level)' in mapfile: _bbox, level = tile_grid(3857).get_affected_bbox_and_level( query.bbox, query.size, req_srs=query.srs) mapfile = mapfile % {'webmercator_level': level} if self.lock: with self.lock(): return self.render_mapfile(mapfile, query) else: return self.render_mapfile(mapfile, query) def map_obj(self, mapfile): if not self._cache_map_obj: m = mapnik.Map(0, 0) mapnik.load_map(m, str(mapfile)) return m # cache loaded map objects # only works when a single proc/thread accesses this object # (forking the render process doesn't work because of open database # file handles that gets passed to the child) if mapfile not in self._map_objs: with self._map_objs_lock: if mapfile not in self._map_objs: m = mapnik.Map(0, 0) mapnik.load_map(m, str(mapfile)) self._map_objs[mapfile] = m return self._map_objs[mapfile] def render_mapfile(self, mapfile, query): return run_non_blocking(self._render_mapfile, (mapfile, query)) def _render_mapfile(self, mapfile, query): start_time = time.time() m = self.map_obj(mapfile) m.resize(query.size[0], query.size[1]) m.srs = '+init=%s' % str(query.srs.srs_code.lower()) envelope = mapnik.Box2d(*query.bbox) m.zoom_to_box(envelope) data = None try: if self.layers: i = 0 for layer in m.layers[:]: if layer.name != 'Unkown' and layer.name not in self.layers: del m.layers[i] else: i += 1 img = mapnik.Image(query.size[0], query.size[1]) if self.scale_factor: mapnik.render(m, img, self.scale_factor) else: mapnik.render(m, img) data = img.tostring(str(query.format)) finally: size = None if data: size = len(data) log_request('%s:%s:%s:%s' % (mapfile, query.bbox, query.srs.srs_code, query.size), status='200' if data else '500', size=size, method='API', duration=time.time()-start_time) return ImageSource(BytesIO(data), size=query.size, image_opts=ImageOptions(format=query.format)) mapproxy-1.11.0/mapproxy/source/tile.py000066400000000000000000000071231320454472400201470ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Retrieve tiles from different tile servers (TMS/TileCache/etc.). """ import sys from mapproxy.image.opts import ImageOptions from mapproxy.source import SourceError from mapproxy.client.http import HTTPClientError from mapproxy.source import InvalidSourceQuery from mapproxy.layer import BlankImage, map_extent_from_grid, CacheMapLayer, MapLayer from mapproxy.util.py import reraise_exception import logging log = logging.getLogger('mapproxy.source.tile') log_config = logging.getLogger('mapproxy.config') class TiledSource(MapLayer): def __init__(self, grid, client, coverage=None, image_opts=None, error_handler=None, res_range=None): MapLayer.__init__(self, image_opts=image_opts) self.grid = grid self.client = client self.image_opts = image_opts or ImageOptions() self.coverage = coverage self.extent = coverage.extent if coverage else map_extent_from_grid(grid) self.res_range = res_range self.error_handler = error_handler def get_map(self, query): if self.grid.tile_size != query.size: ex = InvalidSourceQuery( 'tile size of cache and tile source do not match: %s != %s' % (self.grid.tile_size, query.size) ) log_config.error(ex) raise ex if self.grid.srs != query.srs: ex = InvalidSourceQuery( 'SRS of cache and tile source do not match: %r != %r' % (self.grid.srs, query.srs) ) log_config.error(ex) raise ex if self.res_range and not self.res_range.contains(query.bbox, query.size, query.srs): raise BlankImage() if self.coverage and not self.coverage.intersects(query.bbox, query.srs): raise BlankImage() _bbox, grid, tiles = self.grid.get_affected_tiles(query.bbox, query.size) if grid != (1, 1): raise InvalidSourceQuery('BBOX does not align to tile') tile_coord = next(tiles) try: return self.client.get_tile(tile_coord, format=query.format) except HTTPClientError as e: if self.error_handler: resp = self.error_handler.handle(e.response_code, query) if resp: return resp log.warn('could not retrieve tile: %s', e) reraise_exception(SourceError(e.args[0]), sys.exc_info()) class CacheSource(CacheMapLayer): def __init__(self, tile_manager, extent=None, image_opts=None, max_tile_limit=None, tiled_only=False): CacheMapLayer.__init__(self, tile_manager, extent=extent, image_opts=image_opts, max_tile_limit=max_tile_limit) self.supports_meta_tiles = not tiled_only self.tiled_only = tiled_only def get_map(self, query): if self.tiled_only: query.tiled_only = True return CacheMapLayer.get_map(self, query) mapproxy-1.11.0/mapproxy/source/wms.py000066400000000000000000000236621320454472400200260ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Retrieve maps/information from WMS servers. """ import sys from mapproxy.request.base import split_mime_type from mapproxy.cache.legend import Legend, legend_identifier from mapproxy.image import make_transparent, ImageSource, SubImageSource, bbox_position_in_image from mapproxy.image.merge import concat_legends from mapproxy.image.transform import ImageTransformer from mapproxy.layer import MapExtent, DefaultMapExtent, BlankImage, LegendQuery, MapQuery, MapLayer from mapproxy.source import InfoSource, SourceError, LegendSource from mapproxy.client.http import HTTPClientError from mapproxy.util.py import reraise_exception import logging log = logging.getLogger('mapproxy.source.wms') class WMSSource(MapLayer): supports_meta_tiles = True def __init__(self, client, image_opts=None, coverage=None, res_range=None, transparent_color=None, transparent_color_tolerance=None, supported_srs=None, supported_formats=None, fwd_req_params=None): MapLayer.__init__(self, image_opts=image_opts) self.client = client self.supported_srs = supported_srs or [] self.supported_formats = supported_formats or [] self.fwd_req_params = fwd_req_params or set() self.transparent_color = transparent_color self.transparent_color_tolerance = transparent_color_tolerance if self.transparent_color: self.image_opts.transparent = True self.coverage = coverage self.res_range = res_range if self.coverage: self.extent = MapExtent(self.coverage.bbox, self.coverage.srs) else: self.extent = DefaultMapExtent() def is_opaque(self, query): """ Returns true if we are sure that the image is not transparent. """ if self.res_range and not self.res_range.contains(query.bbox, query.size, query.srs): return False if self.image_opts.transparent: return False if self.opacity is not None and (0.0 < self.opacity < 0.99): return False if not self.coverage: # not transparent and no coverage return True if self.coverage.contains(query.bbox, query.srs): # not transparent and completely inside coverage return True return False def get_map(self, query): if self.res_range and not self.res_range.contains(query.bbox, query.size, query.srs): raise BlankImage() if self.coverage and not self.coverage.intersects(query.bbox, query.srs): raise BlankImage() try: resp = self._get_map(query) if self.transparent_color: resp = make_transparent(resp, self.transparent_color, self.transparent_color_tolerance) resp.opacity = self.opacity return resp except HTTPClientError as e: log.warn('could not retrieve WMS map: %s', e) reraise_exception(SourceError(e.args[0]), sys.exc_info()) def _get_map(self, query): format = self.image_opts.format if not format: format = query.format if self.supported_formats and format not in self.supported_formats: format = self.supported_formats[0] if self.supported_srs: if query.srs not in self.supported_srs: return self._get_transformed(query, format) # some srs are equal but not the same (e.g. 900913/3857) # use only supported srs so we use the right srs code. idx = self.supported_srs.index(query.srs) if self.supported_srs[idx] is not query.srs: query.srs = self.supported_srs[idx] if self.extent and not self.extent.contains(MapExtent(query.bbox, query.srs)): return self._get_sub_query(query, format) resp = self.client.retrieve(query, format) return ImageSource(resp, size=query.size, image_opts=self.image_opts) def _get_sub_query(self, query, format): size, offset, bbox = bbox_position_in_image(query.bbox, query.size, self.extent.bbox_for(query.srs)) if size[0] == 0 or size[1] == 0: raise BlankImage() src_query = MapQuery(bbox, size, query.srs, format, dimensions=query.dimensions) resp = self.client.retrieve(src_query, format) return SubImageSource(resp, size=query.size, offset=offset, image_opts=self.image_opts) def _get_transformed(self, query, format): dst_srs = query.srs src_srs = self._best_supported_srs(dst_srs) dst_bbox = query.bbox src_bbox = dst_srs.transform_bbox_to(src_srs, dst_bbox) src_width, src_height = src_bbox[2]-src_bbox[0], src_bbox[3]-src_bbox[1] ratio = src_width/src_height dst_size = query.size xres, yres = src_width/dst_size[0], src_height/dst_size[1] if xres < yres: src_size = dst_size[0], int(dst_size[0]/ratio + 0.5) else: src_size = int(dst_size[1]*ratio +0.5), dst_size[1] src_query = MapQuery(src_bbox, src_size, src_srs, format, dimensions=query.dimensions) if self.coverage and not self.coverage.contains(src_bbox, src_srs): img = self._get_sub_query(src_query, format) else: resp = self.client.retrieve(src_query, format) img = ImageSource(resp, size=src_size, image_opts=self.image_opts) img = ImageTransformer(src_srs, dst_srs).transform(img, src_bbox, query.size, dst_bbox, self.image_opts) img.format = format return img def _best_supported_srs(self, srs): latlong = srs.is_latlong for srs in self.supported_srs: if srs.is_latlong == latlong: return srs # else return self.supported_srs[0] def _is_compatible(self, other, query): if not isinstance(other, WMSSource): return False if self.opacity is not None or other.opacity is not None: return False if self.supported_srs != other.supported_srs: return False if self.supported_formats != other.supported_formats: return False if self.transparent_color != other.transparent_color: return False if self.transparent_color_tolerance != other.transparent_color_tolerance: return False if self.coverage != other.coverage: return False if (query.dimensions_for_params(self.fwd_req_params) != query.dimensions_for_params(other.fwd_req_params)): return False return True def combined_layer(self, other, query): if not self._is_compatible(other, query): return None client = self.client.combined_client(other.client, query) if not client: return None return WMSSource(client, image_opts=self.image_opts, transparent_color=self.transparent_color, transparent_color_tolerance=self.transparent_color_tolerance, supported_srs=self.supported_srs, supported_formats=self.supported_formats, res_range=None, # layer outside res_range should already be filtered out coverage=self.coverage, fwd_req_params=self.fwd_req_params, ) class WMSInfoSource(InfoSource): def __init__(self, client, fi_transformer=None): self.client = client self.fi_transformer = fi_transformer def get_info(self, query): doc = self.client.get_info(query) if self.fi_transformer: doc = self.fi_transformer(doc) return doc class WMSLegendSource(LegendSource): def __init__(self, clients, legend_cache, static=False): self.clients = clients self.identifier = legend_identifier([c.identifier for c in self.clients]) self._cache = legend_cache self._size = None self.static = static @property def size(self): if not self._size: legend = self.get_legend(LegendQuery(format='image/png', scale=None)) # TODO image size without as_image? self._size = legend.as_image().size return self._size def get_legend(self, query): if self.static: # prevent caching of static legends for different scales legend = Legend(id=self.identifier, scale=None) else: legend = Legend(id=self.identifier, scale=query.scale) if not self._cache.load(legend): legends = [] error_occured = False for client in self.clients: try: legends.append(client.get_legend(query)) except HTTPClientError as e: error_occured = True log.error(e.args[0]) except SourceError as e: error_occured = True # TODO errors? log.error(e.args[0]) format = split_mime_type(query.format)[1] legend = Legend(source=concat_legends(legends, format=format), id=self.identifier, scale=query.scale) if not error_occured: self._cache.store(legend) return legend.source mapproxy-1.11.0/mapproxy/srs.py000066400000000000000000000325151320454472400165240ustar00rootroot00000000000000# -*- coding: utf-8 -*- # This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Spatial reference systems and transformation of coordinates. """ from __future__ import division import math import threading from mapproxy.compat.itertools import izip from mapproxy.compat import string_type from mapproxy.proj import Proj, transform, set_datapath from mapproxy.config import base_config import logging log_system = logging.getLogger('mapproxy.system') log_proj = logging.getLogger('mapproxy.proj') def get_epsg_num(epsg_code): """ >>> get_epsg_num('ePsG:4326') 4326 >>> get_epsg_num(4313) 4313 >>> get_epsg_num('31466') 31466 """ if isinstance(epsg_code, string_type): if ':' in epsg_code: epsg_code = int(epsg_code.split(':')[1]) else: epsg_code = int(epsg_code) return epsg_code def _clean_srs_code(code): """ >>> _clean_srs_code(4326) 'EPSG:4326' >>> _clean_srs_code('31466') 'EPSG:31466' >>> _clean_srs_code('crs:84') 'CRS:84' """ if isinstance(code, string_type) and ':' in code: return code.upper() else: return 'EPSG:' + str(code) class TransformationError(Exception): pass _proj_initalized = False def _init_proj(): global _proj_initalized if not _proj_initalized and 'proj_data_dir' in base_config().srs: proj_data_dir = base_config().srs['proj_data_dir'] log_system.info('loading proj data from %s', proj_data_dir) set_datapath(proj_data_dir) _proj_initalized = True _thread_local = threading.local() def SRS(srs_code): _init_proj() if isinstance(srs_code, _SRS): return srs_code srs_code = _clean_srs_code(srs_code) if not hasattr(_thread_local, 'srs_cache'): _thread_local.srs_cache = {} if srs_code in _thread_local.srs_cache: return _thread_local.srs_cache[srs_code] else: srs = _SRS(srs_code) _thread_local.srs_cache[srs_code] = srs return srs WEBMERCATOR_EPSG = set(('EPSG:900913', 'EPSG:3857', 'EPSG:102100', 'EPSG:102113')) class _SRS(object): # http://trac.openlayers.org/wiki/SphericalMercator proj_init = { 'EPSG:4326': lambda: Proj('+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs +over'), 'CRS:84': lambda: Proj('+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs +over'), } for _epsg in WEBMERCATOR_EPSG: proj_init[_epsg] = lambda: Proj( '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 ' '+lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m ' '+nadgrids=@null +no_defs +over') """ This class represents a Spatial Reference System. """ def __init__(self, srs_code): """ Create a new SRS with the given `srs_code` code. """ self.srs_code = srs_code init = _SRS.proj_init.get(srs_code, None) if init is not None: self.proj = init() else: epsg_num = get_epsg_num(srs_code) self.proj = Proj(init='epsg:%d' % epsg_num) def transform_to(self, other_srs, points): """ :type points: ``(x, y)`` or ``[(x1, y1), (x2, y2), …]`` >>> srs1 = SRS(4326) >>> srs2 = SRS(900913) >>> [str(round(x, 5)) for x in srs1.transform_to(srs2, (8.22, 53.15))] ['915046.21432', '7010792.20171'] >>> srs1.transform_to(srs1, (8.25, 53.5)) (8.25, 53.5) >>> [(str(round(x, 5)), str(round(y, 5))) for x, y in ... srs1.transform_to(srs2, [(8.2, 53.1), (8.22, 53.15), (8.3, 53.2)])] ... #doctest: +NORMALIZE_WHITESPACE [('912819.8245', '7001516.67745'), ('915046.21432', '7010792.20171'), ('923951.77358', '7020078.53264')] """ if self == other_srs: return points if isinstance(points[0], (int, float)) and 2 >= len(points) <= 3: return transform(self.proj, other_srs.proj, *points) x = [p[0] for p in points] y = [p[1] for p in points] transf_pts = transform(self.proj, other_srs.proj, x, y) return izip(transf_pts[0], transf_pts[1]) def transform_bbox_to(self, other_srs, bbox, with_points=16): """ :param with_points: the number of points to use for the transformation. A bbox transformation with only two or four points may cut off some parts due to distortions. >>> ['%.3f' % x for x in ... SRS(4326).transform_bbox_to(SRS(900913), (-180.0, -90.0, 180.0, 90.0))] ['-20037508.343', '-147730762.670', '20037508.343', '147730758.195'] >>> ['%.5f' % x for x in ... SRS(4326).transform_bbox_to(SRS(900913), (8.2, 53.1, 8.3, 53.2))] ['912819.82450', '7001516.67745', '923951.77358', '7020078.53264'] >>> SRS(4326).transform_bbox_to(SRS(4326), (8.25, 53.0, 8.5, 53.75)) (8.25, 53.0, 8.5, 53.75) """ if self == other_srs: return bbox bbox = self.align_bbox(bbox) points = generate_envelope_points(bbox, with_points) transf_pts = self.transform_to(other_srs, points) result = calculate_bbox(transf_pts) log_proj.debug('transformed from %r to %r (%s -> %s)' % (self, other_srs, bbox, result)) return result def align_bbox(self, bbox): """ Align bbox to reasonable values to prevent errors in transformations. E.g. transformations from EPSG:4326 with lat=90 or -90 will fail, so we subtract a tiny delta. At the moment only EPSG:4326 bbox will be modifyed. >>> bbox = SRS(4326).align_bbox((-180, -90, 180, 90)) >>> -90 < bbox[1] < -89.99999998 True >>> 90 > bbox[3] > 89.99999998 True """ # TODO should not be needed anymore since we transform with +over # still a few tests depend on the rounding behavior of this if self.srs_code == 'EPSG:4326': delta = 0.00000001 (minx, miny, maxx, maxy) = bbox if abs(miny - -90.0) < 1e-6: miny = -90.0 + delta if abs(maxy - 90.0) < 1e-6: maxy = 90.0 - delta bbox = minx, miny, maxx, maxy return bbox @property def is_latlong(self): """ >>> SRS(4326).is_latlong True >>> SRS(31466).is_latlong False """ return self.proj.is_latlong() @property def is_axis_order_ne(self): """ Returns `True` if the axis order is North, then East (i.e. y/x or lat/lon). >>> SRS(4326).is_axis_order_ne True >>> SRS('CRS:84').is_axis_order_ne False >>> SRS(31468).is_axis_order_ne True >>> SRS(31463).is_axis_order_ne False >>> SRS(25831).is_axis_order_ne False """ if self.srs_code in base_config().srs.axis_order_ne: return True if self.srs_code in base_config().srs.axis_order_en: return False if self.is_latlong: return True return False @property def is_axis_order_en(self): """ Returns `True` if the axis order is East then North (i.e. x/y or lon/lat). """ return not self.is_axis_order_ne def __eq__(self, other): """ >>> SRS(4326) == SRS("EpsG:4326") True >>> SRS(4326) == SRS("4326") True >>> SRS(4326) == SRS(900913) False >>> SRS(3857) == SRS(900913) True >>> SRS(900913) == SRS(3857) True """ if isinstance(other, _SRS): if (self.srs_code in WEBMERCATOR_EPSG and other.srs_code in WEBMERCATOR_EPSG): return True return self.proj.srs == other.proj.srs else: return NotImplemented def __ne__(self, other): """ >>> SRS(900913) != SRS(900913) False >>> SRS(4326) != SRS(900913) True """ equal_result = self.__eq__(other) if equal_result is NotImplemented: return NotImplemented else: return not equal_result def __str__(self): #pylint: disable-msg=E1101 return "SRS %s ('%s')" % (self.srs_code, self.proj.srs) def __repr__(self): """ >>> repr(SRS(4326)) "SRS('EPSG:4326')" """ return "SRS('%s')" % (self.srs_code,) def __hash__(self): return hash(self.srs_code) def generate_envelope_points(bbox, n): """ Generates points that form a linestring around a given bbox. @param bbox: bbox to generate linestring for @param n: the number of points to generate around the bbox >>> generate_envelope_points((10.0, 5.0, 20.0, 15.0), 4) [(10.0, 5.0), (20.0, 5.0), (20.0, 15.0), (10.0, 15.0)] >>> generate_envelope_points((10.0, 5.0, 20.0, 15.0), 8) ... #doctest: +NORMALIZE_WHITESPACE [(10.0, 5.0), (15.0, 5.0), (20.0, 5.0), (20.0, 10.0),\ (20.0, 15.0), (15.0, 15.0), (10.0, 15.0), (10.0, 10.0)] """ (minx, miny, maxx, maxy) = bbox if n <= 4: n = 0 else: n = int(math.ceil((n - 4) / 4.0)) width = maxx - minx height = maxy - miny minx, maxx = min(minx, maxx), max(minx, maxx) miny, maxy = min(miny, maxy), max(miny, maxy) n += 1 xstep = width / n ystep = height / n result = [] for i in range(n+1): result.append((minx + i*xstep, miny)) for i in range(1, n): result.append((maxx, miny + i*ystep)) for i in range(n, -1, -1): result.append((minx + i*xstep, maxy)) for i in range(n-1, 0, -1): result.append((minx, miny + i*ystep)) return result def calculate_bbox(points): """ Calculates the bbox of a list of points. >>> calculate_bbox([(-5, 20), (3, 8), (99, 0)]) (-5, 0, 99, 20) @param points: list of points [(x0, y0), (x1, y2), ...] @returns: bbox of the input points. """ points = list(points) # points can be INF for invalid transformations, filter out try: minx = min(p[0] for p in points if p[0] != float('inf')) miny = min(p[1] for p in points if p[1] != float('inf')) maxx = max(p[0] for p in points if p[0] != float('inf')) maxy = max(p[1] for p in points if p[1] != float('inf')) return (minx, miny, maxx, maxy) except ValueError: # min/max are called with empty list when everything is inf raise TransformationError() def merge_bbox(bbox1, bbox2): """ Merge two bboxes. >>> merge_bbox((-10, 20, 0, 30), (30, -20, 90, 10)) (-10, -20, 90, 30) """ minx = min(bbox1[0], bbox2[0]) miny = min(bbox1[1], bbox2[1]) maxx = max(bbox1[2], bbox2[2]) maxy = max(bbox1[3], bbox2[3]) return (minx, miny, maxx, maxy) def bbox_equals(src_bbox, dst_bbox, x_delta=None, y_delta=None): """ Compares two bbox and checks if they are equal, or nearly equal. :param x_delta: how precise the comparison should be. should be reasonable small, like a tenth of a pixel. defaults to 1/1.000.000th of the width. :type x_delta: bbox units >>> src_bbox = (939258.20356824622, 6887893.4928338043, ... 1095801.2374962866, 7044436.5267618448) >>> dst_bbox = (939258.20260000182, 6887893.4908000007, ... 1095801.2365000017, 7044436.5247000009) >>> bbox_equals(src_bbox, dst_bbox, 61.1, 61.1) True >>> bbox_equals(src_bbox, dst_bbox, 0.0001) False """ if x_delta is None: x_delta = abs(src_bbox[0] - src_bbox[2]) / 1000000.0 if y_delta is None: y_delta = x_delta return (abs(src_bbox[0] - dst_bbox[0]) < x_delta and abs(src_bbox[1] - dst_bbox[1]) < x_delta and abs(src_bbox[2] - dst_bbox[2]) < y_delta and abs(src_bbox[3] - dst_bbox[3]) < y_delta) def make_lin_transf(src_bbox, dst_bbox): """ Create a transformation function that transforms linear between two plane coordinate systems. One needs to be cartesian (0, 0 at the lower left, x goes up) and one needs to be an image coordinate system (0, 0 at the top left, x goes down). :return: function that takes src x/y and returns dest x/y coordinates >>> transf = make_lin_transf((7, 50, 8, 51), (0, 0, 500, 400)) >>> transf((7.5, 50.5)) (250.0, 200.0) >>> transf((7.0, 50.0)) (0.0, 400.0) >>> transf = make_lin_transf((7, 50, 8, 51), (200, 300, 700, 700)) >>> transf((7.5, 50.5)) (450.0, 500.0) """ func = lambda x_y: (dst_bbox[0] + (x_y[0] - src_bbox[0]) * (dst_bbox[2]-dst_bbox[0]) / (src_bbox[2] - src_bbox[0]), dst_bbox[1] + (src_bbox[3] - x_y[1]) * (dst_bbox[3]-dst_bbox[1]) / (src_bbox[3] - src_bbox[1])) return func mapproxy-1.11.0/mapproxy/template.py000066400000000000000000000036121320454472400175240ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Loading of template files (e.g. capability documents) """ import os import pkg_resources from mapproxy.util.ext.tempita import Template, bunch from mapproxy.config.config import base_config __all__ = ['Template', 'bunch', 'template_loader'] def template_loader(module_name, location='templates', namespace={}): class loader(object): def __call__(self, name, from_template=None, default_inherit=None): if base_config().template_dir: template_file = os.path.join(base_config().template_dir, name) else: template_file = pkg_resources.resource_filename(module_name, location + '/' + name) return Template.from_filename(template_file, namespace=namespace, encoding='utf-8', default_inherit=default_inherit, get_template=self) return loader() class recursive_bunch(bunch): def __getitem__(self, key): if 'default' in self: try: value = dict.__getitem__(self, key) except KeyError: value = dict.__getitem__(self, 'default') else: value = dict.__getitem__(self, key) if isinstance(value, dict): value = recursive_bunch(**value) return value mapproxy-1.11.0/mapproxy/test/000077500000000000000000000000001320454472400163145ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/__init__.py000066400000000000000000000000001320454472400204130ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/helper.py000066400000000000000000000160151320454472400201500ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function import tempfile import os import re import sys from contextlib import contextmanager from lxml import etree from mapproxy.test import mocker from mapproxy.compat import string_type, PY2 from nose.tools import eq_ class Mocker(object): """ This is a base class for unit-tests that use ``mocker``. This class follows the nosetest naming conventions for setup and teardown methods. `setup` will initialize a `mocker.Mocker`. The `teardown` method will run ``mocker.verify()``. """ def setup(self): self.mocker = mocker.Mocker() def expect_and_return(self, mock_call, return_val): """ Register a return value for the mock call. :param return_val: The value mock_call should return. """ self.mocker.result(return_val) def expect(self, mock_call): return mocker.expect(mock_call) def replay(self): """ Finish mock-record phase. """ self.mocker.replay() def mock(self, base_cls=None): """ Return a new mock object. :param base_cls: check method signatures of the mock-calls with this base_cls signature (optional) """ if base_cls: return self.mocker.mock(base_cls) return self.mocker.mock() def teardown(self): self.mocker.verify() class TempFiles(object): """ This class is a context manager for temporary files. >>> with TempFiles(n=2, suffix='.png') as tmp: ... for f in tmp: ... assert os.path.exists(f) >>> for f in tmp: ... assert not os.path.exists(f) """ def __init__(self, n=1, suffix='', no_create=False): self.n = n self.suffix = suffix self.no_create = no_create self.tmp_files = [] def __enter__(self): for _ in range(self.n): fd, tmp_file = tempfile.mkstemp(suffix=self.suffix) os.close(fd) self.tmp_files.append(tmp_file) if self.no_create: os.remove(tmp_file) return self.tmp_files def __exit__(self, exc_type, exc_val, exc_tb): for tmp_file in self.tmp_files: if os.path.exists(tmp_file): os.remove(tmp_file) self.tmp_files = [] class TempFile(TempFiles): def __init__(self, suffix='', no_create=False): TempFiles.__init__(self, suffix=suffix, no_create=no_create) def __enter__(self): return TempFiles.__enter__(self)[0] class LogMock(object): log_methods = ('info', 'debug', 'warn', 'error', 'fail') def __init__(self, module, log_name='log'): self.module = module self.orig_logger = None self.logged_msgs = [] def __enter__(self): self.orig_logger = self.module.log self.module.log = self return self def __getattr__(self, name): if name in self.log_methods: def _log(msg): self.logged_msgs.append((name, msg)) return _log raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name)) def assert_log(self, type, msg): log_type, log_msg = self.logged_msgs.pop(0) assert log_type == type, 'expected %s log message, but was %s' % (type, log_type) assert msg in log_msg.lower(), "expected string '%s' in log message '%s'" % \ (msg, log_msg) def __exit__(self, exc_type, exc_val, exc_tb): self.module.log = self.orig_logger def assert_re(value, regex): """ >>> assert_re('hello', 'l+') >>> assert_re('hello', 'l{3}') Traceback (most recent call last): ... AssertionError: hello ~= l{3} """ match = re.search(regex, value) assert match is not None, '%s ~= %s' % (value, regex) def validate_with_dtd(doc, dtd_name, dtd_basedir=None): if dtd_basedir is None: dtd_basedir = os.path.join(os.path.dirname(__file__), 'schemas') dtd_filename = os.path.join(dtd_basedir, dtd_name) with open(dtd_filename, 'rb') as schema: dtd = etree.DTD(schema) if isinstance(doc, (string_type, bytes)): xml = etree.XML(doc) else: xml = doc is_valid = dtd.validate(xml) print(dtd.error_log.filter_from_errors()) return is_valid def validate_with_xsd(doc, xsd_name, xsd_basedir=None): if xsd_basedir is None: xsd_basedir = os.path.join(os.path.dirname(__file__), 'schemas') xsd_filename = os.path.join(xsd_basedir, xsd_name) with open(xsd_filename, 'rb') as schema: xsd = etree.parse(schema) xml_schema = etree.XMLSchema(xsd) if isinstance(doc, (string_type, bytes)): xml = etree.XML(doc) else: xml = doc is_valid = xml_schema.validate(xml) print(xml_schema.error_log.filter_from_errors()) return is_valid class XPathValidator(object): def __init__(self, doc): self.xml = etree.XML(doc) def assert_xpath(self, xpath, expected=None): assert len(self.xml.xpath(xpath)) > 0, xpath + ' does not match anything' if expected is not None: if callable(expected): assert expected(self.xml.xpath(xpath)[0]) else: eq_(self.xml.xpath(xpath)[0], expected) def xpath(self, xpath): return self.xml.xpath(xpath) def strip_whitespace(data): """ >>> strip_whitespace(' bar\\n zing\\t1') 'barzing1' """ if isinstance(data, bytes): return re.sub(b'\s+', b'', data) else: return re.sub('\s+', '', data) @contextmanager def capture(bytes=False): if PY2: from StringIO import StringIO else: if bytes: from io import BytesIO as StringIO else: from io import StringIO backup_stdout = sys.stdout backup_stderr = sys.stderr try: sys.stdout = StringIO() sys.stderr = StringIO() yield sys.stdout, sys.stderr except Exception as ex: backup_stdout.write(str(ex)) if bytes: backup_stdout.write(sys.stdout.getvalue().decode('utf-8')) backup_stderr.write(sys.stderr.getvalue().decode('utf-8')) else: backup_stdout.write(sys.stdout.getvalue()) backup_stderr.write(sys.stderr.getvalue()) raise finally: sys.stdout = backup_stdout sys.stderr = backup_stderrmapproxy-1.11.0/mapproxy/test/http.py000066400000000000000000000411301320454472400176440ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function import re import threading import sys import cgi import socket import errno import time import base64 from contextlib import contextmanager from mapproxy.util.py import reraise from mapproxy.compat import iteritems, PY2 from mapproxy.compat.modules import urlparse if PY2: from cStringIO import StringIO else: from io import StringIO if PY2: from BaseHTTPServer import HTTPServer as HTTPServer_, BaseHTTPRequestHandler else: from http.server import HTTPServer as HTTPServer_, BaseHTTPRequestHandler class RequestsMismatchError(AssertionError): def __init__(self, assertions): self.assertions = assertions def __str__(self): assertions = [] for assertion in self.assertions: assertions.append(text_indent(str(assertion), ' ', ' - ')) return 'requests mismatch:\n' + '\n'.join(assertions) class RequestError(str): pass def text_indent(text, indent, first_indent=None): if first_indent is None: first_indent = indent text = first_indent + text return text.replace('\n', '\n' + indent) class RequestMismatch(object): def __init__(self, msg, expected, actual): self.msg = msg self.expected = expected self.actual = actual def __str__(self): return ('requests mismatch, expected:\n' + text_indent(str(self.expected), ' ') + '\n got:\n' + text_indent(str(self.actual), ' ')) class HTTPServer(HTTPServer_): allow_reuse_address = True def handle_error(self, request, client_address): _exc_class, exc, _tb = sys.exc_info() if isinstance(exc, socket.error): if exc.errno == errno.EPIPE: # suppres 'Broken pipe' errors raised in timeout tests return HTTPServer_.handle_error(self, request, client_address) class ThreadedStopableHTTPServer(threading.Thread): def __init__(self, address, requests_responses, unordered=False, query_comparator=None): threading.Thread.__init__(self, **{'group': None}) self.requests_responses = requests_responses self.daemon = True self.sucess = False self.shutdown = False self.httpd = HTTPServer(address,mock_http_handler(requests_responses, unordered=unordered, query_comparator=query_comparator)) self.httpd.timeout = 1.0 self.assertions = self.httpd.assertions = [] @property def http_port(self): return self.httpd.socket.getsockname()[1] def run(self): while self.requests_responses: if self.shutdown: break self.httpd.handle_request() if self.requests_responses: missing_req = [req for req, resp in self.requests_responses] self.assertions.append( RequestError('missing requests: ' + ','.join(map(str, missing_req))) ) if not self.assertions: self.sucess = True # force socket close so next test can bind to same address self.httpd.socket.close() class ThreadedSingleRequestHTTPServer(threading.Thread): def __init__(self, address, request_handler): threading.Thread.__init__(self, **{'group': None}) self.daemon = True self.sucess = False self.shutdown = False self.httpd = HTTPServer(address, request_handler) self.httpd.timeout = 1.0 self.assertions = self.httpd.assertions = [] def run(self): self.httpd.handle_request() if not self.assertions: self.sucess = True # force socket close so next test can bind to same address self.httpd.socket.close() def mock_http_handler(requests_responses, unordered=False, query_comparator=None): if query_comparator is None: query_comparator = query_eq class MockHTTPHandler(BaseHTTPRequestHandler): def do_GET(self): self.query_data = self.path return self.do_mock_request('GET') def do_POST(self): length = int(self.headers['content-length']) self.query_data = self.path + '?' + self.rfile.read(length).decode('utf-8') return self.do_mock_request('POST') def _matching_req_resp(self): if len(requests_responses) == 0: return None, None if unordered: for req_resp in requests_responses: req, resp = req_resp if query_comparator(req['path'], self.query_data): requests_responses.remove(req_resp) return req, resp return None, None else: return requests_responses.pop(0) def do_mock_request(self, method): req, resp = self._matching_req_resp() if not req: self.server.assertions.append( RequestError('got unexpected request: %s' % self.query_data) ) return if 'method' in req: if req['method'] != method: self.server.assertions.append( RequestMismatch('unexpected method', req['method'], method) ) self.server.shutdown = True if req.get('require_basic_auth', False): if 'Authorization' not in self.headers: requests_responses.insert(0, (req, resp)) # push back self.send_response(401) self.send_header('WWW-Authenticate', 'Basic realm="Secure Area"') self.end_headers() self.wfile.write(b'no access') return if req.get('headers'): for k, v in req['headers'].items(): if k not in self.headers: self.server.assertions.append( RequestMismatch('missing header', k, self.headers) ) elif self.headers[k] != v: self.server.assertions.append( RequestMismatch('header mismatch', '%s: %s' % (k, v), self.headers) ) if not query_comparator(req['path'], self.query_data): self.server.assertions.append( RequestMismatch('requests differ', req['path'], self.query_data) ) query_actual = set(query_to_dict(self.query_data).items()) query_expected = set(query_to_dict(req['path']).items()) self.server.assertions.append( RequestMismatch('requests params differ', query_expected - query_actual, query_actual - query_expected) ) self.server.shutdown = True if 'req_assert_function' in req: if not req['req_assert_function'](self): self.server.assertions.append( RequestError('req_assert_function failed') ) self.server.shutdown = True if 'duration' in resp: time.sleep(float(resp['duration'])) self.start_response(resp) if resp.get('body_file'): with open(resp['body_file'], 'rb') as f: self.wfile.write(f.read()) else: self.wfile.write(resp['body']) if not requests_responses: self.server.shutdown = True return def start_response(self, resp): self.send_response(int(resp.get('status', '200'))) if 'headers' in resp: for key, value in iteritems(resp['headers']): self.send_header(key, value) self.end_headers() def log_request(self, code, size=None): pass return MockHTTPHandler class MockServ(object): def __init__(self, port=0, host='localhost', unordered=False, bbox_aware_query_comparator=False): self._requested_port = port self.port = port self.host = host self.requests_responses = [] self.unordered = unordered self.query_comparator = None if bbox_aware_query_comparator: self.query_comparator = wms_query_eq self._init_thread() def _init_thread(self): self._thread = ThreadedStopableHTTPServer((self.host, self._requested_port), [], unordered=self.unordered, query_comparator=self.query_comparator) if self._requested_port == 0: self.port = self._thread.http_port self.address = (self.host, self.port) def reset(self): self._init_thread() @property def base_url(self): return 'http://localhost:%d' % (self.port, ) def expects(self, path, method='GET', headers=None): headers = headers or () self.requests_responses.append( (dict(path=path, method=method, headers=headers), {'body': b''})) return self def returns(self, body=None, body_file=None, status_code=200, headers=None): assert body or body_file headers = headers or {} self.requests_responses[-1][1].update( body=body, body_file=body_file, status=status_code, headers=headers) return self def __enter__(self): # copy request_responses to be able to reuse it after .reset() self._thread.requests_responses[:] = self.requests_responses self._thread.start() def __exit__(self, type, value, traceback): self._thread.shutdown = True self._thread.join() if not self._thread.sucess and value: print('requests to mock httpd did not ' 'match expectations:\n %s' % RequestsMismatchError(self._thread.assertions)) if value: raise reraise((type, value, traceback)) if not self._thread.sucess: raise RequestsMismatchError(self._thread.assertions) def wms_query_eq(expected, actual): """ >>> wms_query_eq('bAR=baz&foo=bizz&bbOX=0,0,100000,100000', 'foO=bizz&BBOx=-.0001,0.01,99999.99,100000.09&bar=baz') True >>> wms_query_eq('bAR=baz&foo=bizz&bbOX=0,0,100000,100000', 'foO=bizz&BBOx=-.0001,0.01,99999.99,100000.11&bar=baz') False >>> wms_query_eq('/service?bar=baz&fOO=bizz', 'foo=bizz&bar=baz') False >>> wms_query_eq('/1/2/3.png', '/1/2/3.png') True >>> wms_query_eq('/1/2/3.png', '/1/2/0.png') False """ from mapproxy.srs import bbox_equals if path_from_query(expected) != path_from_query(actual): return False expected = query_to_dict(expected) actual = query_to_dict(actual) if 'bbox' in expected and 'bbox' in actual: expected = expected.copy() expected_bbox = [float(x) for x in expected.pop('bbox').split(',')] actual = actual.copy() actual_bbox = [float(x) for x in actual.pop('bbox').split(',')] if expected != actual: return False if not bbox_equals(expected_bbox, actual_bbox): return False else: if expected != actual: return False return True numbers_only = re.compile('^-?\d+\.\d+(,-?\d+\.\d+)*$') def query_eq(expected, actual): """ >>> query_eq('bAR=baz&foo=bizz', 'foO=bizz&bar=baz') True >>> query_eq('/service?bar=baz&fOO=bizz', 'foo=bizz&bar=baz') False >>> query_eq('/1/2/3.png', '/1/2/3.png') True >>> query_eq('/1/2/3.png', '/1/2/0.png') False >>> query_eq('/map?point=2.9999999999,1.00000000001', '/map?point=3.0,1.0') True """ if path_from_query(expected) != path_from_query(actual): return False expected = query_to_dict(expected) actual = query_to_dict(actual) if set(expected.keys()) != set(actual.keys()): return False for ke, ve in expected.items(): if numbers_only.match(ve): if not float_string_almost_eq(ve, actual[ke]): return False else: if ve != actual[ke]: return False return True def float_string_almost_eq(expected, actual): """ Compares if two strings with comma-separated floats are almost equal. Strings must contain floats. >>> float_string_almost_eq('12345678900', '12345678901') False >>> float_string_almost_eq('12345678900.0', '12345678901.0') True >>> float_string_almost_eq('12345678900.0,-3.0', '12345678901.0,-2.9999999999') True """ if not numbers_only.match(expected) or not numbers_only.match(actual): return False expected_nums = [float(x) for x in expected.split(',')] actual_nums = [float(x) for x in actual.split(',')] if len(expected_nums) != len(actual_nums): return False for e, a in zip(expected_nums, actual_nums): if abs(e - a) > abs((e+a)/2)/10e9: return False return True def assert_query_eq(expected, actual, fuzzy_number_compare=False): path_actual = path_from_query(actual) path_expected = path_from_query(expected) assert path_expected == path_actual, path_expected + '!=' + path_actual query_actual = set(query_to_dict(actual).items()) query_expected = set(query_to_dict(expected).items()) if fuzzy_number_compare: equal = query_eq(expected, actual) else: equal = query_expected == query_actual assert equal, '%s != %s\t%s|%s' % ( expected, actual, query_expected - query_actual, query_actual - query_expected) def path_from_query(query): """ >>> path_from_query('/service?foo=bar') '/service' >>> path_from_query('/1/2/3.png') '/1/2/3.png' >>> path_from_query('foo=bar') '' """ if not ('&' in query or '=' in query): return query if '?' in query: return query.split('?', 1)[0] return '' def query_to_dict(query): """ >>> sorted(query_to_dict('/service?bar=baz&foo=bizz').items()) [('bar', 'baz'), ('foo', 'bizz')] >>> sorted(query_to_dict('bar=baz&foo=bizz').items()) [('bar', 'baz'), ('foo', 'bizz')] """ if not ('&' in query or '=' in query): return {} d = {} if '?' in query: query = query.split('?', 1)[-1] for key, value in cgi.parse_qsl(query): d[key.lower()] = value return d def assert_url_eq(url1, url2): parts1 = urlparse.urlsplit(url1) parts2 = urlparse.urlsplit(url2) assert parts1[0] == parts2[0], '%s != %s (%s)' % (url1, url2, 'schema') assert parts1[1] == parts2[1], '%s != %s (%s)' % (url1, url2, 'location') assert parts1[2] == parts2[2], '%s != %s (%s)' % (url1, url2, 'path') assert query_eq(parts1[3], parts2[3]), '%s != %s (%s)' % (url1, url2, 'query') assert parts1[4] == parts2[4], '%s != %s (%s)' % (url1, url2, 'fragment') @contextmanager def mock_httpd(address, requests_responses, unordered=False, bbox_aware_query_comparator=False): if bbox_aware_query_comparator: query_comparator = wms_query_eq else: query_comparator = query_eq t = ThreadedStopableHTTPServer(address, requests_responses, unordered=unordered, query_comparator=query_comparator) t.start() try: yield except: if not t.sucess: print(str(RequestsMismatchError(t.assertions))) raise finally: t.shutdown = True t.join(1) if not t.sucess: raise RequestsMismatchError(t.assertions) @contextmanager def mock_single_req_httpd(address, request_handler): t = ThreadedSingleRequestHTTPServer(address, request_handler) t.start() try: yield except: if not t.sucess: print(str(RequestsMismatchError(t.assertions))) raise finally: t.shutdown = True t.join(1) if not t.sucess: raise RequestsMismatchError(t.assertions) def make_wsgi_env(query_string, extra_environ={}): env = {'QUERY_STRING': query_string, 'wsgi.url_scheme': 'http', 'HTTP_HOST': 'localhost', } env.update(extra_environ) return env def basic_auth_value(username, password): return base64.b64encode(('%s:%s' % (username, password)).encode('utf-8')) mapproxy-1.11.0/mapproxy/test/image.py000066400000000000000000000147041320454472400177560ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function, division import os from mapproxy.compat.image import ( Image, ImageDraw, ImageColor, ) from mapproxy.compat import string_type, iteritems import tempfile from nose.tools import eq_ from io import BytesIO from contextlib import contextmanager def assert_image_mode(img, mode): pos = img.tell() try: img = Image.open(img) eq_(img.mode, mode) finally: img.seek(pos) def check_format(img, format): assert globals()['is_' + format.lower()](img), 'img is not %s' % format def has_magic_bytes(fileobj, bytes): pos = fileobj.tell() for magic in bytes: fileobj.seek(0) it_is = fileobj.read(len(magic)) == magic fileobj.seek(pos) if it_is: return True return False magic_bytes = { 'png': [b"\211PNG\r\n\032\n"], 'tiff': [b"MM\x00\x2a", b"II\x2a\x00"], 'geotiff': [b"MM\x00\x2a", b"II\x2a\x00"], 'gif': [b"GIF87a", b"GIF89a"], 'jpeg': [b"\xFF\xD8"], 'bmp': [b'BM'] } def create_is_x_functions(): for type_, magic in iteritems(magic_bytes): def create_is_type(type_, magic): def is_type(fileobj): if not hasattr(fileobj, 'read'): fileobj = BytesIO(fileobj) return has_magic_bytes(fileobj, magic) return is_type globals()['is_' + type_] = create_is_type(type_, magic) create_is_x_functions() del create_is_x_functions def is_transparent(img_data): data = BytesIO(img_data) img = Image.open(data) if img.mode == 'P': img = img.convert('RGBA') if img.mode == 'RGBA': return any(img.histogram()[-256:-1]) raise NotImplementedError( 'assert_is_transparent works only for RGBA images, got %s image' % img.mode) def img_from_buf(buf): data = BytesIO(buf) return Image.open(data) def bgcolor_ratio(img_data): """ Return the ratio of the primary/bg color. 1 == only bg color. """ data = BytesIO(img_data) img = Image.open(data) total_colors = img.size[0] * img.size[1] colors = img.getcolors() colors.sort() bgcolor = colors[-1][0] return bgcolor/total_colors def create_tmp_image_file(size, two_colored=False): fd, out_file = tempfile.mkstemp(suffix='.png') os.close(fd) print('creating temp image %s (%r)' % (out_file, size)) img = Image.new('RGBA', size) if two_colored: draw = ImageDraw.Draw(img) draw.rectangle((0, 0, img.size[0]//2, img.size[1]), fill=ImageColor.getrgb('white')) img.save(out_file, 'png') return out_file def create_image(size, color=None, mode=None): if color is not None: if isinstance(color, string_type): if mode is None: mode = 'RGB' img = Image.new(mode, size, color=color) else: if mode is None: mode = 'RGBA' if len(color) == 4 else 'RGB' img = Image.new(mode, size, color=tuple(color)) else: img = create_debug_img(size) return img def create_tmp_image_buf(size, format='png', color=None, mode='RGB'): img = create_image(size, color, mode) data = BytesIO() img.save(data, format) data.seek(0) return data def create_tmp_image(size, format='png', color=None, mode='RGB'): data = create_tmp_image_buf(size, format, color, mode) return data.read() def create_debug_img(size, transparent=True): if transparent: img = Image.new("RGBA", size) else: img = Image.new("RGB", size, ImageColor.getrgb("#EEE")) draw = ImageDraw.Draw(img) draw_pattern(draw, size) return img def draw_pattern(draw, size): w, h = size black_color = ImageColor.getrgb("black") draw.rectangle((0, 0, w-1, h-1), outline=black_color) draw.ellipse((0, 0, w-1, h-1), outline=black_color) step = w/16.0 for i in range(16): color = ImageColor.getrgb('#3' + hex(16-i)[-1] + hex(i)[-1]) draw.line((i*step, 0, i*step, h), fill=color) step = h/16.0 for i in range(16): color = ImageColor.getrgb('#' + hex(16-i)[-1] + hex(i)[-1] + '3') draw.line((0, i*step, w, i*step), fill=color) @contextmanager def tmp_image(size, format='png', color=None, mode='RGB'): if color is not None: img = Image.new(mode, size, color=color) else: img = create_debug_img(size) if format == 'jpeg': img = img.convert('RGB') data = BytesIO() img.save(data, format) data.seek(0) yield data def assert_img_colors_eq(img1, img2, delta=1, pixel_delta=1): """ assert that the colors of two images are equal. Use `delta` to accept small color variations (e.g. (255, 0, 127) == (254, 1, 128) with delta=1) Use `pixel_delta` to accept small variations in the number of pixels for each color (in percent of total pixels). `img1` and `img2` needs to be an image or list of colors like ``[(n, (r, g, b)), (n, (r, g, b)), ...]`` """ colors1 = sorted(img1.getcolors() if hasattr(img1, 'getcolors') else img1) colors2 = sorted(img2.getcolors() if hasattr(img2, 'getcolors') else img2) total_pixels = sum(n for n, _ in colors1) for (n1, c1), (n2, c2) in zip(colors1, colors2): assert abs(n1 - n2) < (total_pixels / 100 * pixel_delta), 'num colors not equal: %r != %r' % (colors1, colors2) assert_colors_eq(c1, c2) assert_colors_equal = assert_img_colors_eq def assert_colors_eq(c1, c2, delta=1): """ assert that two colors are equal. Use `delta` to accept small color variations. """ assert abs(c1[0] - c2[0]) <= delta, 'colors not equal: %r != %r' % (c1, c2) assert abs(c1[1] - c2[1]) <= delta, 'colors not equal: %r != %r' % (c1, c2) assert abs(c1[2] - c2[2]) <= delta, 'colors not equal: %r != %r' % (c1, c2) mapproxy-1.11.0/mapproxy/test/mocker.py000066400000000000000000002434441320454472400201610ustar00rootroot00000000000000""" Mocker Graceful platform for test doubles in Python: mocks, stubs, fakes, and dummies. Copyright (c) 2007-2010, Gustavo Niemeyer All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ import tempfile import unittest import inspect import shutil import types import sys import os import re import gc if sys.version_info < (2, 4): from sets import Set as set # pragma: nocover if sys.version_info[0] == 2: import __builtin__ else: import builtins as __builtin__ from mapproxy.compat import iteritems __all__ = ["Mocker", "Expect", "expect", "IS", "CONTAINS", "IN", "MATCH", "ANY", "ARGS", "KWARGS", "MockerTestCase"] __author__ = "Gustavo Niemeyer " __license__ = "BSD" __version__ = "1.1" ERROR_PREFIX = "[Mocker] " # -------------------------------------------------------------------- # Exceptions class MatchError(AssertionError): """Raised when an unknown expression is seen in playback mode.""" # -------------------------------------------------------------------- # Helper for chained-style calling. class expect(object): """This is a simple helper that allows a different call-style. With this class one can comfortably do chaining of calls to the mocker object responsible by the object being handled. For instance:: expect(obj.attr).result(3).count(1, 2) Is the same as:: obj.attr mocker.result(3) mocker.count(1, 2) """ __mocker__ = None def __init__(self, mock, attr=None): self._mock = mock self._attr = attr def __getattr__(self, attr): return self.__class__(self._mock, attr) def __call__(self, *args, **kwargs): mocker = self.__mocker__ if not mocker: mocker = self._mock.__mocker__ getattr(mocker, self._attr)(*args, **kwargs) return self def Expect(mocker): """Create an expect() "function" using the given Mocker instance. This helper allows defining an expect() "function" which works even in trickier cases such as: expect = Expect(mymocker) expect(iter(mock)).generate([1, 2, 3]) """ return type("Expect", (expect,), {"__mocker__": mocker}) # -------------------------------------------------------------------- # Extensions to Python's unittest. class MockerTestCase(unittest.TestCase): """unittest.TestCase subclass with Mocker support. @ivar mocker: The mocker instance. This is a convenience only. Mocker may easily be used with the standard C{unittest.TestCase} class if wanted. Test methods have a Mocker instance available on C{self.mocker}. At the end of each test method, expectations of the mocker will be verified, and any requested changes made to the environment will be restored. In addition to the integration with Mocker, this class provides a few additional helper methods. """ def __init__(self, methodName="runTest"): # So here is the trick: we take the real test method, wrap it on # a function that do the job we have to do, and insert it in the # *instance* dictionary, so that getattr() will return our # replacement rather than the class method. test_method = getattr(self, methodName, None) if test_method is not None: def test_method_wrapper(): try: result = test_method() except: raise else: if (self.mocker.is_recording() and self.mocker.get_events()): raise RuntimeError("Mocker must be put in replay " "mode with self.mocker.replay()") if (hasattr(result, "addCallback") and hasattr(result, "addErrback")): def verify(result): self.mocker.verify() return result result.addCallback(verify) else: self.mocker.verify() self.mocker.restore() return result # Copy all attributes from the original method.. for attr in dir(test_method): # .. unless they're present in our wrapper already. if not hasattr(test_method_wrapper, attr) or attr == "__doc__": setattr(test_method_wrapper, attr, getattr(test_method, attr)) setattr(self, methodName, test_method_wrapper) # We could overload run() normally, but other well-known testing # frameworks do it as well, and some of them won't call the super, # which might mean that cleanup wouldn't happen. With that in mind, # we make integration easier by using the following trick. run_method = self.run def run_wrapper(*args, **kwargs): try: return run_method(*args, **kwargs) finally: self.__cleanup() self.run = run_wrapper self.mocker = Mocker() self.expect = Expect(self.mocker) self.__cleanup_funcs = [] self.__cleanup_paths = [] super(MockerTestCase, self).__init__(methodName) def __call__(self, *args, **kwargs): # This is necessary for Python 2.3 only, because it didn't use run(), # which is supported above. try: super(MockerTestCase, self).__call__(*args, **kwargs) finally: if sys.version_info < (2, 4): self.__cleanup() def __cleanup(self): for path in self.__cleanup_paths: if os.path.isfile(path): os.unlink(path) elif os.path.isdir(path): shutil.rmtree(path) self.mocker.reset() for func, args, kwargs in self.__cleanup_funcs: func(*args, **kwargs) def addCleanup(self, func, *args, **kwargs): self.__cleanup_funcs.append((func, args, kwargs)) def makeFile(self, content=None, suffix="", prefix="tmp", basename=None, dirname=None, path=None): """Create a temporary file and return the path to it. @param content: Initial content for the file. @param suffix: Suffix to be given to the file's basename. @param prefix: Prefix to be given to the file's basename. @param basename: Full basename for the file. @param dirname: Put file inside this directory. The file is removed after the test runs. """ if path is not None: self.__cleanup_paths.append(path) elif basename is not None: if dirname is None: dirname = tempfile.mkdtemp() self.__cleanup_paths.append(dirname) path = os.path.join(dirname, basename) else: fd, path = tempfile.mkstemp(suffix, prefix, dirname) self.__cleanup_paths.append(path) os.close(fd) if content is None: os.unlink(path) if content is not None: file = open(path, "w") file.write(content) file.close() return path def makeDir(self, suffix="", prefix="tmp", dirname=None, path=None): """Create a temporary directory and return the path to it. @param suffix: Suffix to be given to the file's basename. @param prefix: Prefix to be given to the file's basename. @param dirname: Put directory inside this parent directory. The directory is removed after the test runs. """ if path is not None: os.makedirs(path) else: path = tempfile.mkdtemp(suffix, prefix, dirname) self.__cleanup_paths.append(path) return path def failUnlessIs(self, first, second, msg=None): """Assert that C{first} is the same object as C{second}.""" if first is not second: raise self.failureException(msg or "%r is not %r" % (first, second)) def failIfIs(self, first, second, msg=None): """Assert that C{first} is not the same object as C{second}.""" if first is second: raise self.failureException(msg or "%r is %r" % (first, second)) def failUnlessIn(self, first, second, msg=None): """Assert that C{first} is contained in C{second}.""" if first not in second: raise self.failureException(msg or "%r not in %r" % (first, second)) def failUnlessStartsWith(self, first, second, msg=None): """Assert that C{first} starts with C{second}.""" if first[:len(second)] != second: raise self.failureException(msg or "%r doesn't start with %r" % (first, second)) def failIfStartsWith(self, first, second, msg=None): """Assert that C{first} doesn't start with C{second}.""" if first[:len(second)] == second: raise self.failureException(msg or "%r starts with %r" % (first, second)) def failUnlessEndsWith(self, first, second, msg=None): """Assert that C{first} starts with C{second}.""" if first[len(first)-len(second):] != second: raise self.failureException(msg or "%r doesn't end with %r" % (first, second)) def failIfEndsWith(self, first, second, msg=None): """Assert that C{first} doesn't start with C{second}.""" if first[len(first)-len(second):] == second: raise self.failureException(msg or "%r ends with %r" % (first, second)) def failIfIn(self, first, second, msg=None): """Assert that C{first} is not contained in C{second}.""" if first in second: raise self.failureException(msg or "%r in %r" % (first, second)) def failUnlessApproximates(self, first, second, tolerance, msg=None): """Assert that C{first} is near C{second} by at most C{tolerance}.""" if abs(first - second) > tolerance: raise self.failureException(msg or "abs(%r - %r) > %r" % (first, second, tolerance)) def failIfApproximates(self, first, second, tolerance, msg=None): """Assert that C{first} is far from C{second} by at least C{tolerance}. """ if abs(first - second) <= tolerance: raise self.failureException(msg or "abs(%r - %r) <= %r" % (first, second, tolerance)) def failUnlessMethodsMatch(self, first, second): """Assert that public methods in C{first} are present in C{second}. This method asserts that all public methods found in C{first} are also present in C{second} and accept the same arguments. C{first} may have its own private methods, though, and may not have all methods found in C{second}. Note that if a private method in C{first} matches the name of one in C{second}, their specification is still compared. This is useful to verify if a fake or stub class have the same API as the real class being simulated. """ first_methods = dict(inspect.getmembers(first, inspect.ismethod)) second_methods = dict(inspect.getmembers(second, inspect.ismethod)) for name, first_method in iteritems(first_methods): first_argspec = inspect.getargspec(first_method) first_formatted = inspect.formatargspec(*first_argspec) second_method = second_methods.get(name) if second_method is None: if name[:1] == "_": continue # First may have its own private methods. raise self.failureException("%s.%s%s not present in %s" % (first.__name__, name, first_formatted, second.__name__)) second_argspec = inspect.getargspec(second_method) if first_argspec != second_argspec: second_formatted = inspect.formatargspec(*second_argspec) raise self.failureException("%s.%s%s != %s.%s%s" % (first.__name__, name, first_formatted, second.__name__, name, second_formatted)) def failUnlessRaises(self, excClass, *args, **kwargs): """ Fail unless an exception of class excClass is thrown by callableObj when invoked with arguments args and keyword arguments kwargs. If a different type of exception is thrown, it will not be caught, and the test case will be deemed to have suffered an error, exactly as for an unexpected exception. It returns the exception instance if it matches the given exception class. This may also be used as a context manager when provided with a single argument, as such: with self.failUnlessRaises(ExcClass): logic_which_should_raise() """ return self.failUnlessRaisesRegexp(excClass, None, *args, **kwargs) def failUnlessRaisesRegexp(self, excClass, regexp, *args, **kwargs): """ Fail unless an exception of class excClass is thrown by callableObj when invoked with arguments args and keyword arguments kwargs, and the str(error) value matches the provided regexp. If a different type of exception is thrown, it will not be caught, and the test case will be deemed to have suffered an error, exactly as for an unexpected exception. It returns the exception instance if it matches the given exception class. This may also be used as a context manager when provided with a single argument, as such: with self.failUnlessRaisesRegexp(ExcClass, "something like.*happened"): logic_which_should_raise() """ def match_regexp(error): error_str = str(error) if regexp is not None and not re.search(regexp, error_str): raise self.failureException("%r doesn't match %r" % (error_str, regexp)) excName = self.__class_name(excClass) if args: callableObj = args[0] try: result = callableObj(*args[1:], **kwargs) except excClass as e: match_regexp(e) return e else: raise self.failureException("%s not raised (%r returned)" % (excName, result)) else: test = self class AssertRaisesContextManager(object): def __enter__(self): return self def __exit__(self, type, value, traceback): self.exception = value if value is None: raise test.failureException("%s not raised" % excName) elif isinstance(value, excClass): match_regexp(value) return True return AssertRaisesContextManager() def __class_name(self, cls): return getattr(cls, "__name__", str(cls)) def failUnlessIsInstance(self, obj, cls, msg=None): """Assert that isinstance(obj, cls).""" if not isinstance(obj, cls): if msg is None: msg = "%r is not an instance of %s" % \ (obj, self.__class_name(cls)) raise self.failureException(msg) def failIfIsInstance(self, obj, cls, msg=None): """Assert that isinstance(obj, cls) is False.""" if isinstance(obj, cls): if msg is None: msg = "%r is an instance of %s" % \ (obj, self.__class_name(cls)) raise self.failureException(msg) assertIs = failUnlessIs assertIsNot = failIfIs assertIn = failUnlessIn assertNotIn = failIfIn assertStartsWith = failUnlessStartsWith assertNotStartsWith = failIfStartsWith assertEndsWith = failUnlessEndsWith assertNotEndsWith = failIfEndsWith assertApproximates = failUnlessApproximates assertNotApproximates = failIfApproximates assertMethodsMatch = failUnlessMethodsMatch assertRaises = failUnlessRaises assertRaisesRegexp = failUnlessRaisesRegexp assertIsInstance = failUnlessIsInstance assertIsNotInstance = failIfIsInstance assertNotIsInstance = failIfIsInstance # Poor choice in 2.7/3.2+. # The following are missing in Python < 2.4. assertTrue = unittest.TestCase.failUnless assertFalse = unittest.TestCase.failIf # The following is provided for compatibility with Twisted's trial. assertIdentical = assertIs assertNotIdentical = assertIsNot failUnlessIdentical = failUnlessIs failIfIdentical = failIfIs # -------------------------------------------------------------------- # Mocker. class classinstancemethod(object): def __init__(self, method): self.method = method def __get__(self, obj, cls=None): def bound_method(*args, **kwargs): return self.method(cls, obj, *args, **kwargs) return bound_method class MockerBase(object): """Controller of mock objects. A mocker instance is used to command recording and replay of expectations on any number of mock objects. Expectations should be expressed for the mock object while in record mode (the initial one) by using the mock object itself, and using the mocker (and/or C{expect()} as a helper) to define additional behavior for each event. For instance:: mock = mocker.mock() mock.hello() mocker.result("Hi!") mocker.replay() assert mock.hello() == "Hi!" mock.restore() mock.verify() In this short excerpt a mock object is being created, then an expectation of a call to the C{hello()} method was recorded, and when called the method should return the value C{10}. Then, the mocker is put in replay mode, and the expectation is satisfied by calling the C{hello()} method, which indeed returns 10. Finally, a call to the L{restore()} method is performed to undo any needed changes made in the environment, and the L{verify()} method is called to ensure that all defined expectations were met. The same logic can be expressed more elegantly using the C{with mocker:} statement, as follows:: mock = mocker.mock() mock.hello() mocker.result("Hi!") with mocker: assert mock.hello() == "Hi!" Also, the MockerTestCase class, which integrates the mocker on a unittest.TestCase subclass, may be used to reduce the overhead of controlling the mocker. A test could be written as follows:: class SampleTest(MockerTestCase): def test_hello(self): mock = self.mocker.mock() mock.hello() self.mocker.result("Hi!") self.mocker.replay() self.assertEquals(mock.hello(), "Hi!") """ _recorders = [] # For convenience only. on = expect class __metaclass__(type): def __init__(self, name, bases, dict): # Make independent lists on each subclass, inheriting from parent. self._recorders = list(getattr(self, "_recorders", ())) def __init__(self): self._recorders = self._recorders[:] self._events = [] self._recording = True self._ordering = False self._last_orderer = None def is_recording(self): """Return True if in recording mode, False if in replay mode. Recording is the initial state. """ return self._recording def replay(self): """Change to replay mode, where recorded events are reproduced. If already in replay mode, the mocker will be restored, with all expectations reset, and then put again in replay mode. An alternative and more comfortable way to replay changes is using the 'with' statement, as follows:: mocker = Mocker() with mocker: The 'with' statement will automatically put mocker in replay mode, and will also verify if all events were correctly reproduced at the end (using L{verify()}), and also restore any changes done in the environment (with L{restore()}). Also check the MockerTestCase class, which integrates the unittest.TestCase class with mocker. """ if not self._recording: for event in self._events: event.restore() else: self._recording = False for event in self._events: event.replay() def restore(self): """Restore changes in the environment, and return to recording mode. This should always be called after the test is complete (succeeding or not). There are ways to call this method automatically on completion (e.g. using a C{with mocker:} statement, or using the L{MockerTestCase} class. """ if not self._recording: self._recording = True for event in self._events: event.restore() def reset(self): """Reset the mocker state. This will restore environment changes, if currently in replay mode, and then remove all events previously recorded. """ if not self._recording: self.restore() self.unorder() del self._events[:] def get_events(self): """Return all recorded events.""" return self._events[:] def add_event(self, event): """Add an event. This method is used internally by the implementation, and shouldn't be needed on normal mocker usage. """ self._events.append(event) if self._ordering: orderer = event.add_task(Orderer(event.path)) if self._last_orderer: orderer.add_dependency(self._last_orderer) self._last_orderer = orderer return event def verify(self): """Check if all expectations were met, and raise AssertionError if not. The exception message will include a nice description of which expectations were not met, and why. """ errors = [] for event in self._events: try: event.verify() except AssertionError as e: error = str(e) if not error: raise RuntimeError("Empty error message from %r" % event) errors.append(error) if errors: message = [ERROR_PREFIX + "Unmet expectations:", ""] for error in errors: lines = error.splitlines() message.append("=> " + lines.pop(0)) message.extend([" " + line for line in lines]) message.append("") raise AssertionError(os.linesep.join(message)) def mock(self, spec_and_type=None, spec=None, type=None, name=None, count=True): """Return a new mock object. @param spec_and_type: Handy positional argument which sets both spec and type. @param spec: Method calls will be checked for correctness against the given class. @param type: If set, the Mock's __class__ attribute will return the given type. This will make C{isinstance()} calls on the object work. @param name: Name for the mock object, used in the representation of expressions. The name is rarely needed, as it's usually guessed correctly from the variable name used. @param count: If set to false, expressions may be executed any number of times, unless an expectation is explicitly set using the L{count()} method. By default, expressions are expected once. """ if spec_and_type is not None: spec = type = spec_and_type return Mock(self, spec=spec, type=type, name=name, count=count) def proxy(self, object, spec=True, type=True, name=None, count=True, passthrough=True): """Return a new mock object which proxies to the given object. Proxies are useful when only part of the behavior of an object is to be mocked. Unknown expressions may be passed through to the real implementation implicitly (if the C{passthrough} argument is True), or explicitly (using the L{passthrough()} method on the event). @param object: Real object to be proxied, and replaced by the mock on replay mode. It may also be an "import path", such as C{"time.time"}, in which case the object will be the C{time} function from the C{time} module. @param spec: Method calls will be checked for correctness against the given object, which may be a class or an instance where attributes will be looked up. Defaults to the the C{object} parameter. May be set to None explicitly, in which case spec checking is disabled. Checks may also be disabled explicitly on a per-event basis with the L{nospec()} method. @param type: If set, the Mock's __class__ attribute will return the given type. This will make C{isinstance()} calls on the object work. Defaults to the type of the C{object} parameter. May be set to None explicitly. @param name: Name for the mock object, used in the representation of expressions. The name is rarely needed, as it's usually guessed correctly from the variable name used. @param count: If set to false, expressions may be executed any number of times, unless an expectation is explicitly set using the L{count()} method. By default, expressions are expected once. @param passthrough: If set to False, passthrough of actions on the proxy to the real object will only happen when explicitly requested via the L{passthrough()} method. """ if isinstance(object, basestring): if name is None: name = object import_stack = object.split(".") attr_stack = [] while import_stack: module_path = ".".join(import_stack) try: __import__(module_path) except ImportError: attr_stack.insert(0, import_stack.pop()) if not import_stack: raise continue else: object = sys.modules[module_path] for attr in attr_stack: object = getattr(object, attr) break if isinstance(object, types.UnboundMethodType): object = object.__func__ if spec is True: spec = object if type is True: type = __builtin__.type(object) return Mock(self, spec=spec, type=type, object=object, name=name, count=count, passthrough=passthrough) def replace(self, object, spec=True, type=True, name=None, count=True, passthrough=True): """Create a proxy, and replace the original object with the mock. On replay, the original object will be replaced by the returned proxy in all dictionaries found in the running interpreter via the garbage collecting system. This should cover module namespaces, class namespaces, instance namespaces, and so on. @param object: Real object to be proxied, and replaced by the mock on replay mode. It may also be an "import path", such as C{"time.time"}, in which case the object will be the C{time} function from the C{time} module. @param spec: Method calls will be checked for correctness against the given object, which may be a class or an instance where attributes will be looked up. Defaults to the the C{object} parameter. May be set to None explicitly, in which case spec checking is disabled. Checks may also be disabled explicitly on a per-event basis with the L{nospec()} method. @param type: If set, the Mock's __class__ attribute will return the given type. This will make C{isinstance()} calls on the object work. Defaults to the type of the C{object} parameter. May be set to None explicitly. @param name: Name for the mock object, used in the representation of expressions. The name is rarely needed, as it's usually guessed correctly from the variable name used. @param passthrough: If set to False, passthrough of actions on the proxy to the real object will only happen when explicitly requested via the L{passthrough()} method. """ mock = self.proxy(object, spec, type, name, count, passthrough) event = self._get_replay_restore_event() event.add_task(ProxyReplacer(mock)) return mock def patch(self, object, spec=True): """Patch an existing object to reproduce recorded events. @param object: Class or instance to be patched. @param spec: Method calls will be checked for correctness against the given object, which may be a class or an instance where attributes will be looked up. Defaults to the the C{object} parameter. May be set to None explicitly, in which case spec checking is disabled. Checks may also be disabled explicitly on a per-event basis with the L{nospec()} method. The result of this method is still a mock object, which can be used like any other mock object to record events. The difference is that when the mocker is put on replay mode, the *real* object will be modified to behave according to recorded expectations. Patching works in individual instances, and also in classes. When an instance is patched, recorded events will only be considered on this specific instance, and other instances should behave normally. When a class is patched, the reproduction of events will be considered on any instance of this class once created (collectively). Observe that, unlike with proxies which catch only events done through the mock object, *all* accesses to recorded expectations will be considered; even these coming from the object itself (e.g. C{self.hello()} is considered if this method was patched). While this is a very powerful feature, and many times the reason to use patches in the first place, it's important to keep this behavior in mind. Patching of the original object only takes place when the mocker is put on replay mode, and the patched object will be restored to its original state once the L{restore()} method is called (explicitly, or implicitly with alternative conventions, such as a C{with mocker:} block, or a MockerTestCase class). """ if spec is True: spec = object patcher = Patcher() event = self._get_replay_restore_event() event.add_task(patcher) mock = Mock(self, object=object, patcher=patcher, passthrough=True, spec=spec) patcher.patch_attr(object, '__mocker_mock__', mock) return mock def act(self, path): """This is called by mock objects whenever something happens to them. This method is part of the interface between the mocker and mock objects. """ if self._recording: event = self.add_event(Event(path)) for recorder in self._recorders: recorder(self, event) return Mock(self, path) else: # First run events that may run, then run unsatisfied events, then # ones not previously run. We put the index in the ordering tuple # instead of the actual event because we want a stable sort # (ordering between 2 events is undefined). events = self._events order = [(events[i].satisfied()*2 + events[i].has_run(), i) for i in range(len(events))] order.sort() postponed = None for weight, i in order: event = events[i] if event.matches(path): if event.may_run(path): return event.run(path) elif postponed is None: postponed = event if postponed is not None: return postponed.run(path) raise MatchError(ERROR_PREFIX + "Unexpected expression: %s" % path) def get_recorders(cls, self): """Return recorders associated with this mocker class or instance. This method may be called on mocker instances and also on mocker classes. See the L{add_recorder()} method for more information. """ return (self or cls)._recorders[:] get_recorders = classinstancemethod(get_recorders) def add_recorder(cls, self, recorder): """Add a recorder to this mocker class or instance. @param recorder: Callable accepting C{(mocker, event)} as parameters. This is part of the implementation of mocker. All registered recorders are called for translating events that happen during recording into expectations to be met once the state is switched to replay mode. This method may be called on mocker instances and also on mocker classes. When called on a class, the recorder will be used by all instances, and also inherited on subclassing. When called on instances, the recorder is added only to the given instance. """ (self or cls)._recorders.append(recorder) return recorder add_recorder = classinstancemethod(add_recorder) def remove_recorder(cls, self, recorder): """Remove the given recorder from this mocker class or instance. This method may be called on mocker classes and also on mocker instances. See the L{add_recorder()} method for more information. """ (self or cls)._recorders.remove(recorder) remove_recorder = classinstancemethod(remove_recorder) def result(self, value): """Make the last recorded event return the given value on replay. @param value: Object to be returned when the event is replayed. """ self.call(lambda *args, **kwargs: value) def generate(self, sequence): """Last recorded event will return a generator with the given sequence. @param sequence: Sequence of values to be generated. """ def generate(*args, **kwargs): for value in sequence: yield value self.call(generate) def throw(self, exception): """Make the last recorded event raise the given exception on replay. @param exception: Class or instance of exception to be raised. """ def raise_exception(*args, **kwargs): raise exception self.call(raise_exception) def call(self, func, with_object=False): """Make the last recorded event cause the given function to be called. @param func: Function to be called. @param with_object: If True, the called function will receive the patched or proxied object so that its state may be used or verified in checks. The result of the function will be used as the event result. """ event = self._events[-1] if with_object and event.path.root_object is None: raise TypeError("Mock object isn't a proxy") event.add_task(FunctionRunner(func, with_root_object=with_object)) def count(self, min, max=False): """Last recorded event must be replayed between min and max times. @param min: Minimum number of times that the event must happen. @param max: Maximum number of times that the event must happen. If not given, it defaults to the same value of the C{min} parameter. If set to None, there is no upper limit, and the expectation is met as long as it happens at least C{min} times. """ event = self._events[-1] for task in event.get_tasks(): if isinstance(task, RunCounter): event.remove_task(task) event.prepend_task(RunCounter(min, max)) def is_ordering(self): """Return true if all events are being ordered. See the L{order()} method. """ return self._ordering def unorder(self): """Disable the ordered mode. See the L{order()} method for more information. """ self._ordering = False self._last_orderer = None def order(self, *path_holders): """Create an expectation of order between two or more events. @param path_holders: Objects returned as the result of recorded events. By default, mocker won't force events to happen precisely in the order they were recorded. Calling this method will change this behavior so that events will only match if reproduced in the correct order. There are two ways in which this method may be used. Which one is used in a given occasion depends only on convenience. If no arguments are passed, the mocker will be put in a mode where all the recorded events following the method call will only be met if they happen in order. When that's used, the mocker may be put back in unordered mode by calling the L{unorder()} method, or by using a 'with' block, like so:: with mocker.ordered(): In this case, only expressions in will be ordered, and the mocker will be back in unordered mode after the 'with' block. The second way to use it is by specifying precisely which events should be ordered. As an example:: mock = mocker.mock() expr1 = mock.hello() expr2 = mock.world expr3 = mock.x.y.z mocker.order(expr1, expr2, expr3) This method of ordering only works when the expression returns another object. Also check the L{after()} and L{before()} methods, which are alternative ways to perform this. """ if not path_holders: self._ordering = True return OrderedContext(self) last_orderer = None for path_holder in path_holders: if type(path_holder) is Path: path = path_holder else: path = path_holder.__mocker_path__ for event in self._events: if event.path is path: for task in event.get_tasks(): if isinstance(task, Orderer): orderer = task break else: orderer = Orderer(path) event.add_task(orderer) if last_orderer: orderer.add_dependency(last_orderer) last_orderer = orderer break def after(self, *path_holders): """Last recorded event must happen after events referred to. @param path_holders: Objects returned as the result of recorded events which should happen before the last recorded event As an example, the idiom:: expect(mock.x).after(mock.y, mock.z) is an alternative way to say:: expr_x = mock.x expr_y = mock.y expr_z = mock.z mocker.order(expr_y, expr_x) mocker.order(expr_z, expr_x) See L{order()} for more information. """ last_path = self._events[-1].path for path_holder in path_holders: self.order(path_holder, last_path) def before(self, *path_holders): """Last recorded event must happen before events referred to. @param path_holders: Objects returned as the result of recorded events which should happen after the last recorded event As an example, the idiom:: expect(mock.x).before(mock.y, mock.z) is an alternative way to say:: expr_x = mock.x expr_y = mock.y expr_z = mock.z mocker.order(expr_x, expr_y) mocker.order(expr_x, expr_z) See L{order()} for more information. """ last_path = self._events[-1].path for path_holder in path_holders: self.order(last_path, path_holder) def nospec(self): """Don't check method specification of real object on last event. By default, when using a mock created as the result of a call to L{proxy()}, L{replace()}, and C{patch()}, or when passing the spec attribute to the L{mock()} method, method calls on the given object are checked for correctness against the specification of the real object (or the explicitly provided spec). This method will disable that check specifically for the last recorded event. """ event = self._events[-1] for task in event.get_tasks(): if isinstance(task, SpecChecker): event.remove_task(task) def passthrough(self, result_callback=None): """Make the last recorded event run on the real object once seen. @param result_callback: If given, this function will be called with the result of the *real* method call as the only argument. This can only be used on proxies, as returned by the L{proxy()} and L{replace()} methods, or on mocks representing patched objects, as returned by the L{patch()} method. """ event = self._events[-1] if event.path.root_object is None: raise TypeError("Mock object isn't a proxy") event.add_task(PathExecuter(result_callback)) def __enter__(self): """Enter in a 'with' context. This will run replay().""" self.replay() return self def __exit__(self, type, value, traceback): """Exit from a 'with' context. This will run restore() at all times, but will only run verify() if the 'with' block itself hasn't raised an exception. Exceptions in that block are never swallowed. """ self.restore() if type is None: self.verify() return False def _get_replay_restore_event(self): """Return unique L{ReplayRestoreEvent}, creating if needed. Some tasks only want to replay/restore. When that's the case, they shouldn't act on other events during replay. Also, they can all be put in a single event when that's the case. Thus, we add a single L{ReplayRestoreEvent} as the first element of the list. """ if not self._events or type(self._events[0]) != ReplayRestoreEvent: self._events.insert(0, ReplayRestoreEvent()) return self._events[0] class OrderedContext(object): def __init__(self, mocker): self._mocker = mocker def __enter__(self): return None def __exit__(self, type, value, traceback): self._mocker.unorder() class Mocker(MockerBase): __doc__ = MockerBase.__doc__ # Decorator to add recorders on the standard Mocker class. recorder = Mocker.add_recorder # -------------------------------------------------------------------- # Mock object. class Mock(object): def __init__(self, mocker, path=None, name=None, spec=None, type=None, object=None, passthrough=False, patcher=None, count=True): self.__mocker__ = mocker self.__mocker_path__ = path or Path(self, object) self.__mocker_name__ = name self.__mocker_spec__ = spec self.__mocker_object__ = object self.__mocker_passthrough__ = passthrough self.__mocker_patcher__ = patcher self.__mocker_replace__ = False self.__mocker_type__ = type self.__mocker_count__ = count def __mocker_act__(self, kind, args=(), kwargs={}, object=None): if self.__mocker_name__ is None: self.__mocker_name__ = find_object_name(self, 2) action = Action(kind, args, kwargs, self.__mocker_path__) path = self.__mocker_path__ + action if object is not None: path.root_object = object try: return self.__mocker__.act(path) except MatchError as exception: root_mock = path.root_mock if (path.root_object is not None and root_mock.__mocker_passthrough__): return path.execute(path.root_object) # Reinstantiate to show raise statement on traceback, and # also to make the traceback shown shorter. raise MatchError(str(exception)) except AssertionError as e: lines = str(e).splitlines() message = [ERROR_PREFIX + "Unmet expectation:", ""] message.append("=> " + lines.pop(0)) message.extend([" " + line for line in lines]) message.append("") raise AssertionError(os.linesep.join(message)) def __getattribute__(self, name): if name.startswith("__mocker_"): return super(Mock, self).__getattribute__(name) if name == "__class__": if self.__mocker__.is_recording() or self.__mocker_type__ is None: return type(self) return self.__mocker_type__ if name == "__length_hint__": # This is used by Python 2.6+ to optimize the allocation # of arrays in certain cases. Pretend it doesn't exist. raise AttributeError("No __length_hint__ here!") return self.__mocker_act__("getattr", (name,)) def __setattr__(self, name, value): if name.startswith("__mocker_"): return super(Mock, self).__setattr__(name, value) return self.__mocker_act__("setattr", (name, value)) def __delattr__(self, name): return self.__mocker_act__("delattr", (name,)) def __call__(self, *args, **kwargs): return self.__mocker_act__("call", args, kwargs) def __contains__(self, value): return self.__mocker_act__("contains", (value,)) def __getitem__(self, key): return self.__mocker_act__("getitem", (key,)) def __setitem__(self, key, value): return self.__mocker_act__("setitem", (key, value)) def __delitem__(self, key): return self.__mocker_act__("delitem", (key,)) def __len__(self): # MatchError is turned on an AttributeError so that list() and # friends act properly when trying to get length hints on # something that doesn't offer them. try: result = self.__mocker_act__("len") except MatchError as e: raise AttributeError(str(e)) if type(result) is Mock: return 0 return result def __nonzero__(self): try: result = self.__mocker_act__("nonzero") except MatchError as e: return True if type(result) is Mock: return True return result def __iter__(self): # XXX On py3k, when next() becomes __next__(), we'll be able # to return the mock itself because it will be considered # an iterator (we'll be mocking __next__ as well, which we # can't now). result = self.__mocker_act__("iter") if type(result) is Mock: return iter([]) return result # When adding a new action kind here, also add support for it on # Action.execute() and Path.__str__(). def find_object_name(obj, depth=0): """Try to detect how the object is named on a previous scope.""" try: frame = sys._getframe(depth+1) except: return None for name, frame_obj in iteritems(frame.f_locals): if frame_obj is obj: return name self = frame.f_locals.get("self") if self is not None: try: items = list(self.__dict__.items()) except: pass else: for name, self_obj in items: if self_obj is obj: return name return None # -------------------------------------------------------------------- # Action and path. class Action(object): def __init__(self, kind, args, kwargs, path=None): self.kind = kind self.args = args self.kwargs = kwargs self.path = path self._execute_cache = {} def __repr__(self): if self.path is None: return "Action(%r, %r, %r)" % (self.kind, self.args, self.kwargs) return "Action(%r, %r, %r, %r)" % \ (self.kind, self.args, self.kwargs, self.path) def __eq__(self, other): return (self.kind == other.kind and self.args == other.args and self.kwargs == other.kwargs) def __ne__(self, other): return not self.__eq__(other) def matches(self, other): return (self.kind == other.kind and match_params(self.args, self.kwargs, other.args, other.kwargs)) def execute(self, object): # This caching scheme may fail if the object gets deallocated before # the action, as the id might get reused. It's somewhat easy to fix # that with a weakref callback. For our uses, though, the object # should never get deallocated before the action itself, so we'll # just keep it simple. if id(object) in self._execute_cache: return self._execute_cache[id(object)] execute = getattr(object, "__mocker_execute__", None) if execute is not None: result = execute(self, object) else: kind = self.kind if kind == "getattr": result = getattr(object, self.args[0]) elif kind == "setattr": result = setattr(object, self.args[0], self.args[1]) elif kind == "delattr": result = delattr(object, self.args[0]) elif kind == "call": result = object(*self.args, **self.kwargs) elif kind == "contains": result = self.args[0] in object elif kind == "getitem": result = object[self.args[0]] elif kind == "setitem": result = object[self.args[0]] = self.args[1] elif kind == "delitem": del object[self.args[0]] result = None elif kind == "len": result = len(object) elif kind == "nonzero": result = bool(object) elif kind == "iter": result = iter(object) else: raise RuntimeError("Don't know how to execute %r kind." % kind) self._execute_cache[id(object)] = result return result class Path(object): def __init__(self, root_mock, root_object=None, actions=()): self.root_mock = root_mock self.root_object = root_object self.actions = tuple(actions) self.__mocker_replace__ = False def parent_path(self): if not self.actions: return None return self.actions[-1].path parent_path = property(parent_path) def __add__(self, action): """Return a new path which includes the given action at the end.""" return self.__class__(self.root_mock, self.root_object, self.actions + (action,)) def __eq__(self, other): """Verify if the two paths are equal. Two paths are equal if they refer to the same mock object, and have the actions with equal kind, args and kwargs. """ if (self.root_mock is not other.root_mock or self.root_object is not other.root_object or len(self.actions) != len(other.actions)): return False for action, other_action in zip(self.actions, other.actions): if action != other_action: return False return True def matches(self, other): """Verify if the two paths are equivalent. Two paths are equal if they refer to the same mock object, and have the same actions performed on them. """ if (self.root_mock is not other.root_mock or len(self.actions) != len(other.actions)): return False for action, other_action in zip(self.actions, other.actions): if not action.matches(other_action): return False return True def execute(self, object): """Execute all actions sequentially on object, and return result. """ for action in self.actions: object = action.execute(object) return object def __str__(self): """Transform the path into a nice string such as obj.x.y('z').""" result = self.root_mock.__mocker_name__ or "" for action in self.actions: if action.kind == "getattr": result = "%s.%s" % (result, action.args[0]) elif action.kind == "setattr": result = "%s.%s = %r" % (result, action.args[0], action.args[1]) elif action.kind == "delattr": result = "del %s.%s" % (result, action.args[0]) elif action.kind == "call": args = [repr(x) for x in action.args] items = list(action.kwargs.items()) items.sort() for pair in items: args.append("%s=%r" % pair) result = "%s(%s)" % (result, ", ".join(args)) elif action.kind == "contains": result = "%r in %s" % (action.args[0], result) elif action.kind == "getitem": result = "%s[%r]" % (result, action.args[0]) elif action.kind == "setitem": result = "%s[%r] = %r" % (result, action.args[0], action.args[1]) elif action.kind == "delitem": result = "del %s[%r]" % (result, action.args[0]) elif action.kind == "len": result = "len(%s)" % result elif action.kind == "nonzero": result = "bool(%s)" % result elif action.kind == "iter": result = "iter(%s)" % result else: raise RuntimeError("Don't know how to format kind %r" % action.kind) return result class SpecialArgument(object): """Base for special arguments for matching parameters.""" def __init__(self, object=None): self.object = object def __repr__(self): if self.object is None: return self.__class__.__name__ else: return "%s(%r)" % (self.__class__.__name__, self.object) def matches(self, other): return True def __eq__(self, other): return type(other) == type(self) and self.object == other.object class ANY(SpecialArgument): """Matches any single argument.""" ANY = ANY() class ARGS(SpecialArgument): """Matches zero or more positional arguments.""" ARGS = ARGS() class KWARGS(SpecialArgument): """Matches zero or more keyword arguments.""" KWARGS = KWARGS() class IS(SpecialArgument): def matches(self, other): return self.object is other def __eq__(self, other): return type(other) == type(self) and self.object is other.object class CONTAINS(SpecialArgument): def matches(self, other): try: other.__contains__ except AttributeError: try: iter(other) except TypeError: # If an object can't be iterated, and has no __contains__ # hook, it'd blow up on the test below. We test this in # advance to prevent catching more errors than we really # want. return False return self.object in other class IN(SpecialArgument): def matches(self, other): return other in self.object class MATCH(SpecialArgument): def matches(self, other): return bool(self.object(other)) def __eq__(self, other): return type(other) == type(self) and self.object is other.object def match_params(args1, kwargs1, args2, kwargs2): """Match the two sets of parameters, considering special parameters.""" has_args = ARGS in args1 has_kwargs = KWARGS in args1 if has_kwargs: args1 = [arg1 for arg1 in args1 if arg1 is not KWARGS] elif len(kwargs1) != len(kwargs2): return False if not has_args and len(args1) != len(args2): return False # Either we have the same number of kwargs, or unknown keywords are # accepted (KWARGS was used), so check just the ones in kwargs1. for key, arg1 in iteritems(kwargs1): if key not in kwargs2: return False arg2 = kwargs2[key] if isinstance(arg1, SpecialArgument): if not arg1.matches(arg2): return False elif arg1 != arg2: return False # Keywords match. Now either we have the same number of # arguments, or ARGS was used. If ARGS wasn't used, arguments # must match one-on-one necessarily. if not has_args: for arg1, arg2 in zip(args1, args2): if isinstance(arg1, SpecialArgument): if not arg1.matches(arg2): return False elif arg1 != arg2: return False return True # Easy choice. Keywords are matching, and anything on args is accepted. if (ARGS,) == args1: return True # We have something different there. If we don't have positional # arguments on the original call, it can't match. if not args2: # Unless we have just several ARGS (which is bizarre, but..). for arg1 in args1: if arg1 is not ARGS: return False return True # Ok, all bets are lost. We have to actually do the more expensive # matching. This is an algorithm based on the idea of the Levenshtein # Distance between two strings, but heavily hacked for this purpose. args2l = len(args2) if args1[0] is ARGS: args1 = args1[1:] array = [0]*args2l else: array = [1]*args2l for i in range(len(args1)): last = array[0] if args1[i] is ARGS: for j in range(1, args2l): last, array[j] = array[j], min(array[j-1], array[j], last) else: array[0] = i or int(args1[i] != args2[0]) for j in range(1, args2l): last, array[j] = array[j], last or int(args1[i] != args2[j]) if 0 not in array: return False if array[-1] != 0: return False return True # -------------------------------------------------------------------- # Event and task base. class Event(object): """Aggregation of tasks that keep track of a recorded action. An event represents something that may or may not happen while the mocked environment is running, such as an attribute access, or a method call. The event is composed of several tasks that are orchestrated together to create a composed meaning for the event, including for which actions it should be run, what happens when it runs, and what's the expectations about the actions run. """ def __init__(self, path=None): self.path = path self._tasks = [] self._has_run = False def add_task(self, task): """Add a new task to this task.""" self._tasks.append(task) return task def prepend_task(self, task): """Add a task at the front of the list.""" self._tasks.insert(0, task) return task def remove_task(self, task): self._tasks.remove(task) def replace_task(self, old_task, new_task): """Replace old_task with new_task, in the same position.""" for i in range(len(self._tasks)): if self._tasks[i] is old_task: self._tasks[i] = new_task return new_task def get_tasks(self): return self._tasks[:] def matches(self, path): """Return true if *all* tasks match the given path.""" for task in self._tasks: if not task.matches(path): return False return bool(self._tasks) def has_run(self): return self._has_run def may_run(self, path): """Verify if any task would certainly raise an error if run. This will call the C{may_run()} method on each task and return false if any of them returns false. """ for task in self._tasks: if not task.may_run(path): return False return True def run(self, path): """Run all tasks with the given action. @param path: The path of the expression run. Running an event means running all of its tasks individually and in order. An event should only ever be run if all of its tasks claim to match the given action. The result of this method will be the last result of a task which isn't None, or None if they're all None. """ self._has_run = True result = None errors = [] for task in self._tasks: if not errors or not task.may_run_user_code(): try: task_result = task.run(path) except AssertionError as e: error = str(e) if not error: raise RuntimeError("Empty error message from %r" % task) errors.append(error) else: # XXX That's actually a bit weird. What if a call() really # returned None? This would improperly change the semantic # of this process without any good reason. Test that with two # call()s in sequence. if task_result is not None: result = task_result if errors: message = [str(self.path)] if str(path) != message[0]: message.append("- Run: %s" % path) for error in errors: lines = error.splitlines() message.append("- " + lines.pop(0)) message.extend([" " + line for line in lines]) raise AssertionError(os.linesep.join(message)) return result def satisfied(self): """Return true if all tasks are satisfied. Being satisfied means that there are no unmet expectations. """ for task in self._tasks: try: task.verify() except AssertionError: return False return True def verify(self): """Run verify on all tasks. The verify method is supposed to raise an AssertionError if the task has unmet expectations, with a one-line explanation about why this item is unmet. This method should be safe to be called multiple times without side effects. """ errors = [] for task in self._tasks: try: task.verify() except AssertionError as e: error = str(e) if not error: raise RuntimeError("Empty error message from %r" % task) errors.append(error) if errors: message = [str(self.path)] for error in errors: lines = error.splitlines() message.append("- " + lines.pop(0)) message.extend([" " + line for line in lines]) raise AssertionError(os.linesep.join(message)) def replay(self): """Put all tasks in replay mode.""" self._has_run = False for task in self._tasks: task.replay() def restore(self): """Restore the state of all tasks.""" for task in self._tasks: task.restore() class ReplayRestoreEvent(Event): """Helper event for tasks which need replay/restore but shouldn't match.""" def matches(self, path): return False class Task(object): """Element used to track one specific aspect on an event. A task is responsible for adding any kind of logic to an event. Examples of that are counting the number of times the event was made, verifying parameters if any, and so on. """ def matches(self, path): """Return true if the task is supposed to be run for the given path. """ return True def may_run(self, path): """Return false if running this task would certainly raise an error.""" return True def may_run_user_code(self): """Return true if there's a chance this task may run custom code. Whenever errors are detected, running user code should be avoided, because the situation is already known to be incorrect, and any errors in the user code are side effects rather than the cause. """ return False def run(self, path): """Perform the task item, considering that the given action happened. """ def verify(self): """Raise AssertionError if expectations for this item are unmet. The verify method is supposed to raise an AssertionError if the task has unmet expectations, with a one-line explanation about why this item is unmet. This method should be safe to be called multiple times without side effects. """ def replay(self): """Put the task in replay mode. Any expectations of the task should be reset. """ def restore(self): """Restore any environmental changes made by the task. Verify should continue to work after this is called. """ # -------------------------------------------------------------------- # Task implementations. class OnRestoreCaller(Task): """Call a given callback when restoring.""" def __init__(self, callback): self._callback = callback def restore(self): self._callback() class PathMatcher(Task): """Match the action path against a given path.""" def __init__(self, path): self.path = path def matches(self, path): return self.path.matches(path) def path_matcher_recorder(mocker, event): event.add_task(PathMatcher(event.path)) Mocker.add_recorder(path_matcher_recorder) class RunCounter(Task): """Task which verifies if the number of runs are within given boundaries. """ def __init__(self, min, max=False): self.min = min if max is None: self.max = sys.maxint elif max is False: self.max = min else: self.max = max self._runs = 0 def replay(self): self._runs = 0 def may_run(self, path): return self._runs < self.max def run(self, path): self._runs += 1 if self._runs > self.max: self.verify() def verify(self): if not self.min <= self._runs <= self.max: if self._runs < self.min: raise AssertionError("Performed fewer times than expected.") raise AssertionError("Performed more times than expected.") class ImplicitRunCounter(RunCounter): """RunCounter inserted by default on any event. This is a way to differentiate explicitly added counters and implicit ones. """ def run_counter_recorder(mocker, event): """Any event may be repeated once, unless disabled by default.""" if event.path.root_mock.__mocker_count__: # Rather than appending the task, we prepend it so that the # issue is raised before any other side-effects happen. event.prepend_task(ImplicitRunCounter(1)) Mocker.add_recorder(run_counter_recorder) def run_counter_removal_recorder(mocker, event): """ Events created by getattr actions which lead to other events may be repeated any number of times. For that, we remove implicit run counters of any getattr actions leading to the current one. """ parent_path = event.path.parent_path for event in mocker.get_events()[::-1]: if (event.path is parent_path and event.path.actions[-1].kind == "getattr"): for task in event.get_tasks(): if type(task) is ImplicitRunCounter: event.remove_task(task) Mocker.add_recorder(run_counter_removal_recorder) class MockReturner(Task): """Return a mock based on the action path.""" def __init__(self, mocker): self.mocker = mocker def run(self, path): return Mock(self.mocker, path) def mock_returner_recorder(mocker, event): """Events that lead to other events must return mock objects.""" parent_path = event.path.parent_path for event in mocker.get_events(): if event.path is parent_path: for task in event.get_tasks(): if isinstance(task, MockReturner): break else: event.add_task(MockReturner(mocker)) break Mocker.add_recorder(mock_returner_recorder) class FunctionRunner(Task): """Task that runs a function everything it's run. Arguments of the last action in the path are passed to the function, and the function result is also returned. """ def __init__(self, func, with_root_object=False): self._func = func self._with_root_object = with_root_object def may_run_user_code(self): return True def run(self, path): action = path.actions[-1] if self._with_root_object: return self._func(path.root_object, *action.args, **action.kwargs) else: return self._func(*action.args, **action.kwargs) class PathExecuter(Task): """Task that executes a path in the real object, and returns the result.""" def __init__(self, result_callback=None): self._result_callback = result_callback def get_result_callback(self): return self._result_callback def run(self, path): result = path.execute(path.root_object) if self._result_callback is not None: self._result_callback(result) return result class Orderer(Task): """Task to establish an order relation between two events. An orderer task will only match once all its dependencies have been run. """ def __init__(self, path): self.path = path self._run = False self._dependencies = [] def replay(self): self._run = False def has_run(self): return self._run def may_run(self, path): for dependency in self._dependencies: if not dependency.has_run(): return False return True def run(self, path): for dependency in self._dependencies: if not dependency.has_run(): raise AssertionError("Should be after: %s" % dependency.path) self._run = True def add_dependency(self, orderer): self._dependencies.append(orderer) def get_dependencies(self): return self._dependencies class SpecChecker(Task): """Task to check if arguments of the last action conform to a real method. """ def __init__(self, method): self._method = method self._unsupported = False if method: try: self._args, self._varargs, self._varkwargs, self._defaults = \ inspect.getargspec(method) except TypeError: self._unsupported = True else: if self._defaults is None: self._defaults = () if type(method) is type(self.run): self._args = self._args[1:] def get_method(self): return self._method def _raise(self, message): spec = inspect.formatargspec(self._args, self._varargs, self._varkwargs, self._defaults) raise AssertionError("Specification is %s%s: %s" % (self._method.__name__, spec, message)) def verify(self): if not self._method: raise AssertionError("Method not found in real specification") def may_run(self, path): try: self.run(path) except AssertionError: return False return True def run(self, path): if not self._method: raise AssertionError("Method not found in real specification") if self._unsupported: return # Can't check it. Happens with builtin functions. :-( action = path.actions[-1] obtained_len = len(action.args) obtained_kwargs = action.kwargs.copy() nodefaults_len = len(self._args) - len(self._defaults) for i, name in enumerate(self._args): if i < obtained_len and name in action.kwargs: self._raise("%r provided twice" % name) if (i >= obtained_len and i < nodefaults_len and name not in action.kwargs): self._raise("%r not provided" % name) obtained_kwargs.pop(name, None) if obtained_len > len(self._args) and not self._varargs: self._raise("too many args provided") if obtained_kwargs and not self._varkwargs: self._raise("unknown kwargs: %s" % ", ".join(obtained_kwargs)) def spec_checker_recorder(mocker, event): spec = event.path.root_mock.__mocker_spec__ if spec: actions = event.path.actions if len(actions) == 1: if actions[0].kind == "call": method = getattr(spec, "__call__", None) event.add_task(SpecChecker(method)) elif len(actions) == 2: if actions[0].kind == "getattr" and actions[1].kind == "call": method = getattr(spec, actions[0].args[0], None) event.add_task(SpecChecker(method)) Mocker.add_recorder(spec_checker_recorder) class ProxyReplacer(Task): """Task which installs and deinstalls proxy mocks. This task will replace a real object by a mock in all dictionaries found in the running interpreter via the garbage collecting system. """ def __init__(self, mock): self.mock = mock self.__mocker_replace__ = False def replay(self): global_replace(self.mock.__mocker_object__, self.mock) def restore(self): global_replace(self.mock, self.mock.__mocker_object__) def global_replace(remove, install): """Replace object 'remove' with object 'install' on all dictionaries.""" for referrer in gc.get_referrers(remove): if (type(referrer) is dict and referrer.get("__mocker_replace__", True)): for key, value in list(referrer.items()): if value is remove: referrer[key] = install class Undefined(object): def __repr__(self): return "Undefined" Undefined = Undefined() class Patcher(Task): def __init__(self): super(Patcher, self).__init__() self._monitored = {} # {kind: {id(object): object}} self._patched = {} def is_monitoring(self, obj, kind): monitored = self._monitored.get(kind) if monitored: if id(obj) in monitored: return True cls = type(obj) if issubclass(cls, type): cls = obj bases = set([id(base) for base in cls.__mro__]) bases.intersection_update(monitored) return bool(bases) return False def monitor(self, obj, kind): if kind not in self._monitored: self._monitored[kind] = {} self._monitored[kind][id(obj)] = obj def patch_attr(self, obj, attr, value): original = obj.__dict__.get(attr, Undefined) self._patched[id(obj), attr] = obj, attr, original setattr(obj, attr, value) def get_unpatched_attr(self, obj, attr): cls = type(obj) if issubclass(cls, type): cls = obj result = Undefined for mro_cls in cls.__mro__: key = (id(mro_cls), attr) if key in self._patched: result = self._patched[key][2] if result is not Undefined: break elif attr in mro_cls.__dict__: result = mro_cls.__dict__.get(attr, Undefined) break if isinstance(result, object) and hasattr(type(result), "__get__"): if cls is obj: obj = None return result.__get__(obj, cls) return result def _get_kind_attr(self, kind): if kind == "getattr": return "__getattribute__" return "__%s__" % kind def replay(self): for kind in self._monitored: attr = self._get_kind_attr(kind) seen = set() for obj in self._monitored[kind].itervalues(): cls = type(obj) if issubclass(cls, type): cls = obj if cls not in seen: seen.add(cls) unpatched = getattr(cls, attr, Undefined) self.patch_attr(cls, attr, PatchedMethod(kind, unpatched, self.is_monitoring)) self.patch_attr(cls, "__mocker_execute__", self.execute) def restore(self): for obj, attr, original in self._patched.itervalues(): if original is Undefined: delattr(obj, attr) else: setattr(obj, attr, original) self._patched.clear() def execute(self, action, object): attr = self._get_kind_attr(action.kind) unpatched = self.get_unpatched_attr(object, attr) try: return unpatched(*action.args, **action.kwargs) except AttributeError: type, value, traceback = sys.exc_info() if action.kind == "getattr": # The normal behavior of Python is to try __getattribute__, # and if it raises AttributeError, try __getattr__. We've # tried the unpatched __getattribute__ above, and we'll now # try __getattr__. try: __getattr__ = unpatched("__getattr__") except AttributeError: pass else: return __getattr__(*action.args, **action.kwargs) raise (type, value, traceback) class PatchedMethod(object): def __init__(self, kind, unpatched, is_monitoring): self._kind = kind self._unpatched = unpatched self._is_monitoring = is_monitoring def __get__(self, obj, cls=None): object = obj or cls if not self._is_monitoring(object, self._kind): return self._unpatched.__get__(obj, cls) def method(*args, **kwargs): if self._kind == "getattr" and args[0].startswith("__mocker_"): return self._unpatched.__get__(obj, cls)(args[0]) mock = object.__mocker_mock__ return mock.__mocker_act__(self._kind, args, kwargs, object) return method def __call__(self, obj, *args, **kwargs): # At least with __getattribute__, Python seems to use *both* the # descriptor API and also call the class attribute directly. It # looks like an interpreter bug, or at least an undocumented # inconsistency. Coverage tests may show this uncovered, because # it depends on the Python version. return self.__get__(obj)(*args, **kwargs) def patcher_recorder(mocker, event): mock = event.path.root_mock if mock.__mocker_patcher__ and len(event.path.actions) == 1: patcher = mock.__mocker_patcher__ patcher.monitor(mock.__mocker_object__, event.path.actions[0].kind) Mocker.add_recorder(patcher_recorder) mapproxy-1.11.0/mapproxy/test/schemas/000077500000000000000000000000001320454472400177375ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/inspire/000077500000000000000000000000001320454472400214105ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/000077500000000000000000000000001320454472400227005ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/000077500000000000000000000000001320454472400231765ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/common.xsd000066400000000000000000001725431320454472400252220ustar00rootroot00000000000000 Mandatory if a URL is available to obtain more information on the resource, and/or access related services. If a resource is a spatial data set or spatial data set series, at least one keyword shall be provided from the general environmental multilingual thesaurus (GEMET) describing the relevant spatial data theme as defined in Annex I, II or III to Directive 2007/2/EC. Mandatory when there is a restriction on the spatial resolution for this service. Mandatory if the resource includes textual information. Mandatory if a URL is available to obtain more information on the resource, and/or access related services. If a resource is a spatial data set or spatial data set series, at least one keyword shall be provided from the general environmental multilingual thesaurus (GEMET) describing the relevant spatial data theme as defined in Annex I, II or III to Directive 2007/2/EC. Mandatory when there is a restriction on the spatial resolution for this service. Mandatory if the resource includes textual information. Mandatory if a URL is available to obtain more information on the resource, and/or access related services. If a resource is a spatial data set or spatial data set series, at least one keyword shall be provided from the general environmental multilingual thesaurus (GEMET) describing the relevant spatial data theme as defined in Annex I, II or III to Directive 2007/2/EC. Mandatory when there is a restriction on the spatial resolution for this service. Mandatory if the resource includes textual information. Mandatory if linkage to data sets on which the service operates are available. Mandatory if linkage to the service is available If the resource is a spatial data service, at least one keyword from Part D.4 shall be provided. Mandatory for services with an explicit geographic extent. Mandatory when there is a restriction on the spatial resolution for this service. The element must have values. If no conditions apply to the access and use of the resource, ‘no conditions apply’ shall be used. If conditions are unknown, ‘conditions unknown’ shall be used. Mandatory if linkage to data sets on which the service operates are available. One or more Documentation URIs if available One or more URLs if available One or more Documentation URIs if available One or more URLs if available One or more Documentation URIs if available One or more URLs if available The INSPIRE Metadata Regulation for spatial data services requires that a mandatory keyword which comes from a list and not an originating controlled vocabulary. This is also what the INSPIRE Metadata Technical Guidelines implement Draft implementation to be refined Official mime type for INSPIRE Resources Official mime type for OGC GML currently in the registration process of IANA Unofficial mime type for OGC WMS Unofficial mime type for OGC CSW Unofficial mime type for OGC CSW Capabilities response document Unofficial mime type for OGC CSW Capabilities response document Unofficial mime type for OGC CSW Capabilities response document Unofficial mime type for OGC WFS Unofficial mime type for OGC Service Exception Unofficial mime type for ISO 19139 metadata Mandatory if a URL is available to obtain more information on the resource, and/or access related services. Simplified ISO 8601 implementation YEAR MONTH DAY-----------------------TIME--------------------FRACTIONAL SECONDS---------TIME ZONE Mandatory for services with an explicit geographic extent. Mandatory if an equivalent scale or a resolution distance is available Mandatory for services when there is a restriction on the spatial resolution for service An equivalent scale is generally expressed as an integer value expressing the scale denominator A resolution distance shall be expressed as a numerical value associated with a unit of length ISO 19139 does not offer encoding of spatial resolution for services and INSPIRE Guidelines recommend to put it inside the abstract element in unstructured format, making it virtually impossible to extract the spatial resolution from the rest of the abstract text. The original abstract text is copied for reference. Resource Provider (resourceProvider): Party that supplies the resource. 6.2. Custodian (custodian): Party that accepts accountability and responsibility for the data and ensures appropriate care and maintenance of the resource. 6.3. Owner (owner): Party that owns the resource. 6.4. User (user): Party who uses the resource. 6.5. Distributor (distributor): Party who distributes the resource. 6.6. Originator (originator): Party who created the resource 6.7. Point of Contact (pointOfContact): Party who can be contacted for acquiring knowledge about or acquisition of the resource. 6.8. Principal Investigator (principalInvestigator): Key party responsible for gathering information and conducting research. 6.9. Processor (processor): Party who has processed the data in a manner such that the resource has been modified. 6.10. Publisher (publisher): Party who published the resource. 6.11. Author (author): Party who authored the resource. The bounding box shall be expressed with westbound and eastbound longitudes, and southbound and northbound latitudes in decimal degrees, with a precision of at least two decimals. mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/000077500000000000000000000000001320454472400243255ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_bul.xsd000066400000000000000000000136121320454472400266560ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_cze.xsd000066400000000000000000000117641320454472400266630ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_dan.xsd000066400000000000000000000115741320454472400266430ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_dut.xsd000066400000000000000000000120361320454472400266670ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_eng.xsd000066400000000000000000000145001320454472400266420ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_est.xsd000066400000000000000000000115211320454472400266640ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_fin.xsd000066400000000000000000000116121320454472400266460ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_fre.xsd000066400000000000000000000120771320454472400266540ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_ger.xsd000066400000000000000000000117361320454472400266560ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_gle.xsd000066400000000000000000000116531320454472400266460ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_gre.xsd000066400000000000000000000136411320454472400266530ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_hun.xsd000066400000000000000000000117521320454472400266710ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_ita.xsd000066400000000000000000000117441320454472400266550ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_lav.xsd000066400000000000000000000117461320454472400266640ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_lit.xsd000066400000000000000000000117501320454472400266650ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_mlt.xsd000066400000000000000000000115701320454472400266710ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_pol.xsd000066400000000000000000000120361320454472400266650ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_por.xsd000066400000000000000000000117531320454472400267000ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_rum.xsd000066400000000000000000000117541320454472400267040ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_slo.xsd000066400000000000000000000116751320454472400267000ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_slv.xsd000066400000000000000000000116361320454472400267040ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_spa.xsd000066400000000000000000000120711320454472400266550ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/enums/enum_swe.xsd000066400000000000000000000116401320454472400266710ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/inspire/common/1.0/network.xsd000066400000000000000000000667431320454472400254270ustar00rootroot00000000000000 Extended capabilities for ISO 19128 , OGC CSW, OGC OWS services Scenario 1: Mandatory MetadataUrl element pointing to an INSPIRE Compliant ISO metadata document plus language parameters Scenario 2: Mandatory (where appropriate) metadata elements not mapped to standard capabilities, plus mandatory language parameters, plus OPTIONAL MetadataUrl pointing to an INSPIRE Compliant ISO metadata document Mandatory linkage to the network service If the resource is a spatial data service, at least one keyword from Part D.4 shall be provided. It is not necessary to repeat the default language Mandatory if linkage to the service is available If the resource is a spatial data service, at least one keyword from Part D.4 shall be provided. Mandatory for services with an explicit geographic extent. Mandatory when there is a restriction on the spatial resolution for this service. The element must have values. If no conditions apply to the access and use of the resource, ‘no conditions apply’ shall be used. If conditions are unknown, ‘conditions unknown’ shall be used. Mandatory if linkage to data sets on which the service operates are available. Harmonised name of the layer Layer Title List of Coordinate Reference Systems in which the layer is available Mandatory if linkage to the service is available If the resource is a spatial data service, at least one keyword from Part D.4 shall be provided. Mandatory for services with an explicit geographic extent. Mandatory when there is a restriction on the spatial resolution for this service. The element must have values. If no conditions apply to the access and use of the resource, ‘no conditions apply’ shall be used. If conditions are unknown, ‘conditions unknown’ shall be used. Mandatory if linkage to data sets on which the service operates are available. Mandatory if linkage to the service is available If the resource is a spatial data service, at least one keyword from Part D.4 shall be provided. Mandatory for services with an explicit geographic extent. Mandatory when there is a restriction on the spatial resolution for this service. The element must have values. If no conditions apply to the access and use of the resource, ‘no conditions apply’ shall be used. If conditions are unknown, ‘conditions unknown’ shall be used. Mandatory if linkage to data sets on which the service operates are available. Mandatory if linkage to the service is available If the resource is a spatial data service, at least one keyword from Part D.4 shall be provided. Mandatory for services with an explicit geographic extent. Mandatory when there is a restriction on the spatial resolution for this service. The element must have values. If no conditions apply to the access and use of the resource, ‘no conditions apply’ shall be used. If conditions are unknown, ‘conditions unknown’ shall be used. Mandatory if linkage to data sets on which the service operates are available. Mandatory if linkage to the service is available If the resource is a spatial data service, at least one keyword from Part D.4 shall be provided. Mandatory for services with an explicit geographic extent. Mandatory when there is a restriction on the spatial resolution for this service. The element must have values. If no conditions apply to the access and use of the resource, ‘no conditions apply’ shall be used. If conditions are unknown, ‘conditions unknown’ shall be used. Mandatory if linkage to data sets on which the service operates are available. Mandatory if linkage to the service is available If the resource is a spatial data service, at least one keyword from Part D.4 shall be provided. Mandatory for services with an explicit geographic extent. Mandatory when there is a restriction on the spatial resolution for this service. The element must have values. If no conditions apply to the access and use of the resource, ‘no conditions apply’ shall be used. If conditions are unknown, ‘conditions unknown’ shall be used. Mandatory if linkage to data sets on which the service operates are available. mapproxy-1.11.0/mapproxy/test/schemas/inspire/inspire_vs/000077500000000000000000000000001320454472400235715ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/inspire/inspire_vs/1.0/000077500000000000000000000000001320454472400240675ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/inspire/inspire_vs/1.0/inspire_vs.xsd000066400000000000000000000030311320454472400267650ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/kml/000077500000000000000000000000001320454472400205225ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/kml/2.2.0/000077500000000000000000000000001320454472400211615ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/kml/2.2.0/ReadMe.txt000066400000000000000000000007161320454472400230630ustar00rootroot00000000000000OGC(r) KML 2.2.0 - ReadMe.txt OGC KML standard found in document OGC 07-147r2 at http://www.opengeospatial.org/standards/kml ----------------------------------------------------------------------- Policies, Procedures, Terms, and Conditions of OGC(r) are available http://www.opengeospatial.org/ogc/legal/ . Copyright (c) 2008 Open Geospatial Consortium, Inc. All Rights Reserved. ----------------------------------------------------------------------- mapproxy-1.11.0/mapproxy/test/schemas/kml/2.2.0/atom-author-link.xsd000066400000000000000000000036651320454472400251060ustar00rootroot00000000000000 atom-author-link.xsd 2008-01-23 There is no official atom XSD. This XSD is created based on: http://atompub.org/2005/08/17/atom.rnc. A subset of Atom as used in the ogckml22.xsd is defined here. mapproxy-1.11.0/mapproxy/test/schemas/kml/2.2.0/ogckml22.xsd000066400000000000000000002013701320454472400233240ustar00rootroot00000000000000 ogckml22.xsd 2008-01-23 XML Schema Document for OGC KML version 2.2. Copyright (c) 2008 Open Geospatial Consortium, Inc. All Rights Reserved. not anyURI due to $[x] substitution in PhotoOverlay Snippet deprecated in 2.2 Metadata deprecated in 2.2 Metadata deprecated in 2.2 MetadataType deprecated in 2.2 is the root element. ]]> Url deprecated in 2.2 Url deprecated in 2.2 color deprecated in 2.1 mapproxy-1.11.0/mapproxy/test/schemas/kml/2.2.0/xAL.xsd000066400000000000000000002214601320454472400223720ustar00rootroot00000000000000 xAL: eXtensible Address Language This is an XML document type definition (DTD) for defining addresses. Original Date of Creation: 1 March 2001 Copyright(c) 2000, OASIS. All Rights Reserved [http://www.oasis-open.org] Contact: Customer Information Quality Technical Committee, OASIS http://www.oasis-open.org/committees/ciq VERSION: 2.0 [MAJOR RELEASE] Date of Creation: 01 May 2002 Last Update: 24 July 2002 Previous Version: 1.3 Common Attributes:Type - If not documented then it means, possible values of Type not limited to: Official, Unique, Abbreviation, OldName, Synonym Code:Address element codes are used by groups like postal groups like ECCMA, ADIS, UN/PROLIST for postal services Used by postal services to encode the name of the element. Root element for a list of addresses Specific to DTD to specify the version number of DTD This container defines the details of the address. Can define multiple addresses including tracking address history Postal authorities use specific postal service data to expedient delivery of mail A unique identifier of an address assigned by postal authorities. Example: DPID in Australia Type of identifier. eg. DPID as in Australia Directly affects postal service distribution Specific to postal service Required for some postal services Specific to postal service Required for some postal services Specific to postal service Used for sorting addresses. Values may for example be CEDEX 16 (France) Specific to postal service Latitude of delivery address Specific to postal service Latitude direction of delivery address;N = North and S = South Specific to postal service Longtitude of delivery address Specific to postal service Longtitude direction of delivery address;N=North and S=South Specific to postal service any postal service elements not covered by the container can be represented using this element Specific to postal service USPS, ECMA, UN/PROLIST, etc Use the most suitable option. Country contains the most detailed information while Locality is missing Country and AdminArea Address as one line of free text Postal, residential, corporate, etc Container for Address lines Specification of a country A country code according to the specified scheme Country code scheme possible values, but not limited to: iso.3166-2, iso.3166-3 for two and three character country codes. Type of address. Example: Postal, residential,business, primary, secondary, etc Moved, Living, Investment, Deceased, etc.. Start Date of the validity of address End date of the validity of address Communication, Contact, etc. Key identifier for the element for not reinforced references from other elements. Not required to be unique for the document to be valid, but application may get confused if not unique. Extend this schema adding unique contraint if needed. Occurrence of the building name before/after the type. eg. EGIS BUILDING where name appears before type Name of the dependent locality Number of the dependent locality. Some areas are numbered. Eg. SECTOR 5 in a Suburb as in India or SOI SUKUMVIT 10 as in Thailand Eg. SECTOR occurs before 5 in SECTOR 5 Specification of a large mail user address. Examples of large mail users are postal companies, companies in France with a cedex number, hospitals and airports with their own post code. Large mail user addresses do not have a street name with premise name or premise number in countries like Netherlands. But they have a POBox and street also in countries like France A Postal van is specific for a route as in Is`rael, Rural route Dependent localities are Districts within cities/towns, locality divisions, postal divisions of cities, suburbs, etc. DependentLocality is a recursive element, but no nesting deeper than two exists (Locality-DependentLocality-DependentLocality). City or IndustrialEstate, etc Postal or Political - Sometimes locations must be distinguished between postal system, and physical locations as defined by a political system "VIA" as in Hill Top VIA Parish where Parish is a locality and Hill Top is a dependent locality Eg. Erode (Dist) where (Dist) is the Indicator Name of the firm A MailStop is where the the mail is delivered to within a premise/subpremise/firm or a facility. Name of the large mail user. eg. Smith Ford International airport Airport, Hospital, etc Specification of the identification number of a large mail user. An example are the Cedex codes in France. CEDEX Code eg. Building 429 in which Building is the Indicator Name of the building Name of the the Mail Stop. eg. MSP, MS, etc Number of the Mail stop. eg. 123 in MS 123 "-" in MS-123 Name of the Postal Route Number of the Postal Route Name of the SubPremise EGIS Building where EGIS occurs before Building Name of the SubPremise Location. eg. LOBBY, BASEMENT, GROUND FLOOR, etc... Specification of the identifier of a sub-premise. Examples of sub-premises are apartments and suites. sub-premises in a building are often uniquely identified by means of consecutive identifiers. The identifier can be a number, a letter or any combination of the two. In the latter case, the identifier includes exactly one variable (range) part, which is either a number or a single letter that is surrounded by fixed parts at the left (prefix) or the right (postfix). "TH" in 12TH which is a floor number, "NO." in NO.1, "#" in APT #12, etc. "No." occurs before 1 in No.1, or TH occurs after 12 in 12TH 12TH occurs "before" FLOOR (a type of subpremise) in 12TH FLOOR "/" in 12/14 Archer Street where 12 is sub-premise number and 14 is premise number Prefix of the sub premise number. eg. A in A-12 A-12 where 12 is number and A is prefix and "-" is the separator Suffix of the sub premise number. eg. A in 12A 12-A where 12 is number and A is suffix and "-" is the separator Name of the building Specification of a firm, company, organization, etc. It can be specified as part of an address that contains a street or a postbox. It is therefore different from a large mail user address, which contains no street. A MailStop is where the the mail is delivered to within a premise/subpremise/firm or a facility. Specification of a single sub-premise. Examples of sub-premises are apartments and suites. Each sub-premise should be uniquely identifiable. SubPremiseType: Specification of the name of a sub-premise type. Possible values not limited to: Suite, Appartment, Floor, Unknown Multiple levels within a premise by recursively calling SubPremise Eg. Level 4, Suite 2, Block C Free format address representation. An address can have more than one line. The order of the AddressLine elements must be preserved. Defines the type of address line. eg. Street, Address Line 1, etc. Locality is one level lower than adminisstrative area. Eg.: cities, reservations and any other built-up areas. Name of the locality Specification of a large mail user address. Examples of large mail users are postal companies, companies in France with a cedex number, hospitals and airports with their own post code. Large mail user addresses do not have a street name with premise name or premise number in countries like Netherlands. But they have a POBox and street also in countries like France A Postal van is specific for a route as in Is`rael, Rural route Dependent localities are Districts within cities/towns, locality divisions, postal divisions of cities, suburbs, etc. DependentLocality is a recursive element, but no nesting deeper than two exists (Locality-DependentLocality-DependentLocality). Possible values not limited to: City, IndustrialEstate, etc Postal or Political - Sometimes locations must be distinguished between postal system, and physical locations as defined by a political system Erode (Dist) where (Dist) is the Indicator Specification of a thoroughfare. A thoroughfare could be a rd, street, canal, river, etc. Note dependentlocality in a street. For example, in some countries, a large street will have many subdivisions with numbers. Normally the subdivision name is the same as the road name, but with a number to identifiy it. Eg. SOI SUKUMVIT 3, SUKUMVIT RD, BANGKOK A container to represent a range of numbers (from x thru y)for a thoroughfare. eg. 1-2 Albert Av Starting number in the range Ending number in the range Thoroughfare number ranges are odd or even "No." No.12-13 "-" in 12-14 or "Thru" in 12 Thru 14 etc. No.12-14 where "No." is before actual street number 23-25 Archer St, where number appears before name North Baker Street, where North is the pre-direction. The direction appears before the name. Appears before the thoroughfare name. Ed. Spanish: Avenida Aurora, where Avenida is the leading type / French: Rue Moliere, where Rue is the leading type. Specification of the name of a Thoroughfare (also dependant street name): street name, canal name, etc. Appears after the thoroughfare name. Ed. British: Baker Lane, where Lane is the trailing type. 221-bis Baker Street North, where North is the post-direction. The post-direction appears after the name. DependentThroughfare is related to a street; occurs in GB, IE, ES, PT North Baker Street, where North is the pre-direction. The direction appears before the name. Appears before the thoroughfare name. Ed. Spanish: Avenida Aurora, where Avenida is the leading type / French: Rue Moliere, where Rue is the leading type. Specification of the name of a Thoroughfare (also dependant street name): street name, canal name, etc. Appears after the thoroughfare name. Ed. British: Baker Lane, where Lane is the trailing type. 221-bis Baker Street North, where North is the post-direction. The post-direction appears after the name. Dependent localities are Districts within cities/towns, locality divisions, postal divisions of cities, suburbs, etc. DependentLocality is a recursive element, but no nesting deeper than two exists (Locality-DependentLocality-DependentLocality). Specification of a firm, company, organization, etc. It can be specified as part of an address that contains a street or a postbox. It is therefore different from a large mail user address, which contains no street. Does this thoroughfare have a a dependent thoroughfare? Corner of street X, etc Corner of, Intersection of Corner of Street1 AND Street 2 where AND is the Connector STS in GEORGE and ADELAIDE STS, RDS IN A and B RDS, etc. Use only when both the street types are the same Examples of administrative areas are provinces counties, special regions (such as "Rijnmond"), etc. Name of the administrative area. eg. MI in USA, NSW in Australia Specification of a sub-administrative area. An example of a sub-administrative areas is a county. There are two places where the name of an administrative area can be specified and in this case, one becomes sub-administrative area. Name of the sub-administrative area Province or State or County or Kanton, etc Postal or Political - Sometimes locations must be distinguished between postal system, and physical locations as defined by a political system Erode (Dist) where (Dist) is the Indicator Province or State or County or Kanton, etc Postal or Political - Sometimes locations must be distinguished between postal system, and physical locations as defined by a political system Erode (Dist) where (Dist) is the Indicator Specification of a post office. Examples are a rural post office where post is delivered and a post office containing post office boxes. Specification of the name of the post office. This can be a rural postoffice where post is delivered or a post office containing post office boxes. Specification of the number of the postoffice. Common in rural postoffices MS in MS 62, # in MS # 12, etc. MS occurs before 62 in MS 62 A Postal van is specific for a route as in Is`rael, Rural route Could be a Mobile Postoffice Van as in Isreal eg. Kottivakkam (P.O) here (P.O) is the Indicator PostalCode is the container element for either simple or complex (extended) postal codes. Type: Area Code, Postcode, etc. Specification of a postcode. The postcode is formatted according to country-specific rules. Example: SW3 0A8-1A, 600074, 2067 Old Postal Code, new code, etc Examples are: 1234 (USA), 1G (UK), etc. Delivery Point Suffix, New Postal Code, etc.. The separator between postal code number and the extension. Eg. "-" A post town is not the same as a locality. A post town can encompass a collection of (small) localities. It can also be a subpart of a locality. An actual post town in Norway is "Bergen". Name of the post town GENERAL PO in MIAMI GENERAL PO eg. village, town, suburb, etc Area Code, Postcode, Delivery code as in NZ, etc Specification of a postbox like mail delivery point. Only a single postbox number can be specified. Examples of postboxes are POBox, free mail numbers, etc. Specification of the number of a postbox Specification of the prefix of the post box number. eg. A in POBox:A-123 A-12 where 12 is number and A is prefix and "-" is the separator Specification of the suffix of the post box number. eg. A in POBox:123A 12-A where 12 is number and A is suffix and "-" is the separator Some countries like USA have POBox as 12345-123 "-" is the NumberExtensionSeparator in POBOX:12345-123 Specification of a firm, company, organization, etc. It can be specified as part of an address that contains a street or a postbox. It is therefore different from a large mail user address, which contains no street. Possible values are, not limited to: POBox and Freepost. LOCKED BAG NO:1234 where the Indicator is NO: and Type is LOCKED BAG Subdivision in the firm: School of Physics at Victoria University (School of Physics is the department) Specification of the name of a department. A MailStop is where the the mail is delivered to within a premise/subpremise/firm or a facility. School in Physics School, Division in Radiology division of school of physics Specification of a single premise, for example a house or a building. The premise as a whole has a unique premise (house) number or a premise name. There could be more than one premise in a street referenced in an address. For example a building address near a major shopping centre or raiwlay station Specification of the name of the premise (house, building, park, farm, etc). A premise name is specified when the premise cannot be addressed using a street name plus premise (house) number. EGIS Building where EGIS occurs before Building, DES JARDINS occurs after COMPLEXE DES JARDINS LOBBY, BASEMENT, GROUND FLOOR, etc... Specification for defining the premise number range. Some premises have number as Building C1-C7 Start number details of the premise number range End number details of the premise number range Eg. Odd or even number range Eg. No. in Building No:C1-C5 "-" in 12-14 or "Thru" in 12 Thru 14 etc. No.12-14 where "No." is before actual street number Building 23-25 where the number occurs after building name Specification of the name of a building. Specification of a single sub-premise. Examples of sub-premises are apartments and suites. Each sub-premise should be uniquely identifiable. Specification of a firm, company, organization, etc. It can be specified as part of an address that contains a street or a postbox. It is therefore different from a large mail user address, which contains no street. A MailStop is where the the mail is delivered to within a premise/subpremise/firm or a facility. COMPLEXE in COMPLEX DES JARDINS, A building, station, etc STREET, PREMISE, SUBPREMISE, PARK, FARM, etc NEAR, ADJACENT TO, etc DES, DE, LA, LA, DU in RUE DU BOIS. These terms connect a premise/thoroughfare type and premise/thoroughfare name. Terms may appear with names AVE DU BOIS Prefix before the number. A in A12 Archer Street A-12 where 12 is number and A is prefix and "-" is the separator Suffix after the number. A in 12A Archer Street NEAR, ADJACENT TO, etc 12-A where 12 is number and A is suffix and "-" is the separator Eg.: 23 Archer street or 25/15 Zero Avenue, etc 12 Archer Street is "Single" and 12-14 Archer Street is "Range" No. in Street No.12 or "#" in Street # 12, etc. No.12 where "No." is before actual street number 23 Archer St, Archer Street 23, St Archer 23 Specification of the identifier of the premise (house, building, etc). Premises in a street are often uniquely identified by means of consecutive identifiers. The identifier can be a number, a letter or any combination of the two. Building 12-14 is "Range" and Building 12 is "Single" No. in House No.12, # in #12, etc. No. occurs before 12 No.12 12 in BUILDING 12 occurs "after" premise type BUILDING A in A12 A-12 where 12 is number and A is prefix and "-" is the separator A in 12A 12-A where 12 is number and A is suffix and "-" is the separator Specification of the name of a country. Old name, new name, etc mapproxy-1.11.0/mapproxy/test/schemas/ows/000077500000000000000000000000001320454472400205475ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/ows/1.1.0/000077500000000000000000000000001320454472400212045ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/ows/1.1.0/ReadMe.txt000066400000000000000000000064341320454472400231110ustar00rootroot00000000000000OpenGIS(r) OWS Common- ReadMe.txt =========================== OpenGIS(r) Web Service Common (OWS) Implementation Specification More information on the OGC OWS Common standard may be found at http://www.opengeospatial.org/standards/common The most current schema are available at http://schemas.opengis.net/ . The root (all-components) XML Schema Document, which includes directly and indirectly all the XML Schema Documents, defined by OWS 2.0 is owsAll.xsd . * Latest version is: http://schemas.opengis.net/ows/2.0/owsAll.xsd * ----------------------------------------------------------------------- 2011-02-07 Peter Schut * v1.1.0: The 1.1.0 version of owsExceptionReport.xsd has been corrected to reflect the corrigenda (OGC 07-141). The owsExceptionReport.xsd schema previously referenced an obsolete version of the XML schema. 2010-05-06 Jim Greenwood * v2.0.0: The 2.0.0 version are the XML Schema Documents for OGC document 06-121r9, approved as an Implementation Specification in May 2005. 2010-01-21 Kevin Stegemoller * update/verify copyright (06-135r7 s#3.2) * migrate relative to absolute URLs of schema imports (06-135r7 s#15) * updated xsd:schema:@version attribute (06-135r7 s#13.4) * add archives (.zip) files of previous versions * create/update ReadMe.txt (06-135r7 s#17) 2007-04-03 Arliss Whiteside * v1.1.0: OWS Common specification has been updated to version 1.1.0 (OGC 06-121r3). These very small changes are taken from corrigendum (OGC 07-016) which corrects the schemaLocation references in declarations for the namespace http://www.w3.org/1999/xlink, in the OWS Common 1.1 XML Schema. These schemaLocation references are changed to relatively reference the old schema location at http://www.opengis.net/xlink/1.0.0/xlinks.xsd . * Note: check each OGC numbered document for detailed changes. 2005-11-22 Arliss Whiteside * v1.0.0, v0.4.0, v0.3.2, v0.3.1, v0.3.0: All five of these sets of XML Schema Documents have been edited to reflect the corrigenda to all those OGC documents which are based on the change requests: OGC 05-068r1 "Store xlinks.xsd file at a fixed location" OGC 05-081r2 "Change to use relative paths" * v1.0.0: The 1.0.0 version are the XML Schema Documents for OGC document 05-008, approved as an Implementation Specification in May 2005. * v0.4.0: The 0.4.0 version are the XML Schema Documents for OGC document 04-016r5. * v0.3.2: The 0.3.2 version are the XML Schema Documents after correcting one small incorrect difference from OGC document 04-016r3. * v0.3.1: The 0.3.1 version are the XML Schema Documents attached to OGC document 04-016r3, containing that editing of document 04-016r2. This Recommendation Paper is available to the public at http://portal.opengis.org/files/?artifact_id=6324. * v0.3.0: OWS Common set of XML Schema Documents from OGC document 04-016r2 approved as Recommendation Paper in the April 2004 OGC meetings. ----------------------------------------------------------------------- Policies, Procedures, Terms, and Conditions of OGC(r) are available http://www.opengeospatial.org/ogc/legal/ . Copyright (c) 2010 Open Geospatial Consortium, Inc. All Rights Reserved. ----------------------------------------------------------------------- mapproxy-1.11.0/mapproxy/test/schemas/ows/1.1.0/ows19115subset.xsd000066400000000000000000000320071320454472400243650ustar00rootroot00000000000000 ows19115subset.xsd 2010-01-30 This XML Schema Document encodes the parts of ISO 19115 used by the common "ServiceIdentification" and "ServiceProvider" sections of the GetCapabilities operation response, known as the service metadata XML document. The parts encoded here are the MD_Keywords, CI_ResponsibleParty, and related classes. The UML package prefixes were omitted from XML names, and the XML element names were all capitalized, for consistency with other OWS Schemas. This document also provides a simple coding of text in multiple languages, simplified from Annex J of ISO 19115. OWS is an OGC Standard. Copyright (c) 2006,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/ . Text string with the language of the string identified as recommended in the XML 1.0 W3C Recommendation, section 2.12. Title of this resource, normally used for display to a human. Brief narrative description of this resource, normally used for display to a human. Unordered list of one or more commonly used or formalised word(s) or phrase(s) used to describe the subject. When needed, the optional "type" can name the type of the associated list of keywords that shall all have the same type. Also when needed, the codeSpace attribute of that "type" can reference the type name authority and/or thesaurus. If the xml:lang attribute is not included in a Keyword element, then no language is specified for that element unless specified by another means. All Keyword elements in the same Keywords element that share the same xml:lang attribute value represent different keywords in that language. For OWS use, the optional thesaurusName element was omitted as being complex information that could be referenced by the codeSpace attribute of the Type element. Name or code with an (optional) authority. If the codeSpace attribute is present, its value shall reference a dictionary, thesaurus, or authority for the name or code, such as the organisation who assigned the value, or the dictionary from which it is taken. Type copied from basicTypes.xsd of GML 3 with documentation edited, for possible use outside the ServiceIdentification section of a service metadata document. Identification of, and means of communication with, person(s) responsible for the resource(s). For OWS use in the ServiceProvider section of a service metadata document, the optional organizationName element was removed, since this type is always used with the ProviderName element which provides that information. The optional individualName element was made mandatory, since either the organizationName or individualName element is mandatory. The mandatory "role" element was changed to optional, since no clear use of this information is known in the ServiceProvider section. Identification of, and means of communication with, person responsible for the server. At least one of IndividualName, OrganisationName, or PositionName shall be included. Identification of, and means of communication with, person responsible for the server. For OWS use in the ServiceProvider section of a service metadata document, the optional organizationName element was removed, since this type is always used with the ProviderName element which provides that information. The mandatory "role" element was changed to optional, since no clear use of this information is known in the ServiceProvider section. Name of the responsible person: surname, given name, title separated by a delimiter. Name of the responsible organization. Role or position of the responsible person. Function performed by the responsible party. Possible values of this Role shall include the values and the meanings listed in Subclause B.5.5 of ISO 19115:2003. Address of the responsible party. Information required to enable contact with the responsible person and/or organization. For OWS use in the service metadata document, the optional hoursOfService and contactInstructions elements were retained, as possibly being useful in the ServiceProvider section. Telephone numbers at which the organization or individual may be contacted. Physical and email address at which the organization or individual may be contacted. On-line information that can be used to contact the individual or organization. OWS specifics: The xlink:href attribute in the xlink:simpleLink attribute group shall be used to reference this resource. Whenever practical, the xlink:href attribute with type anyURI should be a URL from which more contact information can be electronically retrieved. The xlink:title attribute with type "string" can be used to name this set of information. The other attributes in the xlink:simpleLink attribute group should not be used. Time period (including time zone) when individuals can contact the organization or individual. Supplemental instructions on how or when to contact the individual or organization. Reference to on-line resource from which data can be obtained. For OWS use in the service metadata document, the CI_OnlineResource class was XML encoded as the attributeGroup "xlink:simpleLink", as used in GML. Telephone numbers for contacting the responsible individual or organization. Telephone number by which individuals can speak to the responsible organization or individual. Telephone number of a facsimile machine for the responsible organization or individual. Location of the responsible individual or organization. Address line for the location. City of the location. State or province of the location. ZIP or other postal code. Country of the physical address. Address of the electronic mailbox of the responsible organization or individual. mapproxy-1.11.0/mapproxy/test/schemas/ows/1.1.0/owsAll.xsd000066400000000000000000000021411320454472400231630ustar00rootroot00000000000000 owsAll.xsd 2010-01-30 This XML Schema Document includes and imports, directly and indirectly, all the XML Schemas defined by the OWS Common Implemetation Specification. OWS is an OGC Standard. Copyright (c) 2006,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/ . mapproxy-1.11.0/mapproxy/test/schemas/ows/1.1.0/owsCommon.xsd000066400000000000000000000272461320454472400237200ustar00rootroot00000000000000 owsCommon.xsd 2010-01-30 This XML Schema Document encodes various parameters and parameter types that can be used in OWS operation requests and responses. OWS is an OGC Standard. Copyright (c) 2006,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/ . XML encoded identifier of a standard MIME type, possibly a parameterized MIME type. Specification version for OWS operation. The string value shall contain one x.y.z "version" value (e.g., "2.1.3"). A version number shall contain three non-negative integers separated by decimal points, in the form "x.y.z". The integers y and z shall not exceed 99. Each version shall be for the Implementation Specification (document) and the associated XML Schemas to which requested operations will conform. An Implementation Specification version normally specifies XML Schemas against which an XML encoded operation response must conform and should be validated. See Version negotiation subclause for more information. This element either references or contains more metadata about the element that includes this element. To reference metadata stored remotely, at least the xlinks:href attribute in xlink:simpleLink shall be included. Either at least one of the attributes in xlink:simpleLink or a substitute for the AbstractMetaData element shall be included, but not both. An Implementation Specification can restrict the contents of this element to always be a reference or always contain metadata. (Informative: This element was adapted from the metaDataProperty element in GML 3.0.) Reference to metadata recorded elsewhere, either external to this XML document or within it. Whenever practical, the xlink:href attribute with type anyURI should include a URL from which this metadata can be electronically retrieved. Optional reference to the aspect of the element which includes this "metadata" element that this metadata provides more information about. Abstract element containing more metadata about the element that includes the containing "metadata" element. A specific server implementation, or an Implementation Specification, can define concrete elements in the AbstractMetaData substitution group. XML encoded minimum rectangular bounding box (or region) parameter, surrounding all the associated data. This type is adapted from the EnvelopeType of GML 3.1, with modified contents and documentation for encoding a MINIMUM size box SURROUNDING all associated data. Position of the bounding box corner at which the value of each coordinate normally is the algebraic minimum within this bounding box. In some cases, this position is normally displayed at the top, such as the top left for some image coordinates. For more information, see Subclauses 10.2.5 and C.13. Position of the bounding box corner at which the value of each coordinate normally is the algebraic maximum within this bounding box. In some cases, this position is normally displayed at the bottom, such as the bottom right for some image coordinates. For more information, see Subclauses 10.2.5 and C.13. Usually references the definition of a CRS, as specified in [OGC Topic 2]. Such a CRS definition can be XML encoded using the gml:CoordinateReferenceSystemType in [GML 3.1]. For well known references, it is not required that a CRS definition exist at the location the URI points to. If no anyURI value is included, the applicable CRS must be either: a) Specified outside the bounding box, but inside a data structure that includes this bounding box, as specified for a specific OWS use of this bounding box type. b) Fixed and specified in the Implementation Specification for a specific OWS use of the bounding box type. The number of dimensions in this CRS (the length of a coordinate sequence in this use of the PositionType). This number is specified by the CRS definition, but can also be specified here. Position instances hold the coordinates of a position in a coordinate reference system (CRS) referenced by the related "crs" attribute or elsewhere. For an angular coordinate axis that is physically continuous for multiple revolutions, but whose recorded values can be discontinuous, special conditions apply when the bounding box is continuous across the value discontinuity: a) If the bounding box is continuous clear around this angular axis, then ordinate values of minus and plus infinity shall be used. b) If the bounding box is continuous across the value discontinuity but is not continuous clear around this angular axis, then some non-normal value can be used if specified for a specific OWS use of the BoundingBoxType. For more information, see Subclauses 10.2.5 and C.13. This type is adapted from DirectPositionType and doubleList of GML 3.1. The adaptations include omission of all the attributes, since the needed information is included in the BoundingBoxType. XML encoded minimum rectangular bounding box (or region) parameter, surrounding all the associated data. This box is specialized for use with the 2D WGS 84 coordinate reference system with decimal values of longitude and latitude. This type is adapted from the general BoundingBoxType, with modified contents and documentation for use with the 2D WGS 84 coordinate reference system. Position of the bounding box corner at which the values of longitude and latitude normally are the algebraic minimums within this bounding box. For more information, see Subclauses 10.4.5 and C.13. Position of the bounding box corner at which the values of longitude and latitude normally are the algebraic minimums within this bounding box. For more information, see Subclauses 10.4.5 and C.13. This attribute can be included when considered useful. When included, this attribute shall reference the 2D WGS 84 coordinate reference system with longitude before latitude and decimal values of longitude and latitude. The number of dimensions in this CRS (the length of a coordinate sequence in this use of the PositionType). This number is specified by the CRS definition, but can also be specified here. Two-dimensional position instances hold the longitude and latitude coordinates of a position in the 2D WGS 84 coordinate reference system. The longitude value shall be listed first, followed by the latitude value, both in decimal degrees. Latitude values shall range from -90 to +90 degrees, and longitude values shall normally range from -180 to +180 degrees. For the longitude axis, special conditions apply when the bounding box is continuous across the +/- 180 degrees meridian longitude value discontinuity: a) If the bounding box is continuous clear around the Earth, then longitude values of minus and plus infinity shall be used. b) If the bounding box is continuous across the value discontinuity but is not continuous clear around the Earth, then some non-normal value can be used if specified for a specific OWS use of the WGS84BoundingBoxType. For more information, see Subclauses 10.4.5 and C.13. mapproxy-1.11.0/mapproxy/test/schemas/ows/1.1.0/owsContents.xsd000066400000000000000000000155231320454472400242600ustar00rootroot00000000000000 owsContents.xsd 2010-01-30 This XML Schema Document encodes the typical Contents section of an OWS service metadata (Capabilities) document. This Schema can be built upon to define the Contents section for a specific OWS. If the ContentsBaseType in this XML Schema cannot be restricted and extended to define the Contents section for a specific OWS, all other relevant parts defined in owsContents.xsd shall be used by the "ContentsType" in the wxsContents.xsd prepared for the specific OWS. OWS is an OGC Standard. Copyright (c) 2006,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/ . Contents of typical Contents section of an OWS service metadata (Capabilities) document. This type shall be extended and/or restricted if needed for specific OWS use to include the specific metadata needed. Unordered set of summary descriptions for the datasets available from this OWS server. This set shall be included unless another source is referenced and all this metadata is available from that source. Unordered set of references to other sources of metadata describing the coverage offerings available from this server. Reference to a source of metadata describing coverage offerings available from this server. This parameter can reference a catalogue server from which dataset metadata is available. This ability is expected to be used by servers with thousands or millions of datasets, for which searching a catalogue is more feasible than fetching a long Capabilities XML document. When no DatasetDescriptionSummaries are included, and one or more catalogue servers are referenced, this set of catalogues shall contain current metadata summaries for all the datasets currently available from this OWS server, with the metadata for each such dataset referencing this OWS server. Typical dataset metadata in typical Contents section of an OWS service metadata (Capabilities) document. This type shall be extended and/or restricted if needed for specific OWS use, to include the specific Dataset description metadata needed. Unordered list of zero or more minimum bounding rectangles surrounding coverage data, using the WGS 84 CRS with decimal degrees and longitude before latitude. If no WGS 84 bounding box is recorded for a coverage, any such bounding boxes recorded for a higher level in a hierarchy of datasets shall apply to this coverage. If WGS 84 bounding box(es) are recorded for a coverage, any such bounding boxes recorded for a higher level in a hierarchy of datasets shall be ignored. For each lowest-level coverage in a hierarchy, at least one applicable WGS84BoundingBox shall be either recorded or inherited, to simplify searching for datasets that might overlap a specified region. If multiple WGS 84 bounding boxes are included, this shall be interpreted as the union of the areas of these bounding boxes. Unambiguous identifier or name of this coverage, unique for this server. Unordered list of zero or more minimum bounding rectangles surrounding coverage data, in AvailableCRSs. Zero or more BoundingBoxes are allowed in addition to one or more WGS84BoundingBoxes to allow more precise specification of the Dataset area in AvailableCRSs. These Bounding Boxes shall not use any CRS not listed as an AvailableCRS. However, an AvailableCRS can be listed without a corresponding Bounding Box. If no such bounding box is recorded for a coverage, any such bounding boxes recorded for a higher level in a hierarchy of datasets shall apply to this coverage. If such bounding box(es) are recorded for a coverage, any such bounding boxes recorded for a higher level in a hierarchy of datasets shall be ignored. If multiple bounding boxes are included with the same CRS, this shall be interpreted as the union of the areas of these bounding boxes. Optional unordered list of additional metadata about this dataset. A list of optional metadata elements for this dataset description could be specified in the Implementation Specification for this service. Metadata describing zero or more unordered subsidiary datasets available from this server. mapproxy-1.11.0/mapproxy/test/schemas/ows/1.1.0/owsDataIdentification.xsd000066400000000000000000000167751320454472400262200ustar00rootroot00000000000000 owsDataIdentification.xsd 2010-01-30 This XML Schema Document encodes the parts of the MD_DataIdentification class of ISO 19115 (OGC Abstract Specification Topic 11) which are expected to be used for most datasets. This Schema also encodes the parts of this class that are expected to be useful for other metadata. Both may be used within the Contents section of OWS service metadata (Capabilities) documents. OWS is an OGC Standard. Copyright (c) 2006,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/ . Human-readable descriptive information for the object it is included within. This type shall be extended if needed for specific OWS use to include additional metadata for each type of information. This type shall not be restricted for a specific OWS to change the multiplicity (or optionality) of some elements. If the xml:lang attribute is not included in a Title, Abstract or Keyword element, then no language is specified for that element unless specified by another means. All Title, Abstract and Keyword elements in the same Description that share the same xml:lang attribute value represent the description of the parent object in that language. Multiple Title or Abstract elements shall not exist in the same Description with the same xml:lang attribute value unless otherwise specified. Basic metadata identifying and describing a set of data. Optional unique identifier or name of this dataset. Optional unordered list of additional metadata about this data(set). A list of optional metadata elements for this data identification could be specified in the Implementation Specification for this service. Extended metadata identifying and describing a set of data. This type shall be extended if needed for each specific OWS to include additional metadata for each type of dataset. If needed, this type should first be restricted for each specific OWS to change the multiplicity (or optionality) of some elements. Unordered list of zero or more bounding boxes whose union describes the extent of this dataset. Unordered list of zero or more references to data formats supported for server outputs. Unordered list of zero or more available coordinate reference systems. Unique identifier or name of this dataset. Reference to a format in which this data can be encoded and transferred. More specific parameter names should be used by specific OWS specifications wherever applicable. More than one such parameter can be included for different purposes. Coordinate reference system in which data from this data(set) or resource is available or supported. More specific parameter names should be used by specific OWS specifications wherever applicable. More than one such parameter can be included for different purposes. Access constraint applied to assure the protection of privacy or intellectual property, or any other restrictions on retrieving or using data from or otherwise using this server. The reserved value NONE (case insensitive) shall be used to mean no access constraints are imposed. Fees and terms for retrieving data from or otherwise using this server, including the monetary units as specified in ISO 4217. The reserved value NONE (case insensitive) shall be used to mean no fees or terms. Identifier of a language used by the data(set) contents. This language identifier shall be as specified in IETF RFC 4646. When this element is omitted, the language used is not identified. mapproxy-1.11.0/mapproxy/test/schemas/ows/1.1.0/owsDomainType.xsd000066400000000000000000000340331320454472400245310ustar00rootroot00000000000000 owsDomainType.xsd 2010-01-30 This XML Schema Document encodes the allowed values (or domain) of a quantity, often for an input or output parameter to an OWS. Such a parameter is sometimes called a variable, quantity, literal, or typed literal. Such a parameter can use one of many data types, including double, integer, boolean, string, or URI. The allowed values can also be encoded for a quantity that is not explicit or not transferred, but is constrained by a server implementation. OWS is an OGC Standard. Copyright (c) 2006,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/ . Valid domain (or allowed set of values) of one quantity, with its name or identifier. Name or identifier of this quantity. Valid domain (or allowed set of values) of one quantity, with needed metadata but without a quantity name or identifier. Optional default value for this quantity, which should be included when this quantity has a default value. Meaning metadata should be referenced or included for each quantity. This data type metadata should be referenced or included for each quantity. Unit of measure, which should be included when this set of PossibleValues has units or a more complete reference system. Optional unordered list of other metadata about this quantity. A list of required and optional other metadata elements for this quantity should be specified in the Implementation Specification for this service. Specifies the possible values of this quantity. Specifies that any value is allowed for this parameter. Specifies that no values are allowed for this parameter or quantity. Reference to externally specified list of all the valid values and/or ranges of values for this quantity. (Informative: This element was simplified from the metaDataProperty element in GML 3.0.) Human-readable name of the list of values provided by the referenced document. Can be empty string when this list has no name. Indicates that this quantity has units or a reference system, and identifies the unit or reference system used by the AllowedValues or ValuesReference. Identifier of unit of measure of this set of values. Should be included then this set of values has units (and not a more complete reference system). Identifier of reference system used by this set of values. Should be included then this set of values has a reference system (not just units). List of all the valid values and/or ranges of values for this quantity. For numeric quantities, signed values should be ordered from negative infinity to positive infinity. A single value, encoded as a string. This type can be used for one value, for a spacing between allowed values, or for the default value of a parameter. The default value for a quantity for which multiple values are allowed. A range of values of a numeric parameter. This range can be continuous or discrete, defined by a fixed spacing between adjacent valid values. If the MinimumValue or MaximumValue is not included, there is no value limit in that direction. Inclusion of the specified minimum and maximum values in the range shall be defined by the rangeClosure. Shall be included when the allowed values are NOT continuous in this range. Shall not be included when the allowed values are continuous in this range. Shall be included unless the default value applies. Minimum value of this numeric parameter. Maximum value of this numeric parameter. The regular distance or spacing between the allowed values in a range. Specifies which of the minimum and maximum values are included in the range. Note that plus and minus infinity are considered closed bounds. The specified minimum and maximum values are included in this range. The specified minimum and maximum values are NOT included in this range. The specified minimum value is NOT included in this range, and the specified maximum value IS included in this range. The specified minimum value IS included in this range, and the specified maximum value is NOT included in this range. References metadata about a quantity, and provides a name for this metadata. (Informative: This element was simplified from the metaDataProperty element in GML 3.0.) Human-readable name of the metadata described by associated referenced document. Reference to data or metadata recorded elsewhere, either external to this XML document or within it. Whenever practical, this attribute should be a URL from which this metadata can be electronically retrieved. Alternately, this attribute can reference a URN for well-known metadata. For example, such a URN could be a URN defined in the "ogc" URN namespace. Definition of the meaning or semantics of this set of values. This Meaning can provide more specific, complete, precise, machine accessible, and machine understandable semantics about this quantity, relative to other available semantic information. For example, other semantic information is often provided in "documentation" elements in XML Schemas or "description" elements in GML objects. Definition of the data type of this set of values. In this case, the xlink:href attribute can reference a URN for a well-known data type. For example, such a URN could be a data type identification URN defined in the "ogc" URN namespace. Definition of the reference system used by this set of values, including the unit of measure whenever applicable (as is normal). In this case, the xlink:href attribute can reference a URN for a well-known reference system, such as for a coordinate reference system (CRS). For example, such a URN could be a CRS identification URN defined in the "ogc" URN namespace. Definition of the unit of measure of this set of values. In this case, the xlink:href attribute can reference a URN for a well-known unit of measure (uom). For example, such a URN could be a UOM identification URN defined in the "ogc" URN namespace. mapproxy-1.11.0/mapproxy/test/schemas/ows/1.1.0/owsExceptionReport.xsd000066400000000000000000000114341320454472400256120ustar00rootroot00000000000000 owsExceptionReport.xsd 2011-02-07 This XML Schema Document encodes the Exception Report response to all OWS operations. OWS is an OGC Standard. Copyright (c) 2006,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/ . Report message returned to the client that requested any OWS operation when the server detects an error while processing that operation request. Unordered list of one or more Exception elements that each describes an error. These Exception elements shall be interpreted by clients as being independent of one another (not hierarchical). Specification version for OWS operation. The string value shall contain one x.y.z "version" value (e.g., "2.1.3"). A version number shall contain three non-negative integers separated by decimal points, in the form "x.y.z". The integers y and z shall not exceed 99. Each version shall be for the Implementation Specification (document) and the associated XML Schemas to which requested operations will conform. An Implementation Specification version normally specifies XML Schemas against which an XML encoded operation response must conform and should be validated. See Version negotiation subclause for more information. Identifier of the language used by all included exception text values. These language identifiers shall be as specified in IETF RFC 4646. When this attribute is omitted, the language used is not identified. An Exception element describes one detected error that a server chooses to convey to the client. Ordered sequence of text strings that describe this specific exception or error. The contents of these strings are left open to definition by each server implementation. A server is strongly encouraged to include at least one ExceptionText value, to provide more information about the detected error than provided by the exceptionCode. When included, multiple ExceptionText values shall provide hierarchical information about one detected error, with the most significant information listed first. A code representing the type of this exception, which shall be selected from a set of exceptionCode values specified for the specific service operation and server. When included, this locator shall indicate to the client where an exception was encountered in servicing the client's operation request. This locator should be included whenever meaningful information can be provided by the server. The contents of this locator will depend on the specific exceptionCode and OWS service, and shall be specified in the OWS Implementation Specification. mapproxy-1.11.0/mapproxy/test/schemas/ows/1.1.0/owsGetCapabilities.xsd000066400000000000000000000161411320454472400255110ustar00rootroot00000000000000 owsGetCapabilities.xsd 2010-01-30 This XML Schema Document defines the GetCapabilities operation request and response XML elements and types, which are common to all OWSs. This XML Schema shall be edited by each OWS, for example, to specify a specific value for the "service" attribute. OWS is an OGC Standard. Copyright (c) 2006,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/ . XML encoded GetCapabilities operation response. This document provides clients with service metadata about a specific service instance, usually including metadata about the tightly-coupled data served. If the server does not implement the updateSequence parameter, the server shall always return the complete Capabilities document, without the updateSequence parameter. When the server implements the updateSequence parameter and the GetCapabilities operation request included the updateSequence parameter with the current value, the server shall return this element with only the "version" and "updateSequence" attributes. Otherwise, all optional elements shall be included or not depending on the actual value of the Contents parameter in the GetCapabilities operation request. This base type shall be extended by each specific OWS to include the additional contents needed. Service metadata document version, having values that are "increased" whenever any change is made in service metadata document. Values are selected by each server, and are always opaque to clients. When not supported by server, server shall not return this attribute. XML encoded GetCapabilities operation request. This operation allows clients to retrieve service metadata about a specific service instance. In this XML encoding, no "request" parameter is included, since the element name specifies the specific operation. This base type shall be extended by each specific OWS to include the additional required "service" attribute, with the correct value for that OWS. When omitted, server shall return latest supported version. When omitted or not supported by server, server shall return complete service metadata (Capabilities) document. When omitted or not supported by server, server shall return service metadata document using the MIME type "text/xml". When omitted or not supported by server, server shall return latest complete service metadata document. Service type identifier, where the string value is the OWS type abbreviation, such as "WMS" or "WFS". Prioritized sequence of one or more specification versions accepted by client, with preferred versions listed first. See Version negotiation subclause for more information. Unordered list of zero or more names of requested sections in complete service metadata document. Each Section value shall contain an allowed section name as specified by each OWS specification. See Sections parameter subclause for more information. Service metadata document version, having values that are "increased" whenever any change is made in service metadata document. Values are selected by each server, and are always opaque to clients. See updateSequence parameter use subclause for more information. Prioritized sequence of zero or more GetCapabilities operation response formats desired by client, with preferred formats listed first. Each response format shall be identified by its MIME type. See AcceptFormats parameter use subclause for more information. mapproxy-1.11.0/mapproxy/test/schemas/ows/1.1.0/owsGetResourceByID.xsd000066400000000000000000000063371320454472400254250ustar00rootroot00000000000000 owsGetResourceByID.xsd 2010-01-30 This XML Schema Document encodes the GetResourceByID operation request message. This typical operation is specified as a base for profiling in specific OWS specifications. For information on the allowed changes and limitations in such profiling, see Subclause 9.4.1 of the OWS Common specification. OWS is an OGC Standard. Copyright (c) 2006,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/ . XML encoded GetResourceByID operation response. The complexType used by this element shall be specified by each specific OWS. Request to a service to perform the GetResourceByID operation. This operation allows a client to retrieve one or more identified resources, including datasets and resources that describe datasets or parameters. In this XML encoding, no "request" parameter is included, since the element name specifies the specific operation. Unordered list of zero or more resource identifiers. These identifiers can be listed in the Contents section of the service metadata (Capabilities) document. For more information on this parameter, see Subclause 9.4.2.1 of the OWS Common specification. Optional reference to the data format to be used for response to this operation request. This element shall be included when multiple output formats are available for the selected resource(s), and the client desires a format other than the specified default, if any. mapproxy-1.11.0/mapproxy/test/schemas/ows/1.1.0/owsInputOutputData.xsd000066400000000000000000000101361320454472400255700ustar00rootroot00000000000000 owsInputOutputData.xsd 2010-01-30 This XML Schema Document specifies types and elements for input and output of operation data, allowing including multiple data items with each data item either included or referenced. The contents of each type and element specified here can be restricted and/or extended for each use in a specific OWS specification. OWS is an OGC Standard. Copyright (c) 2006,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/ . Response from an OWS operation, allowing including multiple output data items with each item either included or referenced. This OperationResponse element, or an element using the ManifestType with a more specific element name, shall be used whenever applicable for responses from OWS operations. This element is specified for use where the ManifestType contents are needed for an operation response, but the Manifest element name is not fully applicable. This element or the ManifestType shall be used instead of using the ows:ReferenceType proposed in OGC 04-105. Input data in a XML-encoded OWS operation request, allowing including multiple data items with each data item either included or referenced. This InputData element, or an element using the ManifestType with a more-specific element name (TBR), shall be used whenever applicable within XML-encoded OWS operation requests. Complete reference to a remote resource that needs to be retrieved from an OWS using an XML-encoded operation request. This element shall be used, within an InputData or Manifest element that is used for input data, when that input data needs to be retrieved from another web service using a XML-encoded OWS operation request. This element shall not be used for local payload input data or for requesting the resource from a web server using HTTP Get. The XML-encoded operation request message to be sent to request this input data from another web server using HTTP Post. Reference to the XML-encoded operation request message to be sent to request this input data from another web server using HTTP Post. The referenced message shall be attached to the same message (using the cid scheme), or be accessible using a URL. mapproxy-1.11.0/mapproxy/test/schemas/ows/1.1.0/owsManifest.xsd000066400000000000000000000144701320454472400242310ustar00rootroot00000000000000 owsManifest.xsd 2010-01-30 This XML Schema Document specifies types and elements for document or resource references and for package manifests that contain multiple references. The contents of each type and element specified here can be restricted and/or extended for each use in a specific OWS specification. OWS is an OGC Standard. Copyright (c) 2006,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/ . Base for a reference to a remote or local resource. This type contains only a restricted and annotated set of the attributes from the xlink:simpleLink attributeGroup. Reference to a remote resource or local payload. A remote resource is typically addressed by a URL. For a local payload (such as a multipart mime message), the xlink:href must start with the prefix cid:. Reference to a resource that describes the role of this reference. When no value is supplied, no particular role value is to be inferred. Although allowed, this attribute is not expected to be useful in this application of xlink:simpleLink. Describes the meaning of the referenced resource in a human-readable fashion. Although allowed, this attribute is not expected to be useful in this application of xlink:simpleLink. Although allowed, this attribute is not expected to be useful in this application of xlink:simpleLink. Complete reference to a remote or local resource, allowing including metadata about that resource. Optional unique identifier of the referenced resource. The format of the referenced resource. This element is omitted when the mime type is indicated in the http header of the reference. Optional unordered list of additional metadata about this resource. A list of optional metadata elements for this ReferenceType could be specified in the Implementation Specification for each use of this type in a specific OWS. Logical group of one or more references to remote and/or local resources, allowing including metadata about that group. A Group can be used instead of a Manifest that can only contain one group. Unordered list of one or more groups of references to remote and/or local resources. mapproxy-1.11.0/mapproxy/test/schemas/ows/1.1.0/owsOperationsMetadata.xsd000066400000000000000000000213361320454472400262460ustar00rootroot00000000000000 owsOperationsMetadata.xsd 2010-01-30 This XML Schema Document encodes the basic contents of the "OperationsMetadata" section of the GetCapabilities operation response, also known as the Capabilities XML document. OWS is an OGC Standard. Copyright (c) 2006,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/ . Metadata about the operations and related abilities specified by this service and implemented by this server, including the URLs for operation requests. The basic contents of this section shall be the same for all OWS types, but individual services can add elements and/or change the optionality of optional elements. Metadata for unordered list of all the (requests for) operations that this server interface implements. The list of required and optional operations implemented shall be specified in the Implementation Specification for this service. Optional unordered list of parameter valid domains that each apply to one or more operations which this server interface implements. The list of required and optional parameter domain limitations shall be specified in the Implementation Specification for this service. Optional unordered list of valid domain constraints on non-parameter quantities that each apply to this server. The list of required and optional constraints shall be specified in the Implementation Specification for this service. Individual software vendors and servers can use this element to provide metadata about any additional server abilities. Metadata for one operation that this server implements. Unordered list of Distributed Computing Platforms (DCPs) supported for this operation. At present, only the HTTP DCP is defined, so this element will appear only once. Optional unordered list of parameter domains that each apply to this operation which this server implements. If one of these Parameter elements has the same "name" attribute as a Parameter element in the OperationsMetadata element, this Parameter element shall override the other one for this operation. The list of required and optional parameter domain limitations for this operation shall be specified in the Implementation Specification for this service. Optional unordered list of valid domain constraints on non-parameter quantities that each apply to this operation. If one of these Constraint elements has the same "name" attribute as a Constraint element in the OperationsMetadata element, this Constraint element shall override the other one for this operation. The list of required and optional constraints for this operation shall be specified in the Implementation Specification for this service. Optional unordered list of additional metadata about this operation and its' implementation. A list of required and optional metadata elements for this operation should be specified in the Implementation Specification for this service. (Informative: This metadata might specify the operation request parameters or provide the XML Schemas for the operation request.) Name or identifier of this operation (request) (for example, GetCapabilities). The list of required and optional operations implemented shall be specified in the Implementation Specification for this service. Information for one distributed Computing Platform (DCP) supported for this operation. At present, only the HTTP DCP is defined, so this element only includes the HTTP element. Connect point URLs for the HTTP Distributed Computing Platform (DCP). Normally, only one Get and/or one Post is included in this element. More than one Get and/or Post is allowed to support including alternative URLs for uses such as load balancing or backup. Connect point URL prefix and any constraints for the HTTP "Get" request method for this operation request. Connect point URL and any constraints for the HTTP "Post" request method for this operation request. Connect point URL and any constraints for this HTTP request method for this operation request. In the OnlineResourceType, the xlink:href attribute in the xlink:simpleLink attribute group shall be used to contain this URL. The other attributes in the xlink:simpleLink attribute group should not be used. Optional unordered list of valid domain constraints on non-parameter quantities that each apply to this request method for this operation. If one of these Constraint elements has the same "name" attribute as a Constraint element in the OperationsMetadata or Operation element, this Constraint element shall override the other one for this operation. The list of required and optional constraints for this request method for this operation shall be specified in the Implementation Specification for this service. mapproxy-1.11.0/mapproxy/test/schemas/ows/1.1.0/owsServiceIdentification.xsd000066400000000000000000000067651320454472400267450ustar00rootroot00000000000000 owsServiceIdentification.xsd 2010-01-30 This XML Schema Document encodes the common "ServiceIdentification" section of the GetCapabilities operation response, known as the Capabilities XML document. This section encodes the SV_ServiceIdentification class of ISO 19119 (OGC Abstract Specification Topic 12). OWS is an OGC Standard. Copyright (c) 2006,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/ . General metadata for this specific server. This XML Schema of this section shall be the same for all OWS. A service type name from a registry of services. For example, the values of the codeSpace URI and name and code string may be "OGC" and "catalogue." This type name is normally used for machine-to-machine communication. Unordered list of one or more versions of this service type implemented by this server. This information is not adequate for version negotiation, and shall not be used for that purpose. Unordered list of identifiers of Application Profiles that are implemented by this server. This element should be included for each specified application profile implemented by this server. The identifier value should be specified by each Application Profile. If this element is omitted, no meaning is implied. If this element is omitted, no meaning is implied. Unordered list of access constraints applied to assure the protection of privacy or intellectual property, and any other restrictions on retrieving or using data from or otherwise using this server. The reserved value NONE (case insensitive) shall be used to mean no access constraints are imposed. When this element is omitted, no meaning is implied. mapproxy-1.11.0/mapproxy/test/schemas/ows/1.1.0/owsServiceProvider.xsd000066400000000000000000000043471320454472400256000ustar00rootroot00000000000000 owsServiceProvider.xsd 2010-01-30 This XML Schema Document encodes the common "ServiceProvider" section of the GetCapabilities operation response, known as the Capabilities XML document. This section encodes the SV_ServiceProvider class of ISO 19119 (OGC Abstract Specification Topic 12). OWS is an OGC Standard. Copyright (c) 2006,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/ . Metadata about the organization that provides this specific service instance or server. A unique identifier for the service provider organization. Reference to the most relevant web site of the service provider. Information for contacting the service provider. The OnlineResource element within this ServiceContact element should not be used to reference a web site of the service provider. mapproxy-1.11.0/mapproxy/test/schemas/sld/000077500000000000000000000000001320454472400205215ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/sld/1.1.0/000077500000000000000000000000001320454472400211565ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/sld/1.1.0/sld_capabilities.xsd000066400000000000000000000031351320454472400251730ustar00rootroot00000000000000 Styled Layer Descriptor version 1.1.0 (2010-02-01) SLD is an OGC Standard. Copyright (c) 2007,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/ . mapproxy-1.11.0/mapproxy/test/schemas/wms/000077500000000000000000000000001320454472400205455ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/wms/1.0.0/000077500000000000000000000000001320454472400212015ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/wms/1.0.0/capabilities_1_0_0.dtd000066400000000000000000000407041320454472400252120ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/wms/1.0.0/capabilities_1_0_0.xml000066400000000000000000000173311320454472400252370ustar00rootroot00000000000000 is defined. --> ]> GetMap Acme Corp. Map Server WMT Map Server maintained by Acme Corporation. Contact: webmaster@wmt.acme.com. High-quality maps showing roadrunner nests and possible ambush locations. bird roadrunner ambush http://hostname:port/path/ none none Date in YYYYMMDD format 8-digit date in YYYYMMDD format. If absent, the latest available date (usually today, but not for non-daily measurements) is sent. Acme Corp. Map Server EPSG:4326 wmt_graticule Alignment test grid The WMT Graticule is a 10-degree grid suitable for testing alignment among Map Servers. graticule test ROADS_RIVERS Roads and Rivers EPSG:26986 ROADS_1M Roads at 1:1M scale Roads at a scale of 1 to 1 million. road transportation atlas http://www.opengis.org?roads.xml RIVERS_1M Rivers at 1:1M scale Rivers at a scale of 1 to 1 million. river canal water http://www.opengis.org?rivers.xml Weather Data EPSG:4326 Clouds Forecast cloud cover Temperature Forecast temperature Pressure Forecast barometric pressure mapproxy-1.11.0/mapproxy/test/schemas/wms/1.0.7/000077500000000000000000000000001320454472400212105ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/wms/1.0.7/capabilities_1_0_7.dtd000066400000000000000000000565121320454472400252340ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/wms/1.0.7/capabilities_1_0_7.xml000066400000000000000000000255401320454472400252560ustar00rootroot00000000000000 is defined. --> ]> GetMap Acme Corp. Map Server WMT Map Server maintained by Acme Corporation. Contact: webmaster@wmt.acme.com. High-quality maps showing roadrunner nests and possible ambush locations. bird roadrunner ambush Jeff deLaBeaujardiere NASA Computer Scientist postal
NASA Goddard Space Flight Center, Code 933
Greenbelt MD 20771 USA
+1 301 286-1569 +1 301 286-1777 delabeau@iniki.gsfc.nasa.gov
none none
Acme Corp. Map Server EPSG:4326 ROADS_RIVERS Roads and Rivers EPSG:26986 State College University http://www.university.edu/icons/logo.gif http://www.university.edu/data/roads_rivers.gml ROADS_1M Roads at 1:1M scale Roads at a scale of 1 to 1 million. road transportation atlas http://www.university.edu/fgdc/clearinghouse/metadata/roads.txt http://www.university.edu/fgdc/clearinghouse/metadata/roads.xml RIVERS_1M Rivers at 1:1M scale Rivers at a scale of 1 to 1 million. river canal waterway Weather Forecast Data EPSG:4326 1999-01-01/2000-08-22/P1D Clouds Forecast cloud cover Temperature Forecast temperature Pressure Forecast barometric pressure 1999-01-01/2000-08-22/P1D 0,1000,3000,5000,10000 ozone_image Global ozone distribution (1992) 1992
mapproxy-1.11.0/mapproxy/test/schemas/wms/1.1.0/000077500000000000000000000000001320454472400212025ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/wms/1.1.0/capabilities_1_1_0.dtd000066400000000000000000000236561320454472400252230ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/wms/1.1.0/capabilities_1_1_0.xml000066400000000000000000000315511320454472400252410ustar00rootroot00000000000000 ]> OGC:WMS Acme Corp. Map Server WMT Map Server maintained by Acme Corporation. Contact: webmaster@wmt.acme.com. High-quality maps showing roadrunner nests and possible ambush locations. bird roadrunner ambush Jeff deLaBeaujardiere NASA Computer Scientist postal
NASA Goddard Space Flight Center, Code 933
Greenbelt MD 20771 USA
+1 301 286-1569 +1 301 286-1777 delabeau@iniki.gsfc.nasa.gov
none none
application/vnd.ogc.wms_xml image/gif image/png image/jpeg application/vnd.ogc.gml text/plain text/html application/vnd.ogc.gml application/vnd.ogc.se_xml application/vnd.ogc.se_inimage application/vnd.ogc.se_blank Acme Corp. Map Server EPSG:4326 ROADS_RIVERS Roads and Rivers EPSG:26986 State College University image/gif 123456 application/vnd.ogc.se_xml" ROADS_1M Roads at 1:1M scale Roads at a scale of 1 to 1 million. road transportation atlas 123456 text/plain text/xml RIVERS_1M Rivers at 1:1M scale Rivers at a scale of 1 to 1 million. river canal waterway Weather Forecast Data EPSG:4326 1999-01-01/2000-08-22/P1D Clouds Forecast cloud cover Temperature Forecast temperature Pressure Forecast barometric pressure 1999-01-01/2000-08-22/P1D 0,1000,3000,5000,10000 ozone_image Global ozone distribution (1992) 1992 population World population, annual 1990/2000/P1Y
mapproxy-1.11.0/mapproxy/test/schemas/wms/1.1.0/exception_1_1_0.dtd000066400000000000000000000003151320454472400245530ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/wms/1.1.0/exception_1_1_0.xml000066400000000000000000000020711320454472400246010ustar00rootroot00000000000000 Plain text message about an error. Another message, this time with a SE code supplied. , line 42 A message that includes angle brackets in text must be enclosed in a Character Data Section as in this example. All XML-like markup is ignored except for this sequence of three closing characters: ]]> foo.c An error occurred Similarly, actual XML can be enclosed in a CDATA section. A generic parser will ignore that XML, but application-specific software may choose to process it. ]]> mapproxy-1.11.0/mapproxy/test/schemas/wms/1.1.1/000077500000000000000000000000001320454472400212035ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/wms/1.1.1/OGC-exception.xsd000066400000000000000000000054441320454472400243360ustar00rootroot00000000000000 The ServiceExceptionReport element contains one or more ServiceException elements that describe a service exception. The Service exception element is used to describe a service exception. The ServiceExceptionType type defines the ServiceException element. The content of the element is an exception message that the service wished to convey to the client application. A service may associate a code with an exception by using the code attribute. The locator attribute may be used by a service to indicate to a client where in the client's request an exception was encountered. If the request included a 'handle' attribute, this may be used to identify the offending component of the request. Otherwise the service may try to use other means to locate the exception such as line numbers or byte offset from the begining of the request, etc ... mapproxy-1.11.0/mapproxy/test/schemas/wms/1.1.1/WMS_DescribeLayerResponse.dtd000066400000000000000000000026711320454472400266700ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/wms/1.1.1/WMS_MS_Capabilities.dtd000066400000000000000000000243061320454472400254230ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/wms/1.1.1/WMS_exception_1_1_1.dtd000066400000000000000000000003141320454472400253020ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/wms/1.1.1/capabilities_1_1_1.dtd000066400000000000000000000244111320454472400252130ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/wms/1.1.1/capabilities_1_1_1.xml000066400000000000000000000315721320454472400252460ustar00rootroot00000000000000 ]> OGC:WMS Acme Corp. Map Server WMT Map Server maintained by Acme Corporation. Contact: webmaster@wmt.acme.com. High-quality maps showing roadrunner nests and possible ambush locations. bird roadrunner ambush Jeff deLaBeaujardiere NASA Computer Scientist postal
NASA Goddard Space Flight Center, Code 933
Greenbelt MD 20771 USA
+1 301 286-1569 +1 301 286-1777 delabeau@iniki.gsfc.nasa.gov
none none
application/vnd.ogc.wms_xml image/gif image/png image/jpeg application/vnd.ogc.gml text/plain text/html application/vnd.ogc.gml application/vnd.ogc.se_xml application/vnd.ogc.se_inimage application/vnd.ogc.se_blank Acme Corp. Map Server EPSG:4326 ROADS_RIVERS Roads and Rivers EPSG:26986 State College University image/gif 123456 application/vnd.ogc.se_xml" ROADS_1M Roads at 1:1M scale Roads at a scale of 1 to 1 million. road transportation atlas 123456 text/plain text/xml RIVERS_1M Rivers at 1:1M scale Rivers at a scale of 1 to 1 million. river canal waterway Weather Forecast Data EPSG:4326 1999-01-01/2000-08-22/P1D Clouds Forecast cloud cover Temperature Forecast temperature Pressure Forecast barometric pressure 1999-01-01/2000-08-22/P1D 0,1000,3000,5000,10000 ozone_image Global ozone distribution (1992) 1992 population World population, annual 1990/2000/P1Y
mapproxy-1.11.0/mapproxy/test/schemas/wms/1.1.1/exception_1_1_1.dtd000066400000000000000000000003151320454472400245550ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/wms/1.1.1/exception_1_1_1.xml000066400000000000000000000021071320454472400246030ustar00rootroot00000000000000 Plain text message about an error. Another message, this one with a Service Exception code supplied. , line 42 A message that includes angle brackets in text must be enclosed in a Character Data Section as in this example. All XML-like markup is ignored except for this sequence of three closing characters: ]]> foo.c An error occurred Similarly, actual XML can be enclosed in a CDATA section. A generic parser will ignore that XML, but application-specific software may choose to process it. ]]> mapproxy-1.11.0/mapproxy/test/schemas/wms/1.3.0/000077500000000000000000000000001320454472400212045ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/wms/1.3.0/ReadMe.txt000066400000000000000000000004731320454472400231060ustar00rootroot00000000000000This set of XML Schema Documents for OpenGIS Web Map Service Version 1.3.0 has been edited to reflect the corrigendum to document OGC 04-024 that are based on the change requests: OGC 05-068r1 "Store xlinks.xsd file at a fixed location" OGC 05-081r2 "Change to use relative paths" Arliss Whiteside, 2005-11-22 mapproxy-1.11.0/mapproxy/test/schemas/wms/1.3.0/capabilities_1_3_0.xml000066400000000000000000000263231320454472400252460ustar00rootroot00000000000000 WMS Acme Corp. Map Server Map Server maintained by Acme Corporation. Contact: webmaster@wmt.acme.com. High-quality maps showing roadrunner nests and possible ambush locations. bird roadrunner ambush Jeff Smith NASA Computer Scientist postal
NASA Goddard Space Flight Center
Greenbelt MD 20771 USA
+1 301 555-1212 user@host.com
none none 16 2048 2048
text/xml image/gif image/png image/jpeg text/xml text/plain text/html XML INIMAGE BLANK Acme Corp. Map Server CRS:84 ROADS_RIVERS Roads and Rivers EPSG:26986 -71.63 -70.78 41.75 42.90 State College University image/gif 123456 XML" ROADS_1M Roads at 1:1M scale Roads at a scale of 1 to 1 million. road transportation atlas 123456 text/plain text/xml RIVERS_1M Rivers at 1:1M scale Rivers at a scale of 1 to 1 million. river canal waterway Weather Forecast Data CRS:84 -180 180 -90 90 1999-01-01/2000-08-22/P1D Clouds Forecast cloud cover Temperature Forecast temperature Pressure Forecast barometric pressure 1999-01-01/2000-08-22/P1D 0,1000,3000,5000,10000 ozone_image Global ozone distribution (1992) -180 180 -90 90 1992 population World population, annual -180 180 -90 90 1990/2000/P1Y
mapproxy-1.11.0/mapproxy/test/schemas/wms/1.3.0/capabilities_1_3_0.xsd000066400000000000000000000516171320454472400252500ustar00rootroot00000000000000 A WMS_Capabilities document is returned in response to a GetCapabilities request made on a WMS. The Name is typically for machine-to-machine communication. The Title is for informative display to a human. The abstract is a longer narrative description of an object. List of keywords or keyword phrases to help catalog searching. A single keyword or phrase. An OnlineResource is typically an HTTP URL. The URL is placed in the xlink:href attribute, and the value "simple" is placed in the xlink:type attribute. A container for listing an available format's MIME type. General service metadata. Information about a contact person for the service. A Capability lists available request types, how exceptions may be reported, and whether any extended capabilities are defined. It also includes an optional list of map layers available from this server. Available WMS Operations are listed in a Request element. For each operation offered by the server, list the available output formats and the online resource. Available Distributed Computing Platforms (DCPs) are listed here. At present, only HTTP is defined. Available HTTP request methods. At least "Get" shall be supported. The URL prefix for the HTTP "Get" request method. The URL prefix for the HTTP "Post" request method. An Exception element indicates which error-reporting formats are supported. Individual service providers may use this element to report extended capabilities. Nested list of zero or more map Layers offered by this server. Identifier for a single Coordinate Reference System (CRS). The EX_GeographicBoundingBox attributes indicate the limits of the enclosing rectangle in longitude and latitude decimal degrees. The BoundingBox attributes indicate the limits of the bounding box in units of the specified coordinate reference system. The Dimension element declares the existence of a dimension and indicates what values along a dimension are valid. Attribution indicates the provider of a Layer or collection of Layers. The provider's URL, descriptive title string, and/or logo image URL may be supplied. Client applications may choose to display one or more of these items. A format element indicates the MIME type of the logo image located at LogoURL. The logo image's width and height assist client applications in laying out space to display the logo. A Map Server may use zero or more MetadataURL elements to offer detailed, standardized metadata about the data underneath a particular layer. The type attribute indicates the standard to which the metadata complies. The format element indicates how the metadata is structured. A Map Server may use zero or more Identifier elements to list ID numbers or labels defined by a particular Authority. For example, the Global Change Master Directory (gcmd.gsfc.nasa.gov) defines a DIF_ID label for every dataset. The authority name and explanatory URL are defined in a separate AuthorityURL element, which may be defined once and inherited by subsidiary layers. Identifiers themselves are not inherited. A Map Server may use DataURL offer a link to the underlying data represented by a particular layer. A Map Server may use FeatureListURL to point to a list of the features represented in a Layer. A Style element lists the name by which a style is requested and a human-readable title for pick lists, optionally (and ideally) provides a human-readable description, and optionally gives a style URL. A Map Server may use zero or more LegendURL elements to provide an image(s) of a legend relevant to each Style of a Layer. The Format element indicates the MIME type of the legend. Width and height attributes may be provided to assist client applications in laying out space to display the legend. StyleSheeetURL provides symbology information for each Style of a Layer. A Map Server may use StyleURL to offer more information about the data or symbology underlying a particular Style. While the semantics are not well-defined, as long as the results of an HTTP GET request against the StyleURL are properly MIME-typed, Viewer Clients and Cascading Map Servers can make use of this. A possible use could be to allow a Map Server to provide legend information. Minimum scale denominator for which it is appropriate to display this layer. Maximum scale denominator for which it is appropriate to display this layer. mapproxy-1.11.0/mapproxy/test/schemas/wms/1.3.0/exceptions_1_3_0.xml000066400000000000000000000021131320454472400247650ustar00rootroot00000000000000 Plain text message about an error. Another error message, this one with a service exception code supplied. , line 42 A message that includes angle brackets in text must be enclosed in a Character Data Section as in this example. All XML-like markup is ignored except for this sequence of three closing characters: ]]> foo.c An error occurred Similarly, actual XML can be enclosed in a CDATA section. A generic parser will ignore that XML, but application-specific software may choose to process it. ]]> mapproxy-1.11.0/mapproxy/test/schemas/wms/1.3.0/exceptions_1_3_0.xsd000066400000000000000000000017261320454472400247740ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/wmsc/000077500000000000000000000000001320454472400207105ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/wmsc/1.1.1/000077500000000000000000000000001320454472400213465ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/wmsc/1.1.1/OGC-exception.xsd000066400000000000000000000054441320454472400245010ustar00rootroot00000000000000 The ServiceExceptionReport element contains one or more ServiceException elements that describe a service exception. The Service exception element is used to describe a service exception. The ServiceExceptionType type defines the ServiceException element. The content of the element is an exception message that the service wished to convey to the client application. A service may associate a code with an exception by using the code attribute. The locator attribute may be used by a service to indicate to a client where in the client's request an exception was encountered. If the request included a 'handle' attribute, this may be used to identify the offending component of the request. Otherwise the service may try to use other means to locate the exception such as line numbers or byte offset from the begining of the request, etc ... mapproxy-1.11.0/mapproxy/test/schemas/wmsc/1.1.1/WMS_DescribeLayerResponse.dtd000066400000000000000000000026711320454472400270330ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/wmsc/1.1.1/WMS_MS_Capabilities.dtd000066400000000000000000000250351320454472400255660ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/wmsc/1.1.1/WMS_exception_1_1_1.dtd000066400000000000000000000003141320454472400254450ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/wmsc/1.1.1/capabilities_1_1_1.dtd000066400000000000000000000244111320454472400253560ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/wmsc/1.1.1/capabilities_1_1_1.xml000066400000000000000000000315721320454472400254110ustar00rootroot00000000000000 ]> OGC:WMS Acme Corp. Map Server WMT Map Server maintained by Acme Corporation. Contact: webmaster@wmt.acme.com. High-quality maps showing roadrunner nests and possible ambush locations. bird roadrunner ambush Jeff deLaBeaujardiere NASA Computer Scientist postal
NASA Goddard Space Flight Center, Code 933
Greenbelt MD 20771 USA
+1 301 286-1569 +1 301 286-1777 delabeau@iniki.gsfc.nasa.gov
none none
application/vnd.ogc.wms_xml image/gif image/png image/jpeg application/vnd.ogc.gml text/plain text/html application/vnd.ogc.gml application/vnd.ogc.se_xml application/vnd.ogc.se_inimage application/vnd.ogc.se_blank Acme Corp. Map Server EPSG:4326 ROADS_RIVERS Roads and Rivers EPSG:26986 State College University image/gif 123456 application/vnd.ogc.se_xml" ROADS_1M Roads at 1:1M scale Roads at a scale of 1 to 1 million. road transportation atlas 123456 text/plain text/xml RIVERS_1M Rivers at 1:1M scale Rivers at a scale of 1 to 1 million. river canal waterway Weather Forecast Data EPSG:4326 1999-01-01/2000-08-22/P1D Clouds Forecast cloud cover Temperature Forecast temperature Pressure Forecast barometric pressure 1999-01-01/2000-08-22/P1D 0,1000,3000,5000,10000 ozone_image Global ozone distribution (1992) 1992 population World population, annual 1990/2000/P1Y
mapproxy-1.11.0/mapproxy/test/schemas/wmsc/1.1.1/exception_1_1_1.dtd000066400000000000000000000003151320454472400247200ustar00rootroot00000000000000 mapproxy-1.11.0/mapproxy/test/schemas/wmsc/1.1.1/exception_1_1_1.xml000066400000000000000000000021071320454472400247460ustar00rootroot00000000000000 Plain text message about an error. Another message, this one with a Service Exception code supplied. , line 42 A message that includes angle brackets in text must be enclosed in a Character Data Section as in this example. All XML-like markup is ignored except for this sequence of three closing characters: ]]> foo.c An error occurred Similarly, actual XML can be enclosed in a CDATA section. A generic parser will ignore that XML, but application-specific software may choose to process it. ]]> mapproxy-1.11.0/mapproxy/test/schemas/wmts/000077500000000000000000000000001320454472400207315ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/wmts/1.0/000077500000000000000000000000001320454472400212275ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/wmts/1.0/ReadMe.txt000066400000000000000000000021141320454472400231230ustar00rootroot00000000000000OpenGIS(r) WMTS 1.0.0 - ReadMe.txt ====================================== ----------------------------------------------------------------------- Web Map Tile Service (WMTS) interface standard (OGC 07-057r7) More information on the OGC WMTS standard may be found at http://www.opengeospatial.org/standards/wmts The most current schema are available at http://schemas.opengis.net/ . ----------------------------------------------------------------------- 2010-05-04 Kevin Stegemoller * v1.0: post wmts/1.0.0 as wmts/1.0 from OGC 07-057r7 * v1.0: These documents were validated with: + XSV Validator version 3.1.1 + Xerces-c validator version 2.8.0 + libxml2 validator version 2.7.3 + AltovaXML 2009 + MSXML parser 4.0 sp2. -- Joan Maso ----------------------------------------------------------------------- Policies, Procedures, Terms, and Conditions of OGC(r) are available http://www.opengeospatial.org/ogc/legal/ . Copyright (c) 2010 Open Geospatial Consortium, Inc. All Rights Reserved. ----------------------------------------------------------------------- mapproxy-1.11.0/mapproxy/test/schemas/wmts/1.0/wmts.xsd000066400000000000000000000022631320454472400227440ustar00rootroot00000000000000 wmts 2009-02-14 This XML Schema Document includes all WMTS schemas and is useful for SOAP messages. WMTS is an OGC Standard. Copyright (c) 2009,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/. mapproxy-1.11.0/mapproxy/test/schemas/wmts/1.0/wmtsAbstract.wsdl000066400000000000000000000126441320454472400246070ustar00rootroot00000000000000 wmts_abstract.wsdl 2009-02-14 This WSDL document encodes describes the WMTS service for KVP and SOAP encodings. WMTS is an OGC Standard. Copyright (c) 2009,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/. mapproxy-1.11.0/mapproxy/test/schemas/wmts/1.0/wmtsGetCapabilities_request.xsd000066400000000000000000000030141320454472400274610ustar00rootroot00000000000000 wmtsGetCapabilities_request 2009-01-31 This XML Schema Document defines the XML WMTS GetCapabilites request that can be used in SOAP encodings. WMTS is an OGC Standard. Copyright (c) 2009,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/. WMTS GetCapabilities operation request. mapproxy-1.11.0/mapproxy/test/schemas/wmts/1.0/wmtsGetCapabilities_response.xsd000066400000000000000000000523141320454472400276360ustar00rootroot00000000000000 wmtsGetCapabilities_response 2009-01-31 This XML Schema Document encodes the WMTS GetCapabilities operations response message. WMTS is an OGC Standard. Copyright (c) 2009,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/. XML defines the WMTS GetCapabilities operation response. ServiceMetadata document provides clients with service metadata about a specific service instance, including metadata about the tightly-coupled data served. If the server does not implement the updateSequence parameter, the server SHALL always return the complete Capabilities document, without the updateSequence parameter. When the server implements the updateSequence parameter and the GetCapabilities operation request included the updateSequence parameter with the current value, the server SHALL return this element with only the "version" and "updateSequence" attributes. Otherwise, all optional elements SHALL be included or not depending on the actual value of the Contents parameter in the GetCapabilities operation request. Metadata about the data served by this server. For WMTS, this section SHALL contain data about layers and TileMatrixSets Metadata describing a theme hierarchy for the layers Reference to a WSDL resource Reference to a ServiceMetadata resource on resource oriented architectural style A description of the geometry of a tile fragmentation Metadata about the styles of this layer Supported valid output MIME types for a tile Supported valid output MIME types for a FeatureInfo. If there isn't any, The server do not support FeatureInfo requests for this layer. Extra dimensions for a tile and FeatureInfo requests. Reference to a tileMatrixSet and limits URL template to a tile or a FeatureInfo resource on resource oriented architectural style An unambiguous reference to this style, identifying a specific version when needed, normally used by software Description of an image that represents the legend of the map This style is used when no style is specified Zero or more LegendURL elements may be provided, providing an image(s) of a legend relevant to each Style of a Layer. The Format element indicates the MIME type of the legend. minScaleDenominator and maxScaleDenominator attributes may be provided to indicate to the client which scale(s) (inclusive) the legend image is appropriate for. (If provided, these values must exactly match the scale denominators of available TileMatrixes.) width and height attributes may be provided to assist client applications in laying out space to display the legend. The URL from which the legend image can be retrieved A supported output format for the legend image Denominator of the minimum scale (inclusive) for which this legend image is valid Denominator of the maximum scale (exclusive) for which this legend image is valid Width (in pixels) of the legend image Height (in pixels) of the legend image Metadata about a particular dimension that the tiles of a layer are available. A name of dimensional axis Units of measure of dimensional axis. Symbol of the units. Default value that will be used if a tile request does not specify a value or uses the keyword 'default'. A value of 1 (or 'true') indicates (a) that temporal data are normally kept current and (b) that the request value of this dimension accepts the keyword 'current'. Available value for this dimension. Metadata about the TileMatrixSet reference. Reference to a tileMatrixSet Indices limits for this tileMatrixSet. The absence of this element means that tile row and tile col indices are only limited by 0 and the corresponding tileMatrixSet maximum definitions. Metadata about a the limits of the tile row and tile col indices. Metadata describing the limits of the TileMatrixSet indices. Multiplicity must be the multiplicity of TileMatrix in this TileMatrixSet. Metadata describing the limits of a TileMatrix for this layer. Reference to a TileMatrix identifier Minimum tile row index valid for this layer. From 0 to maxTileRow Maximim tile row index valid for this layer. From minTileRow to matrixWidth-1 of the tileMatrix section of this tileMatrixSet Minimum tile column index valid for this layer. From 0 to maxTileCol Maximim tile column index valid for this layer. From minTileCol to tileHeight-1 of the tileMatrix section of this tileMatrixSet. Format of the resource representation that can be retrieved one resolved the URL template. Resource type to be retrieved. It can only be "tile" or "FeatureInfo" URL template. A template processor will be applied to substitute some variables between {} for their values and get a URL to a resource. We cound not use a anyURI type (that conforms the character restrictions specified in RFC2396 and excludes '{' '}' characters in some XML parsers) because this attribute must accept the '{' '}' caracters. Describes a particular set of tile matrices. Tile matrix set identifier Minimum bounding rectangle surrounding the visible layer presented by this tile matrix set, in the supported CRS Reference to one coordinate reference system (CRS). Reference to a well known scale set. urn:ogc:def:wkss:OGC:1.0:GlobalCRS84Scale, urn:ogc:def:wkss:OGC:1.0:GlobalCRS84Pixel, urn:ogc:def:wkss:OGC:1.0:GoogleCRS84Quad and urn:ogc:def:wkss:OGC:1.0:GoogleMapsCompatible are possible values that are defined in Annex E. It has to be consistent with the SupportedCRS and with the ScaleDenominators of the TileMatrix elements. Describes a scale level and its tile matrix. Describes a particular tile matrix. Tile matrix identifier. Typically an abreviation of the ScaleDenominator value or its equivalent pixel size Scale denominator level of this tile matrix Position in CRS coordinates of the top-left corner of this tile matrix. This are the precise coordinates of the top left corner of top left pixel of the 0,0 tile in SupportedCRS coordinates of this TileMatrixSet. Width of each tile of this tile matrix in pixels. Height of each tile of this tile matrix in pixels Width of the matrix (number of tiles in width) Height of the matrix (number of tiles in height) Provides a set of hierarchical themes that the client can use to categorize the layers by. Metadata describing the top-level themes where layers available on this server can be classified. Name of the theme Metadata describing the child (subordinate) themes of this theme where layers available on this server can be classified Reference to layer mapproxy-1.11.0/mapproxy/test/schemas/wmts/1.0/wmtsGetFeatureInfo_request.xsd000066400000000000000000000042401320454472400273010ustar00rootroot00000000000000 wmtsGetFeatureInfo_request 2009-01-31 This XML Schema Document defines XML WMTS GetFeatureInfo request that can be used in SOAP encodings. WMTS is an OGC Standard. Copyright (c) 2009,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/. The corresponding GetTile request parameters Row index of a pixel in the tile Column index of a pixel in the tile Output MIME type format of the retrieved information mapproxy-1.11.0/mapproxy/test/schemas/wmts/1.0/wmtsGetFeatureInfo_response.xsd000066400000000000000000000054011320454472400274470ustar00rootroot00000000000000 wmtsGetFeatureInfo_response 2009-06-14 This XML Schema Document was intended to encode SOAP response for a WMTS GetFeatureInfo request but it can be used in other encoding. Since GetFeatureInfo response is completely open, it can not be more specific. WMTS is an OGC Standard. Copyright (c) 2009,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/. This allows to define any FeatureCollection that is a substitutionGroup of gml:_GML and use it here. A Geography Markup Language GML Simple Features Profile level 0 response format is strongly recommended as a FeatureInfo response. This allows to include any text format that is not a gml:_FeatureCollection like HTML, TXT, etc This allows to include any binary format. Binary formats are not common response for a GeFeatureInfo requests but possible for some imaginative implementations. This allows to include any XML content that it is not any of the previous ones. mapproxy-1.11.0/mapproxy/test/schemas/wmts/1.0/wmtsGetTile_request.xsd000066400000000000000000000062531320454472400257750ustar00rootroot00000000000000 wmtsGetTile_request 2009-01-31 This XML Schema Document encodes XML WMTS GetTile request that can be used in SOAP encodings. WMTS is an OGC Standard. Copyright (c) 2009,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/. A layer identifier has to be referenced A style identifier has to be referenced. Output format of the tile Dimension name and value A TileMatrixSet identifier has to be referenced A TileMatrix identifier has to be referenced Row index of tile matrix Column index of tile matrix Dimension value Dimension name mapproxy-1.11.0/mapproxy/test/schemas/wmts/1.0/wmtsKVP.xsd000066400000000000000000000055011320454472400233230ustar00rootroot00000000000000 wmtsGetTile_request 2009-01-31 This XML Schema Document defines XML WMTS GetTile request that can be used in SOAP encodings. WMTS is an OGC Standard. Copyright (c) 2009,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/. XML encoded identifier comma separated list of a standard MIME type, possibly a parameterized MIME type. Comma separated list of available ServiceMetadata root elements. Comma separated list of a standard MIME type, possibly a parameterized MIME type. mapproxy-1.11.0/mapproxy/test/schemas/wmts/1.0/wmtsPayload_response.xsd000066400000000000000000000046271320454472400262020ustar00rootroot00000000000000 wmtsPayload_response 2009-06-15 This XML Schema Document initially was intended to encode SOAP response for a WMTS GetTile request but in the future it might be used and part of a WMTS service (or even in any OWS service) that needs a binary encoding. WMTS is an OGC Standard. Copyright (c) 2009,2010 Open Geospatial Consortium, Inc. All Rights Reserved. To obtain additional rights of use, visit http://www.opengeospatial.org/legal/. MIMEType format of the PayloadContent once base64 decodified. Binary content encoded in base64. It could be useful to enclose it in a CDATA element to avoid XML parsing. MIMEType format of the TextContent Text string like HTML, XHTML, XML or TXT. HTML and TXT data has to be enclosed in a CDATA element to avoid XML parsing. mapproxy-1.11.0/mapproxy/test/schemas/xlink/000077500000000000000000000000001320454472400210645ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/xlink/1.0.0/000077500000000000000000000000001320454472400215205ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/schemas/xlink/1.0.0/ReadMe.txt000066400000000000000000000002661320454472400234220ustar00rootroot00000000000000This XML Schema Document named xlinks.xsd has been stored here based on the change request: OGC 05-068r1 "Store xlinks.xsd file at a fixed location" Arliss Whiteside, 2005-11-22 mapproxy-1.11.0/mapproxy/test/schemas/xlink/1.0.0/xlinks.xsd000066400000000000000000000122011320454472400235440ustar00rootroot00000000000000 xlinks.xsd v3.0b2 2001-07 GML 3.0 candidate xlinks schema. Copyright (c) 2001 OGC, All Rights Reserved. The 'show' attribute is used to communicate the desired presentation of the ending resource on traversal from the starting resource; it's value should be treated as follows: new - load ending resource in a new window, frame, pane, or other presentation context replace - load the resource in the same window, frame, pane, or other presentation context embed - load ending resource in place of the presentation of the starting resource other - behavior is unconstrained; examine other markup in the link for hints none - behavior is unconstrained The 'actuate' attribute is used to communicate the desired timing of traversal from the starting resource to the ending resource; it's value should be treated as follows: onLoad - traverse to the ending resource immediately on loading the starting resource onRequest - traverse from the starting resource to the ending resource only on a post-loading event triggered for this purpose other - behavior is unconstrained; examine other markup in link for hints none - behavior is unconstrained mapproxy-1.11.0/mapproxy/test/schemas/xml.xsd000066400000000000000000000212041320454472400212560ustar00rootroot00000000000000

About the XML namespace

This schema document describes the XML namespace, in a form suitable for import by other schema documents.

See http://www.w3.org/XML/1998/namespace.html and http://www.w3.org/TR/REC-xml for information about this namespace.

Note that local names in this namespace are intended to be defined only by the World Wide Web Consortium or its subgroups. The names currently defined in this namespace are listed below. They should not be used with conflicting semantics by any Working Group, specification, or document instance.

See further below in this document for more information about how to refer to this schema document from your own XSD schema documents and about the namespace-versioning policy governing this schema document.

lang (as an attribute name)

denotes an attribute whose value is a language code for the natural language of the content of any element; its value is inherited. This name is reserved by virtue of its definition in the XML specification.

Notes

Attempting to install the relevant ISO 2- and 3-letter codes as the enumerated possible values is probably never going to be a realistic possibility.

See BCP 47 at http://www.rfc-editor.org/rfc/bcp/bcp47.txt and the IANA language subtag registry at http://www.iana.org/assignments/language-subtag-registry for further information.

The union allows for the 'un-declaration' of xml:lang with the empty string.

space (as an attribute name)

denotes an attribute whose value is a keyword indicating what whitespace processing discipline is intended for the content of the element; its value is inherited. This name is reserved by virtue of its definition in the XML specification.

base (as an attribute name)

denotes an attribute whose value provides a URI to be used as the base for interpreting any relative URIs in the scope of the element on which it appears; its value is inherited. This name is reserved by virtue of its definition in the XML Base specification.

See http://www.w3.org/TR/xmlbase/ for information about this attribute.

id (as an attribute name)

denotes an attribute whose value should be interpreted as if declared to be of type ID. This name is reserved by virtue of its definition in the xml:id specification.

See http://www.w3.org/TR/xml-id/ for information about this attribute.

Father (in any context at all)

denotes Jon Bosak, the chair of the original XML Working Group. This name is reserved by the following decision of the W3C XML Plenary and XML Coordination groups:

In appreciation for his vision, leadership and dedication the W3C XML Plenary on this 10th day of February, 2000, reserves for Jon Bosak in perpetuity the XML name "xml:Father".

About this schema document

This schema defines attributes and an attribute group suitable for use by schemas wishing to allow xml:base, xml:lang, xml:space or xml:id attributes on elements they define.

To enable this, such a schema must import this schema for the XML namespace, e.g. as follows:

          <schema . . .>
           . . .
           <import namespace="http://www.w3.org/XML/1998/namespace"
                      schemaLocation="http://www.w3.org/2001/xml.xsd"/>
     

or

           <import namespace="http://www.w3.org/XML/1998/namespace"
                      schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
     

Subsequently, qualified reference to any of the attributes or the group defined below will have the desired effect, e.g.

          <type . . .>
           . . .
           <attributeGroup ref="xml:specialAttrs"/>
     

will define a type which will schema-validate an instance element with any of those attributes.

Versioning policy for this schema document

In keeping with the XML Schema WG's standard versioning policy, this schema document will persist at http://www.w3.org/2009/01/xml.xsd.

At the date of issue it can also be found at http://www.w3.org/2001/xml.xsd.

The schema document at that URI may however change in the future, in order to remain compatible with the latest version of XML Schema itself, or with the XML namespace itself. In other words, if the XML Schema or XML namespaces change, the version of this document at http://www.w3.org/2001/xml.xsd will change accordingly; the version at http://www.w3.org/2009/01/xml.xsd will not change.

Previous dated (and unchanging) versions of this schema document are at:

mapproxy-1.11.0/mapproxy/test/system/000077500000000000000000000000001320454472400176405ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/__init__.py000066400000000000000000000065741320454472400217650ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import os import tempfile import shutil from webtest import TestApp as TestApp_ from mapproxy.wsgiapp import make_wsgi_app class TestApp(TestApp_): """ Wraps webtest.TestApp and explicitly converts URLs to strings. Behavior changed with webtest from 1.2->1.3. """ def get(self, url, *args, **kw): return TestApp_.get(self, str(url), *args, **kw) def module_setup(test_config, config_file, with_cache_data=False): prepare_env(test_config, config_file, with_cache_data) create_app(test_config) def prepare_env(test_config, config_file, with_cache_data=False): if 'fixture_dir' not in test_config: test_config['fixture_dir'] = os.path.join(os.path.dirname(__file__), 'fixture') fixture_layer_conf = os.path.join(test_config['fixture_dir'], config_file) if 'base_dir' not in test_config: test_config['tmp_dir'] = tempfile.mkdtemp() test_config['base_dir'] = os.path.join(test_config['tmp_dir'], 'etc') os.mkdir(test_config['base_dir']) test_config['config_file'] = os.path.join(test_config['base_dir'], config_file) test_config['cache_dir'] = os.path.join(test_config['base_dir'], 'cache_data') shutil.copy(fixture_layer_conf, test_config['config_file']) if with_cache_data: shutil.copytree(os.path.join(test_config['fixture_dir'], 'cache_data'), test_config['cache_dir']) def create_app(test_config): app = make_wsgi_app(test_config['config_file'], ignore_config_warnings=False) app.base_config.debug_mode = True test_config['app'] = TestApp(app, use_unicode=False) def module_teardown(test_config): shutil.rmtree(test_config['base_dir']) if 'tmp_dir' in test_config: shutil.rmtree(test_config['tmp_dir']) test_config.clear() def make_base_config(test_config): def wrapped(): if hasattr(test_config['app'], 'base_config'): return test_config['app'].base_config return test_config['app'].app.base_config return wrapped class SystemTest(object): def setup(self): self.app = self.config['app'] self.created_tiles = [] self.base_config = make_base_config(self.config) def created_tiles_filenames(self): base_dir = self.base_config().cache.base_dir for filename in self.created_tiles: yield os.path.join(base_dir, filename) def _test_created_tiles(self): for filename in self.created_tiles_filenames(): if not os.path.exists(filename): assert False, "didn't found tile " + filename def teardown(self): self._test_created_tiles() for filename in self.created_tiles_filenames(): if os.path.exists(filename): os.remove(filename) mapproxy-1.11.0/mapproxy/test/system/fixture/000077500000000000000000000000001320454472400213265ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/arcgis.yaml000066400000000000000000000027721320454472400234720ustar00rootroot00000000000000services: tms: wms: featureinfo_types: ['json'] layers: - name: app2_layer title: ArcGIS Cache Layer sources: [app2_cache] - name: app2_with_layers_layer title: ArcGIS Cache Layer sources: [app2_with_layers_cache] - name: app2_with_layers_fi_layer title: ArcGIS Cache Layer sources: [app2_with_layers_fi_cache] - name: app2_wrong_url_layer title: ArcGIS Cache Layer sources: [app2_wrong_url_cache] caches: app2_cache: grids: [GLOBAL_MERCATOR] sources: [app2_source] app2_with_layers_cache: grids: [GLOBAL_MERCATOR] sources: [app2_with_layers_source] app2_with_layers_fi_cache: grids: [GLOBAL_MERCATOR] sources: [app2_with_layers_fi_source] app2_wrong_url_cache: grids: [GLOBAL_MERCATOR] sources: [app2_wrong_url_source] sources: app2_source: type: arcgis req: url: http://localhost:42423/arcgis/rest/services/ExampleLayer/ImageServer app2_with_layers_source: type: arcgis req: layers: show:0,1 url: http://localhost:42423/arcgis/rest/services/ExampleLayer/MapServer app2_with_layers_fi_source: type: arcgis opts: featureinfo: true featureinfo_tolerance: 10 featureinfo_return_geometries: true supported_srs: ['EPSG:3857'] req: layers: show:1,2,3 url: http://localhost:42423/arcgis/rest/services/ExampleLayer/MapServer app2_wrong_url_source: type: arcgis req: url: http://localhost:42423/arcgis/rest/services/NonExistentLayer/ImageServer mapproxy-1.11.0/mapproxy/test/system/fixture/auth.yaml000066400000000000000000000023121320454472400231510ustar00rootroot00000000000000services: tms: kml: wmts: demo: wms: md: title: 'My WMS' layers: - name: layer1 title: layer 1 sources: [dummy] layers: - name: layer1a title: layer 1a sources: [dummy] - name: layer1b title: layer 1b sources: [dummy_fi] - name: layer2 title: layer 2 layers: - name: layer2a title: layer 2a sources: [dummy] - name: layer2b title: layer 2b layers: - name: layer2b1 title: layer 2b1 sources: [dummy_fi] - name: layer3 title: layer 3 sources: [cache] caches: cache: grids: [GLOBAL_MERCATOR] format: 'image/jpeg' disable_storage: True meta_size: [1, 1] meta_buffer: 0 sources: [source] dummy: grids: [GLOBAL_MERCATOR] sources: [dummy] dummy_fi: grids: [GLOBAL_MERCATOR] sources: [dummy_fi] sources: dummy: type: debug dummy_fi: type: wms wms_opts: featureinfo: True coverage: bbox: [179, 89, 180, 89.9] bbox_srs: 'EPSG:4326' req: url: http://localhost:42423/service layers: fi source: type: tile url: http://localhost:42423/%(tms_path)s.pngmapproxy-1.11.0/mapproxy/test/system/fixture/cache.gpkg000066400000000000000000001300001320454472400232350ustar00rootroot00000000000000SQLite format 3@ , GP10-"  2k 1 =no_spatial_ref_systilestest_case2016-06-13T16:24:03.423ZsE|sE|AsE|AsE|AsE|_  =buildingsfeaturesbuildings2016-06-13T13:19:59.348Z0k~(@%<64+rm\@)(˒:*k 9=cachetilescacheCreated with Mapproxy.2016-06-10T15:03:39.390ZsE|sE|AsE|AsE| 1 1no_spatial_ref_sys buildings cache  test_case buildings cache data_type TEXT NOT NULL, -- Type of data stored in the table: "features" per clause Features (http://www.geopackage.org/spec/#features), "tiles" per clause Tiles (http://www.geopackage.org/spec/#tiles), or an implementer-defined value for other data tables per clause in an Extended GeoPackage identifier TEXT UNIQUE, -- A human-readable identifier (e.g. short name) for the table_name content description TEXT DEFAULT '', -- A human-readable description for the table_name content last_change DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), -- Timestamp value in ISO 8601 format as defined by the strftime function %Y-%m-%dT%H:%M:%fZ format string applied to the current time min_x DOUBLE, -- Bounding box minimum easting or longitude for all content in table_name min_y DOUBLE, -- Bounding box minimum northing or latitude for all content in table_name max_x DOUBLE, -- Bounding box maximum easting or longitude for all content in table_name max_y DOUBLE, -- Bounding box maximum northing or latitude for all content in table_name srs_id INTEGER, -- Spatial Reference System ID: gpkg_spatial_ref_sys.srs_id; when data_type is features, SHALL also match gpkg_geometry_columns.srs_id; When data_type is tiles, SHALL also match gpkg_tile_matrix_set.srs.id CONSTRAINT fk_gc_r_srs_id FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys(srs_id)) L''Wtablegpkg_contentsgpkg_contentsCREATE TABLE gpkg_contents (table_name TEXT NOT NULL PRIMARY KEY, -- The name of the tiles, or feature table 9M'indexsqlite_autoindex_gpkg_contents_1gpkg_contents9M'indexsqlite_autoindex_gpkg_contents_2gpkg_contents %%j55wtablegpkg_spatial_ref_sysgpkg_spatial_ref_sysCREATE TABLE gpkg_spatial_ref_sys (srs_name TEXT NOT NULL, -- Human readable name of this SRS (Spatial Reference System) srs_id INTEGER NOT NULL PRIMARY KEY, -- Unique identifier for each Spatial Reference System within a GeoPackage organization TEXT NOT NULL, -- Case-insensitive name of the defining organization e.g. EPSG or epsg organization_coordsys_id INTEGER NOT NULL, -- Numeric ID of the Spatial Reference System assigned by the organization definition TEXT NOT NULL, -- Well-known Text representation of the Spatial Reference System description TEXT)X--ctablegpkg_tile_matrixgpkg_tile_matrix CREATE TABLE gpkg_tile_matrix (table_ hAxO&U*) $#     ) cache?E|?E|) cache?E|?E|) cache@E|@E|) cache@E|@E|' cache@@@#E|@#E|' cache @3E|@3E|' cache @CE|@CE|' cache @SE|@SE|' cache @cE|@cE|' cache @sE|@sE|' cache@E|@E|' cache@E|@E|% cache@@@E|@E|% cache @E|@E|% cache@E|@E|% cache@E|@E|% cache@E|@E|$  cache@E|@E|"  cacheAE|AE| sg[OC7+   cache cache cache cache cache cache  cache cache cache cache cache cache cache cache cache cache cache  cache  cachename TEXT NOT NULL, -- Tile Pyramid User Data Table Name zoom_level INTEGER NOT NULL, -- 0 <= zoom_level <= max_level for table_name matrix_width INTEGER NOT NULL, -- Number of columns (>= 1) in tile matrix at this zoom level matrix_height INTEGER NOT NULL, -- Number of rows (>= 1) in tile matrix at this zoom level tile_width INTEGER NOT NULL, -- Tile width in pixels (>= 1) for this zoom level tile_height INTEGER NOT NULL, -- Tile height in pixels (>= 1) for this zoom level pixel_x_size DOUBLE NOT NULL, -- In t_table_name srid units or default meters for srid 0 (>0) pixel_y_size DOUBLE NOT NULL, -- In t_table_name srid units or default meters for srid 0 (>0) CONSTRAINT pk_ttm PRIMARY KEY (table_name, zoom_level), CONSTRAINT fk_tmm_table_name FOREIGN KEY (table_name) REFERENCES gpkg_contents(table_name)) 22s*?S-indexsqlite_autoindex_gpkg_tile_matrix_1gpkg_tile_matrix 557tablegpkg_tile_matrix_setgpkg_tile_matrix_setCREATE TABLE gpkg_tile_matrix_set G[5indexsqlite_autoindex_gpkg_tile_matrix_set_1gpkg_tile_matrix_setS tablecachecacheCREATE TABLE cache (id INTEGER PRIMARY KEY AUTOINCREMENT, -- Autoincrement primary key zoom_level INTEGER NOT NULL, -- min(zoom_level) <= zoom_level <= max(zoom_level) for t_table_name tile_column INTEGER NOT NULL, -- 0 to tile_matrix matrix_width - 1 tile_row INTEGER NOT NULL, -- 0 to tile_matrix matrix_height - 1 tile_data BLOB NOT NULL, -- Of an image MIME type specified in clauses Tile Encoding PNG, Tile Encoding JPEG, Tile Encoding WEBP UNIQUE (zoom_level, tile_column, tile_row)) @;no_gpkg_spatial_ref_syssE|sE|AsE|AsE|/cache 1sE|sE|AsE|AsE| ;no_gpkg_spatial_ref_sys cache (table_name TEXT NOT NULL PRIMARY KEY, -- Tile Pyramid User Data Table Name srs_id INTEGER NOT NULL, -- Spatial Reference System ID: gpkg_spatial_ref_sys.srs_id min_x DOUBLE NOT NULL, -- Bounding box minimum easting or longitude for all content in table_name min_y DOUBLE NOT NULL, -- Bounding box minimum northing or latitude for all content in table_name max_x DOUBLE NOT NULL, -- Bounding box maximum easting or longitude for all content in table_name max_y DOUBLE NOT NULL, -- Bounding box maximum northing or latitude for all content in table_name 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))  8PNG  IHDRkXTPLTE݋(('777GGGvutggfVVU@@?à`_^   =Q+=Q+indexsqlite_autoindex_no_gpkg_content_1no_gpkg_contentz CtablebuildingsbuildingsCREATE TABLE "buildings" ( "fid" INTEGER PRIMARY KEY AUTOINCREMENT, 'geom' POLYGON, 'OGC_FID' INTEGER, 'OSM_ID' TEXT(32), 'OSM_WAY_ID' TEXT(9), 'NAME' TEXT(50), 'TYPE' TEXT(32), 'AEROWAY' TEXT(32), 'AMENITY' TEXT(16), 'ADMIN_LEVE' TEXT(32), 'BARRIER' TEXT(32), 'BOUNDARY' TEXT(32), 'BUILDING' TEXT(12), 'CRAFT' TEXT(32), 'GEOLOGICAL' TEXT(32), 'HISTORIC' TEXT(32), 'LAND_AREA' TEXT(32), 'LANDUSE' TEXT(32), 'LEISURE' TEXT(32), 'MAN_MADE' TEXT(32), 'MILITARY' TEXT(32), 'NATURAL' TEXT(32), 'OFFICE' TEXT(32), 'PLACE' TEXT(32), 'SHOP' TEXT(11), 'SPORT' TEXT(32), 'TOURISM' TEXT(6), 'OTHER_TAGS' TEXT(116))P ++Ytablesqlite_sequencesqlite_sequenceCREATE TABLE sqlite_sequence(name,seq)) =indexsqlite_autoindex_cache_1cache - no_gpkg_contents  buildings cache ?s NONE undefined @s NONE undefined f= WGS 84 / Pseudo-Mercatorepsg PROJCS["WGS 84 / Pseudo-Mercator",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"]] ,AUTHORITY["EPSG","9122"]]AUTHORITY["EPSG","4326"]],PROJECTION[ "Mercator_1SP"],PARAMETER["central_meridian",0],PARAMETER[ "scale_factor",1],PARAMETER["false_easting",0],PARAMETER[ "false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS[ "X",EAST],AXIS["Y",NORTH] oZ1% Not providedepsg 1 Added via Mapproxy. fWGS 84epsg 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"]], AUTHORITY["EPSG","4326"]] >!_IDATxv*K@p\N& n{3 H xqa$%<`7ȰIee*w =qm3&J2*n(Z6\Vm^c*8|-9725^m93 ɶIFv{Ml# >EQ9LdmNaK‰Q8`%JP”Vb#ί-ۭ;Am YJ%D-NQ7 \Xm֘x6"%,3̲" mdfE% g(#bDJZn $' g!a&M*l8a gOI$VYcC ݉0LF:q:aRwPda܂&R8](P ݊f;5noNEaK_+ ao,8l+ڽ!'*(p %pib6Iv3lb1$"R(H(Ou&ۓ#N^P RD+2۾f&b> @#*LdG"e‰HbBbR}! ݪ^3ITVV$K'TIٶ2%V{QG#*Jn[6c"+@xSXϵ=CaKP,,5K dp챜jD $\ ereu6l_$)J@.,1,=cmw~ϣW3ja;5df)m'w ጢfv2WUɂE)8?j{ D)wÜlOb7d aR D'sgڌPq q 5 V[f$$|KfoƟby7*LĊv* 9VV{,1l+5 g"ᯤɨ6 R JjceDvJ;=-76#oiu$iY-ʕ=a=ALHDf%`n\#dk$@a=;5"0=fBdO ͞ć%jlTClKvokK2ٽJT鿴¢(rf8 f;kFĹ_ N²pl'Spm'pRI\V)'a; #w$v:qi".ZV ,lRᤱYaX NrMN>xGX$Iah:YlI|\ʞg$ <(l!H@04 `>K4J&b8 @ؑ,ADT9(| Y7ZoE0'B0@زtG(GBdk=5ZWAȟ$I%I|{p>b%DHj(H*Hj{*YE8l P[#YjUzGrb=s8'E qӪ"#,aInlO$! =)H95B59rND(ʞ"R%?"40ST{>H!  EJYpOd)0"FEHE2 FkZ,xvNTHj/G[lf\S;``cU©D2LA6{_$ NюIC ֵ I9Nّ2dJAvZz&j#FC^c Fo)gEW8^]N'(d+pѪ]Pev * l1DE`0ۭ\Qp"E^k!HvJv)Jd.IKZ Z6 TEjDqvQ=F"N%dxEeEN$&;IvY=QwDJĝf)|${j!Al`jl.I*8QaxMj"jSf5Zp•oSdUvIvR$vM՞)' )HxXrOQ)sD=Ɏw(Z?Y %-wu=O!@@x [6]T&!LD HvOהZ+Ol..`V l*Tav NAvA5Y<vω#E d N jvI.)72{Dwjw/PH.)T[A)X+SKRK[)[ D5g$FXvqMx [k;g"ZCXLvq)GYl'Q#Evq9`fl۬'$Fc8l w)(jؓ+*UXs(+w# !LgAALvGmm9 EOS Pv 'ZYuTE!J*I{6H&\Sxس+ѲrOW)GT PS)-Vv:@-J^R j"ٗ*bz## Z#Eն>(* jgC(}qlpj;b3A }uDDQp˝0Y@EULv^8P/W Ea jgèkRع0z0ٹ پ^n=۹ %AJw0707о+پ)] %AҤwI K34M}i˿)/mmS}ц?5;m`;Ҭ\O 0q~c w7+W~ im=Ҥ\ZW~`_w#,~Wu>>|Oj#w/7~{5IHp3굻_eUͦͦ،f 17]6>}cn\ͦ[>b\W?^Ttt1ތ>Uf\W͸nWo0yc\%n)R9%i2w>YҕfiG7,]nڷ&p݇ wƇwڵO_en,fGᣛ?||MtfmMt>|7~cgGC;Qϥܯ}p݇7aޮ>||V6>}cn\p݇s<_s>}ÇM0woWc >f>6|ܮ|2&>|1CQx&MJJ9~ow>|uM3, ~ͷUra݇>U݇>}tkx~ݭ݇>|{~}$KÇtnmM7w|:, Tq]fCs>}p؄lb{137cv3n'w1cq]}?{Շw>}161<6><cp#c o lbt{23?1|UonTXzI5g8=V+G˪w /}kPIENDB` kko 47 50 00 03 e6 10 00 00 e3 d1 7c 84 50 08 30 c0 a7 6e d3 44 44 08 30 c0 c9 a3 65 ee 6b 22 28 40 86 ce c6 a5 85 22 28 40 01 03 00 00 00 01 00 00 00 09 00 00 00 b1 dc d2 6a 48 08 30 c0 86 ce c6 a5 85 22 28 40 a7 6e d3 44 44 08 30 c0 2c d6 70 91 7b 22 28 40 34 5f ca 65 48 08 30 c0 5f 16 c9 0d 75 22 28 40 46 b6 4e b7 47 08 30 c0 06 eb a4 63 73 22 28 40 cd 59 9f 72 4c 08 30 c0 c9 a3 65 ee 6b 22 28 40 e3 d1 7c 84 50 08 30 c0 41 b5 66 d0 75 22 28 40 1a c3 9c a0 4d 08 30 c0 8c 6e 18 60 7a 22 28 40 25 e1 e7 64 4e 08 30 c0 c6 80 91 3c 7c 22 28 40 b1 dc d2 6a 48 08 30 c0 86 ce c6 a5 85 22 28 40 yes Y.PNG  IHDRkXTPLTE雵޹̔ΩΘ̱ɩdžɗР#  VV?S-indexsqlite_autoindex_no_gpkg_contents_1no_gpkg_contentsMa;indexsqlite_autoindex_no_gpkg_spatial_ref_sys_1no_gpkg_spatial_ref_sys!;;5tableno_gpkg_spatial_ref_sysno_gpkg_spatial_ref_sys CREATE TABLE no_gpkg_spatial_ref_sys (id INTEGER PRIMARY KEY AUTOINCREMENT, -- Autoincrement primary key zoom_level INTEGER NOT NULL, -- min(zoom_level) <= zoom_level <= max(zoom_level) for t_table_name tile_column INTEGER NOT NULL, -- 0 to tile_matrix matrix_width - 1 tile_row INTEGER NOT NULL, -- 0 to tile_matrix matrix_height - 1 tile_data BLOB NOT NULL, -- Of an image MIME type specified in clauses Tile Encoding PNG, Tile Encoding JPEG, Tile Encoding WEBP UNIQUE (zoom_level, tile_column, tile_row))   Ma;indexsqlite_autoindex_no_gp?S-indexsqlite_autoindex_no_gpkg_contents_1no_gpkg_contentsz--'tableno_gpkg_contentsno_gpkg_contentsCREATE TABLE no_gpkg_contents (id INTEGER PRIMARY KEY AUTOINCREMENT, -- Autoincrement primary key zoom_level INTEGER NOT NULL, -- min(zoom_level) <= zoom_level <= max(zoom_level) for t_table_name tile_column INTEGER NOT NULL, -- 0 to tile_matrix matrix_width - 1 tile_row INTEGER NOT NULL, -- 0 to tile_matrix matrix_height - 1 tile_data BLOB NOT NULL, -- Of an image MIME type specified in clauses Tile Encoding PNG, Tile Encoding JPEG, Tile Encoding WEBP UNIQUE (zoom_level, tile_column, tile_row))$=(%IDATxvTۖ-a\vp2&{׿|?"$$dA%p?\p?\.JJw圵Vo&k9%UTU4~/ҥtȪ%1T)VjY)G>R4FvTާ4~#k-mH@7~#nd:)Qd0~#(QMIa6?g4VU'IQ -j]Gt~GW:"%ޒHJ\zZ~OIT5XED&xoki,3rYxJ-&UV9WQi~FY%YO9;rQU2zis:"*%>HiY_ƙs\Ȏ RUUIx*_PEVx{}LU4gn3H~Iޖ%ok$m3'zu_]R[uRly+RA5rd2i<#*Tg[)Na|Ǎ?**7\T*FWe$NV 0$Rig@.*dH|/FCҲl)Vm6mkH|~NK[/=΅KȉʙH5rQҒƉRRG4 E|(qHV>ɪP[ݸYx'FX0qJo6`MRkk&z“J񍓕QK㝜bt t; 9ei^U;x(G蹬eim^yx3'ykwéG7[-lxی ) @.+QiOqbgWC- # ^`)HugP%֥=I^Y8&-&SxNmOXjZg UҒ;5zۑ >RJF$HsQ9IUU.M\mJO^7ܣmճ5jQ 6'F$%Rd$FTIuwz{U^n4N 1FK[W v#xJ`!) IgU{(%VR;S;9ŚƉSdoW1R$E O 1l|g,To˥WRYSHcoEqH_bHv0;#1|챬c ٓSUÝ.U>RƍL1]-jp')Uom{;#%Vjd810IUԞku*ђƙS|V-Ɖv#d(qGWtp"HD,- w;jRKC Q)DV* ٍG٤Ij;v|hT*5.KwS"ksZJJ[SJ6O;8|̦-$XEJ?jm)-9ø 3QYIJ*e RG.mOԍ\$>iFWh:UJpIݙ@vE,D5?* U*ʞzKVGFP3܂Ӝ%ȈLUK"Mdoij8k|D3XRUZɪEdD+ ۨHK%Q<=5`IUI&J Q|@3X'GZHj8ciI'k=Ksؗ< X.?HG=  >FU"4|f7+8;E//gd.K8+CuxaKuݮށZҰq/{vVUgE9!8X;; սJY'wFTwl,W_eԩssSwbkVd[\\GZ; nd0r2Fu73TyxHx($Rmw`""YW_n=3{e_PH$UyG}A8Ej59? Vøƣ%1U+#ŦKø^_$!eJ\`(j#3T97%b',xKjirQ7+1(5=U {s6F/r]#JY".E?]̫Mٔ BE3e J3GK[m *l JW(=Tk0!q (H|`cS {Pf)g%Jـ r9z[/'ڂ*i |ED|e/:#wiWeRj((G_E)e2g߈[ˑ=m|/FBrQRmiAa#]rucډHEu*Q< Y$#b3IZO _eE֑뜊QS˅߮y([\C)]2Kn7AV$"e+ v0x+4/p4x#yR"H ޖ4QKM)}Mufn ͢M)enx+v}9cFh^ٮixђ0x#:Di\`zZϫ$Fe~* 5K߿H*Nvs!j^6&xF 'g WN]mP h )xG˔rU}͗/_6)s*Ej-J$N8eUygy lwl۞F(RY̫ "T6?*.M:,PW(MlȑR((((%HFa~ZœͦF0T.Pz; dx m--?[Kolk?vsPoD)ARQdOB E0flf)e?_ uFYw0rᲐ`gh&۵m#oġ7Tyj])S< oW27 v8sdzbxV/qI*oyռST,J _)[*TIġl}3z@:gcYTOb1n۞Tx >lO0wdC-MKs69Y#uYy֖+Ϛ(m*m{\\+/ 9K);F"i')oaseY;˗ChWI*\6z&^ēv AŒaM F)2 8硔]r7Bec**JlĸW_fJp_aM֍KH횋v6e F)8CվԈZYNH[z/ J_7]k[b(Th3[k*vMϰGs QJD-C,z.qR~G8_kYNƜrR G I<#/K$E]_l-lu<'S] Q-`IUJٗم♶6pinfnW3|xţE۞62mKhxZ4墨2,(Jd ž-e(kO“qXf)QLG^g~kbnT`0`;$mm*YR'* sϳМ}iq瑶0v%v 247Ҝ,q NR8r_\[<Ԫ0p+8 P/Ƴ<2̄=z/8\,s΋9\2/. +%r b禬+;GQ{ظ#xT6|6s Yo̫klJD\vQUMs*/T[a`(1.'N](Ws3Kh+_97Ws*xD)Q#C&VqRf Tr4VsR#jW\/@WT)Sr63&Qt("ٗʭU>lx~N4z?Y[y̢Q>1v[K
Ai=|*yqd=0QȾZW*["[P/x'fs(׍y[16i7|ܮ[5L2} #ݥm W3i7XH,s>`)i7Qf)D#xMQ}&W&2* x_#?/$#'{V gmL!24SK© e*BMv*x\ER ^lqd3;0ĝtĻ"vdD7J^"BsSJDw"xJF%)h}pbl2[bux#{f Fir+srQJSGihYaec&YGv5'SM4PNfTHjF*>mܱ,%+zrQ&>Xeo%^c,qtEeֈY6>w2V `u5SNKrK|8jww<2ԌЅ.6JirK2J>8w dLSU|Bȸᦖ;p4;q202^fT,3%4g NRpx C|Ίݸ82f9fR<\8d0r F)1aBi`WYx 4RJ3HJ,*(% |i{峖[6n>4dM7A6om(EQ7cxgw7 J Q6W3~rJQUsqb!>owȹzVۭ(bƏ0>ONũ< D?W>kwȣG9Bxq(r"wQ]({N$Q^x&\>w3x.N#Ərd2a*qca<;ug&ٳm[q8NMFx9|3"F⁡#:xxAoc<6qr-0Nf$Bid '8VvW\(>v3x'^f Cfj#\J !ؽ𐳫>SJRmk^ HjF0>S3Hj5~; >S $>&ƍ m t|isf|,$ |ghPKC8lr!3g&>Enq(8 4>5PF8CA*_ǣq#QJJhƍ(NM_5L4' |' p.-_Oˎe ۈf:O_>ST)i=6>Rg5'l70>3 |c(vI>тa3>l%ux%U\z^v3#ܓ7!Ts6 6KUk"ި"\[[MONc$1lqbtiUl|Nc#xE#-a'8I=?'|0 4-Jl_5{oZk.'V+B_>;o55F-nU鈿ZOho-+<IYxUxSҚ8 gkwR/;$~LWU]p#$Ƴ|._,# .> MmFs<*Vx7*:<7j7w gY.uUd4d;[n^emY:iXxOLvfv騵H|A3%iW g[to2DϫOqwQ o6'- <%~J|AUVNix)a <Rmxh@xhy :?@m)* <͞?DqH7 RR!zFm4}Mo,~ oDT%̀N% ,G]RT1|\*?XUe"$ N<g -?[lg -?[lg -?[lg -?[l?u2IENDB`mapproxy-1.11.0/mapproxy/test/system/fixture/cache.mbtiles000066400000000000000000000200001320454472400237420ustar00rootroot00000000000000SQLite format 3@ - Gs?indexidx_tiletilesCREATE UNIQUE INDEX idx_tile on tiles (zoom_level, tile_column, tile_row)6KtabletilestilesCREATE TABLE tiles ( zoom_level integer, tile_column integer, tile_row integer, tile_data blob) 8PNG  IHDRkXTPLTE݋(('777GGGvutggfVVU@@?à`_^  >!_IDATxv*K@p\N& n{3 H xqa$%<`7ȰIee*w =qm3&J2*n(Z6\Vm^c*8|-9725^m93 ɶIFv{Ml# >EQ9LdmNaK‰Q8`%JP”Vb#ί-ۭ;Am YJ%D-NQ7 \Xm֘x6"%,3̲" mdfE% g(#bDJZn $' g!a&M*l8a gOI$VYcC ݉0LF:q:aRwPda܂&R8](P ݊f;5noNEaK_+ ao,8l+ڽ!'*(p %pib6Iv3lb1$"R(H(Ou&ۓ#N^P RD+2۾f&b> @#*LdG"e‰HbBbR}! ݪ^3ITVV$K'TIٶ2%V{QG#*Jn[6c"+@xSXϵ=CaKP,,5K dp챜jD $\ ereu6l_$)J@.,1,=cmw~ϣW3ja;5df)m'w ጢfv2WUɂE)8?j{ D)wÜlOb7d aR D'sgڌPq q 5 V[f$$|KfoƟby7*LĊv* 9VV{,1l+5 g"ᯤɨ6 R JjceDvJ;=-76#oiu$iY-ʕ=a=ALHDf%`n\#dk$@a=;5"0=fBdO ͞ć%jlTClKvokK2ٽJT鿴¢(rf8 f;kFĹ_ N²pl'Spm'pRI\V)'a; #w$v:qi".ZV ,lRᤱYaX NrMN>xGX$Iah:YlI|\ʞg$ <(l!H@04 `>K4J&b8 @ؑ,ADT9(| Y7ZoE0'B0@زtG(GBdk=5ZWAȟ$I%I|{p>b%DHj(H*Hj{*YE8l P[#YjUzGrb=s8'E qӪ"#,aInlO$! =)H95B59rND(ʞ"R%?"40ST{>H!  EJYpOd)0"FEHE2 FkZ,xvNTHj/G[lf\S;``cU©D2LA6{_$ NюIC ֵ I9Nّ2dJAvZz&j#FC^c Fo)gEW8^]N'(d+pѪ]Pev * l1DE`0ۭ\Qp"E^k!HvJv)Jd.IKZ Z6 TEjDqvQ=F"N%dxEeEN$&;IvY=QwDJĝf)|${j!Al`jl.I*8QaxMj"jSf5Zp•oSdUvIvR$vM՞)' )HxXrOQ)sD=Ɏw(Z?Y %-wu=O!@@x [6]T&!LD HvOהZ+Ol..`V l*Tav NAvA5Y<vω#E d N jvI.)72{Dwjw/PH.)T[A)X+SKRK[)[ D5g$FXvqMx [k;g"ZCXLvq)GYl'Q#Evq9`fl۬'$Fc8l w)(jؓ+*UXs(+w# !LgAALvGmm9 EOS Pv 'ZYuTE!J*I{6H&\Sxس+ѲrOW)GT PS)-Vv:@-J^R j"ٗ*bz## Z#Eն>(* jgC(}qlpj;b3A }uDDQp˝0Y@EULv^8P/W Ea jgèkRع0z0ٹ پ^n=۹ %AJw0707о+پ)] %AҤwI K34M}i˿)/mmS}ц?5;m`;Ҭ\O 0q~c w7+W~ im=Ҥ\ZW~`_w#,~Wu>>|Oj#w/7~{5IHp3굻_eUͦͦ،f 17]6>}cn\ͦ[>b\W?^Ttt1ތ>Uf\W͸nWo0yc\%n)R9%i2w>YҕfiG7,]nڷ&p݇ wƇwڵO_en,fGᣛ?||MtfmMt>|7~cgGC;Qϥܯ}p݇7aޮ>||V6>}cn\p݇s<_s>}ÇM0woWc >f>6|ܮ|2&>|1CQx&MJJ9~ow>|uM3, ~ͷUra݇>U݇>}tkx~ݭ݇>|{~}$KÇtnmM7w|:, Tq]fCs>}p؄lb{137cv3n'w1cq]}?{Շw>}161<6><cp#c o lbt{23?1|UonTXzI5g8=V+G˪w /}kPIENDB`mapproxy-1.11.0/mapproxy/test/system/fixture/cache_band_merge.yaml000066400000000000000000000026761320454472400254330ustar00rootroot00000000000000services: demo: wmts: wms: md: title: Foo layers: - name: dop_l title: DOP L sources: [dop_l_cache] tile_sources: [dop_l_cache] - name: dop_0 title: DOP 0 tile_sources: [dop_0_cache] - name: dop_021 title: DOP 021 tile_sources: [dop_021_cache] - name: dop_0122 title: DOP 0122 tile_sources: [dop_0122_cache] caches: dop_l_cache: grids: [GLOBAL_WEBMERCATOR] disable_storage: true sources: l: [ {source: dop_cache, band: 0, factor: 0.25}, {source: dop_cache, band: 1, factor: 0.7}, {source: dop_cache, band: 2, factor: 0.05}, ] dop_0_cache: grids: [GLOBAL_WEBMERCATOR] disable_storage: true image: mode: RGB sources: l: [{source: dop_cache, band: 0}] dop_021_cache: grids: [GLOBAL_WEBMERCATOR] disable_storage: true sources: r: [{source: dop_cache, band: 0}] g: [{source: dop_cache, band: 2}] b: [{source: dop_cache, band: 1}] dop_0122_cache: grids: [GLOBAL_WEBMERCATOR] disable_storage: true sources: r: [{source: dop_cache, band: 0}] g: [{source: dop_cache, band: 1}] b: [{source: dop_cache, band: 2}] a: [{source: dop_cache, band: 2, factor: 0.25}] dop_cache: grids: [GLOBAL_WEBMERCATOR] sources: [dop_wms] sources: dop_wms: type: wms req: url: http://localhost:42423/ layers: dop globals: image: paletted: falsemapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/000077500000000000000000000000001320454472400233625ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/dop_cache_EPSG3857/000077500000000000000000000000001320454472400264345ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/dop_cache_EPSG3857/00/000077500000000000000000000000001320454472400266535ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/dop_cache_EPSG3857/00/000/000077500000000000000000000000001320454472400271525ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/dop_cache_EPSG3857/00/000/000/000077500000000000000000000000001320454472400274515ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/dop_cache_EPSG3857/00/000/000/000/000077500000000000000000000000001320454472400277505ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/dop_cache_EPSG3857/00/000/000/000/000/000077500000000000000000000000001320454472400302475ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/dop_cache_EPSG3857/00/000/000/000/000/000/000077500000000000000000000000001320454472400305465ustar00rootroot00000000000000000.png000077500000000000000000000013701320454472400315000ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/dop_cache_EPSG3857/00/000/000/000/000/000PNG  IHDR?1IDATxA@DIX]}64f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H{^Y%IENDB`mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/wms_cache_EPSG900913/000077500000000000000000000000001320454472400266175ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/wms_cache_EPSG900913/01/000077500000000000000000000000001320454472400270375ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/wms_cache_EPSG900913/01/000/000077500000000000000000000000001320454472400273365ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/wms_cache_EPSG900913/01/000/000/000077500000000000000000000000001320454472400276355ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/wms_cache_EPSG900913/01/000/000/000/000077500000000000000000000000001320454472400301345ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/wms_cache_EPSG900913/01/000/000/000/000/000077500000000000000000000000001320454472400304335ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/wms_cache_EPSG900913/01/000/000/000/000/000/000077500000000000000000000000001320454472400307325ustar00rootroot00000000000000001.jpeg000066400000000000000000000237461320454472400320360ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/wms_cache_EPSG900913/01/000/000/000/000/000JFIFHHC  !"$"$C"@ !1AQa"q2#BRT$35bs%r+!1"AQ2qa3C ?T+t()JJRR()JJRR()JJRR()JJRR()JJRR()JJRRv"2 ns2{dgHIeF.OJnk+wVmŔ?^=F+6OHmv$*r2Az^oEW,`_2HdIM$jL!%!JUbQ$8$zѼ,A29^٦{@$sJ K9.loVA'@T60;qy'КMm\TCF6=TzW_/젖w%0 16Gxӭ`'CWڀ?wv9鼲㎜jIib}Ρqd ;OҼTܶͣ˴Q;"tiWJRR()JJRR()JJUe 2@<W2xGpb ^--$ΐg領7.8'p+ic4Qp N h`mҤskdX[J5FxU#Olpk&Dָ֮-aPEq*,qʅ%Et=UAZ}м&9f`{f8)w#f(^i nr8#'֮x$=VW:d_ˆs`,tˍRhYF$`6x\s݋k<ְ)qS'=}%Z-L-!q!Y%OOn5z {+N*~7Vn4x̅ ΝgȺťhm"rc z"^5¬n!À2K~/r(t?!C{⣖,ƘG5:~n5y,l0 r699ǽoݼ1B#BUʫ)djpN8Ϧ`]xȭ4n?,RFPO|KiFp OXbQv֭ OYµ,חMxD1jαk{@ǜ=20z{TS׏s@_Xn'&K;U'E_Ifdp\{Ԙ@*7K2?;G%st*eP2iӂhRN)JXzPBҩ6r9ǩ}3Yx}Vfҭ7g#lΫ}>}%#;RoK K-8&EĪqzg'5]iGyGYgQB^( R)@)JP R@Mdv7Z[(2?rpw'گ_K߭T Sn#v@QĞ,*r[E-"6|=OZb3o_)~;\`?jQW,0x6~f@ d;>`ÀsμKe8cq 1/6,=F%<ӡjsot-GJ@9 ӹϠ5KǺeMElvPG=sFA%q ,OڸnKlI#dW]jz~swwO"Zx]A٘K4جsa`Vvy\YC4B1hEQa5J䗦, 9G,xOә`vcp'+zc `[ VPfD(+:6G=j!{1Y6)v*JxR^F+q@. !I9VlNm:$0l$3ҮVi%av㏦+:-R()JKN.H:~sZH]^\Y\np<,+sNӂMLRʂ)@)JP Rƍ$ G< mtN+]>KK[B/ rsVn!"HT@p=NtMN8єJi vgW-/Rxl海L ѰR:u֩NUMYM )ae$l!VE0sڱnK9-22q#{1ۚ\:I>m$]\u#1Vy$ir++dۗ Jʡsehn!(rCp眃Y]\ PX@D|+|qڴr-$ԱFc$V]MZ; HV) 㓌cQʹeCYaꖛ*!pTcH 4Xh΅IO/~q\V<_L2"jI" =k>'Sd9RWA`c:wsIieHfcԓYE1\H)B?CX)323evIH-P㞞_zs%߈a1.dNCPkNɤG1%=R$nT4:+ND.v5MJRWUdeoFҽUJIӥH綘+6T`uRA>+?ZeilhъP@%*sӟ^M`V rf)JTAJ!pkvA.qXyYGV4y$XFwcU'NMRBGIlvT%@'vf+Ke AHREd6ZU 䏵cVFy=wv225m)&:qrbd3ߏj*QhkK;Q?!.|OHAoVߺČ:;ujeKo%P]}` Д\ldr,|698fkZou& ț]fIܥI=yN&<RyprWmp1Hՙ峹ۚl.+h![@J'8/P4].Nsv K 㜂::P5@iwd9!JFs18麔.20I?ګ'2 ts~ Q\Em*E#9OQk]S_j= FgZ*F9K֡k$ r@dg*8X%1p=sֺa_[TVB@5q16}HIFAq:]ҦMFGF9': :$\Zhq*q֦puHeQ[BRB *t╷X0mRHG T^MS1 qyHdzuTX."Dy_3T?NTȨ)6̱vUG[kXySjDZkI˖46TWGYXd{ZFG&n:ykkҨ)5->H>=>ye9#Vn;*-hzIMJc㏽t_y5VkH^,[J`0G4oĬ RsāxF{04锺qN@'OZkjo)K E-ěs,czAԴ{ege`FZohkqE{d*# =G"ZMmN#L`.GA4(m@Y; j;Mr@Z6@xnN׈ pIfvRRUKTX+ooL3jd|q܃'=kȪlnH^cquC5Ly#oR?N.K{xF2ǿ%PrlxA4:/^U݉灐A#)дX?)*?% ڥGF8}Lf>S`Tr=F:s[]L K#c'"O8Ԛi⑬Lp (`vj?a>ե 9=6ظ8{l &&Siֿ6yc #g8ELx-6(b\p\AE'9>޾)]Sj?Jh]Ŕ_i󓰬{<5r+J#drORCIau#ёdO]OIy$ҭUeB˴d\jKW0pJDy'9Ϯvm-M3BBP@yYԓӥ? [2iJq4VI<#5,z:vRve~GΠ/Ѵo˨ff9z03cK_\M$w[E,O&)YT='ֵ0ڴJ/2e;uqKҩZs)JG$k";dϵX^6n䄏nP8>cגr\OzE'߹& vw#U$Fv,z}8ԫ؆;@1&$)M@|=ɫ+jQ-#|=Cn9r IKV^YkO)J++eJR ]_y5X*k*”x)J˪u `2T71x㸒nPX^3.qfv軀;uAXGmw^[hW7VHK\>18o^:wZ]Αq gCLMfa(YY3HY@`77'}O]rFct0e[kasvʒ+O4AUs~?Z隥lkF&B=ҹ混 ?69ru#YirW7IٞԨ#⠾f%ezqQtu=6; RaV*3(99V ekZ%)/'͌adnK\mnxG+v,kXR黌'>/-|3F ;fn1=N=SEUd "څBn=TE%׉32\S)# n= 5..A,baCIiW-[]G8I du`sQZGJP$Z/*ɽBønrWZFq&Ɵ{%ԾyE@<>Ƶ>8ӭ5hSp3GoluZ=$/;_>ՄI$I'Oz]S)p[S E+˰P UlqTӊP4<2PbhhB)vA<NkVz\wW7PE#eZ@ !UW.p#i8\=Z[wO|2tkQz@dH銁jEwwͅk5p:g95gĺ֮F 6Qq`ڠ`gONs®4UVϡJ=+V1n:.m?F[:Hpdp ~wDU$d9+s]OG[\ӫnIYa&e#^Ռ\iB3=+.8K Дv Rȥ)@+g!2}*1 6s +3;A=֒<ȃQ[G>g5zeǵAa `JBes36Kvru 6<[1z$19{eE Q$2쉏p>_"zoڵuVK&m=FqLt{o²sry?IoG?/Jޑf4dT3: \& #GSBM}tY.u3>z*R4+dy=\]O}}"mh@s4#,'&)JWJwB7pO5j3!#oqջաOdaƠFn{ێ?*€+I3;'B8P:US^G.)^R$sz0#=Z^ՠtد! 8A :{Vڴ sEJ!,&(8%?q>-];٫ȼoXvum74r(e%ysl`2J?p)0WcE+l`,ȲF*rޘKksou"A*}Asj-OZv(?r$UO늟Ow\q8)uAgG"@IYH^Yv{'7quwuy|-z^9$;&ճu9P'xQII`G [*Y0QǿZ`/`t#"WC,a]\2N*rA2,4)JW)@mM+.Qicګ%ۇSm|7mw^Ye rGϯޢyŠ6ˈ|=xށ6;K[y_i; /98 2x$+ ^cNkj)WLJRR(V9d3F#tWiVm\q y.#xziRZC1`OL[RumR^դ2,f'$hX]gYTp 6dGn9X?zuzRFH}֬"{ok'.@Bq9Z͕G$ZAKFuf껔39m-fu(3ڟAgޡ~1^]m7POMcsҥ9b'2\cXXN}DLv=Mj:|shMǽ9?*%[Œn'}z zU=Z j .~K7wBp#XUA'\ncWY-+1JRRnm6 9TS";9~8>ӭ5$Y"M˖$`qڤK;'JUgCҩ.콧ȵ% t*DR()J@H JPtuO ˬiI V =[J4Yl~}>޺cyo PL>_1J䃂2x'=MEp6*Yͅ)JJ)JP R}JK77r^;c;Tw DN -Z7I7J&dYe0k'wk: x.PPSM^wS*B${ sՉS("r+V4Mpv&u 8b"0 y#'$l/®/JǓ5RIaoB@=@5ZPP@٪Ҕ=o/")JJRK<+=:.ѣ@ŖU{QJTvcDZyGRu HA 9R֡_cRT{+a2q*N2rsH=ūXX'VO>[ķV5,{>FjPy-yI|]5[mw': Zh ao=J+\H-s c|Tn8F9YPQ<")s9AJU؜O[D1}:UrK"41ª}EvK $4 6(Fy V^G8Ǯ+k[XYMi~V%J>^A!zBYIt\+3`Ǔ^V/4H3y w[&7ɯc%׆t ܶ%p8\t:u&]X[|B22" I皞A5/+`|rs=3[6 #ŋœSu]:gUlcUsV" [ew"F9=Z09Z~|JTaJRVzf*ZJqpp18k\ k r܎bϪ]Y-9& 0!=ϏL: Ac H\cḚ4ݻ HQe S@Qz){Hmapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/wms_cache_transparent_EPSG900913/000077500000000000000000000000001320454472400312405ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/wms_cache_transparent_EPSG900913/01/000077500000000000000000000000001320454472400314605ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/wms_cache_transparent_EPSG900913/01/000/000077500000000000000000000000001320454472400317575ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/wms_cache_transparent_EPSG900913/01/000/000/000077500000000000000000000000001320454472400322565ustar00rootroot00000000000000000/000077500000000000000000000000001320454472400324765ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/wms_cache_transparent_EPSG900913/01/000/000000/000077500000000000000000000000001320454472400327755ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/wms_cache_transparent_EPSG900913/01/000/000/000000/000077500000000000000000000000001320454472400332745ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/wms_cache_transparent_EPSG900913/01/000/000/000/000001.png000066400000000000000000000007171320454472400343070ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/system/fixture/cache_data/wms_cache_transparent_EPSG900913/01/000/000/000/000/000PNG  IHDR\rfIDATxԱ 0A|~*c qϝ f8IENDB`mapproxy-1.11.0/mapproxy/test/system/fixture/cache_geopackage.yaml000066400000000000000000000017461320454472400254330ustar00rootroot00000000000000globals: cache: base_dir: cache_data/ services: tms: wms: md: title: MapProxy test fixture layers: - name: gpkg title: TMS Cache Layer sources: [gpkg_cache, new_gpkg, new_gpkg_table] - name: gpkg_new title: TMS Cache Layer sources: [new_gpkg] caches: gpkg_cache: grids: [cache_grid] cache: type: geopackage filename: ./cache.gpkg table_name: cache tile_lock_dir: ./testlockdir sources: [tms] new_gpkg: grids: [new_grid] sources: [] cache: type: geopackage filename: ./cache_new.gpkg table_name: cache tile_lock_dir: ./testlockdir new_gpkg_table: grids: [cache_grid] cache: type: geopackage filename: ./cache.gpkg table_name: new_cache tile_lock_dir: ./testlockdir sources: [tms] grids: cache_grid: srs: EPSG:900913 new_grid: srs: EPSG:4326 sources: tms: type: tile url: http://localhost:42423/tiles/%(tc_path)s.png mapproxy-1.11.0/mapproxy/test/system/fixture/cache_grid_names.yaml000066400000000000000000000015141320454472400254460ustar00rootroot00000000000000globals: cache: meta_size: [1, 1] meta_buffer: 0 services: demo: tms: use_grid_names: True kml: use_grid_names: True layers: - name: wms_cache title: Cached Layer sources: [wms_cache] - name: wms_cache_no_grid_name title: Cached Layer (not grid name) sources: [wms_cache_no_grid_name] caches: wms_cache: format: image/jpeg sources: [wms_source] grids: [utm32n] cache: type: file use_grid_names: True wms_cache_no_grid_name: format: image/jpeg sources: [wms_source] grids: [utm32n] cache: type: file use_grid_names: False grids: utm32n: srs: 'EPSG:25832' bbox: [5,50,10,55] bbox_srs: EPSG:4326 num_levels: 12 sources: wms_source: type: wms req: url: http://localhost:42423/service layers: bar mapproxy-1.11.0/mapproxy/test/system/fixture/cache_mbtiles.yaml000066400000000000000000000006321320454472400247750ustar00rootroot00000000000000globals: cache: base_dir: cache_data/ services: tms: wms: md: title: MapProxy test fixture layers: - name: mb title: TMS Cache Layer sources: [mb_cache] caches: mb_cache: cache: type: mbtiles filename: ./cache.mbtiles tile_lock_dir: ./testlockdir sources: [tms] sources: tms: type: tile url: http://localhost:42423/tiles/%(tc_path)s.png mapproxy-1.11.0/mapproxy/test/system/fixture/cache_s3.yaml000066400000000000000000000016451320454472400236700ustar00rootroot00000000000000globals: cache: s3: bucket_name: default_bucket services: tms: wms: md: title: MapProxy S3 layers: - name: default title: Default sources: [default_cache] - name: quadkey title: Quadkey sources: [quadkey_cache] - name: reverse title: Reverse sources: [reverse_cache] caches: default_cache: grids: [webmercator] cache: type: s3 sources: [tms] quadkey_cache: grids: [webmercator] cache: type: s3 bucket_name: tiles directory_layout: quadkey directory: quadkeytiles sources: [tms] reverse_cache: grids: [webmercator] cache: type: s3 bucket_name: tiles directory_layout: reverse_tms directory: reversetiles sources: [tms] grids: webmercator: name: WebMerc base: GLOBAL_WEBMERCATOR sources: tms: type: tile url: http://localhost:42423/tiles/%(tc_path)s.png mapproxy-1.11.0/mapproxy/test/system/fixture/cache_source.yaml000066400000000000000000000030241320454472400246340ustar00rootroot00000000000000services: tms: wms: srs: ['EPSG:4326', 'EPSG:4258', 'CRS:84', 'EPSG:900913', 'EPSG:3857', 'EPSG:31466', 'EPSG:31467', 'EPSG:31468', 'EPSG:25831', 'EPSG:25832', 'EPSG:25833'] md: title: test layers: - name: tms_transf title: transformed tile source sources: [tms_cache_out] - name: new_cache title: access to existing cache sources: [new_cache] - name: combined title: access to one compatible cache and one other sources: [cache_combined] caches: tms_cache_out: grids: [utm32n] meta_buffer: 0 meta_size: [2, 2] sources: [tms_cache_in] tms_cache_in: grids: [osm_grid] disable_storage: true sources: [tms_source] new_cache: grids: [sub_grid] sources: [old_cache] old_cache: grids: [osm_grid] sources: [tms_source] cache_combined: grids: [utm32n] sources: [cache_osm, cache_utm] cache_osm: grids: [osm_grid] sources: [tms_source] cache_utm: grids: [utm32n] sources: [tms_utm32n_source] sources: tms_source: type: tile url: http://localhost:42423/tiles/%(tc_path)s.png tms_utm32n_source: type: tile grid: utm32n url: http://localhost:42423/tiles/utm/%(tc_path)s.png grids: utm32n: srs: 'EPSG:25832' bbox: [4, 46, 16, 56] bbox_srs: 'EPSG:4326' min_res: 5700 osm_grid: base: GLOBAL_MERCATOR srs: 'EPSG:3857' origin: nw sub_grid: base: osm_grid bbox: [0, 0, 20037508.342789244, 20037508.342789244] min_res: 78271.51696402048 num_levels: 18 mapproxy-1.11.0/mapproxy/test/system/fixture/cgi.py000066400000000000000000000011431320454472400224410ustar00rootroot00000000000000#! /usr/bin/env python """ CGI script that returns a red 256x256 PNG file. """ if __name__ == '__main__': import sys if sys.version_info[0] == 2: w = sys.stdout.write else: w = sys.stdout.buffer.write w(b"Content-type: image/png\r\n") w(b"\r\n") w(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01\x00\x00\x00\x01\x00\x01\x03\x00\x00\x00f\xbc:%\x00\x00\x00\x06PLTE\xff\x00\x00\x00\x00\x00A\xa3\x12\x03\x00\x00\x00\x1fIDATx\x9c\xed\xc1\x01\r\x00\x00\x00\xc2\xa0\xf7Om\x0e7\xa0\x00\x00\x00\x00\x00\x00\x00\x00\xbe\r!\x00\x00\x01\xf1g!\xee\x00\x00\x00\x00IEND\xaeB`\x82')mapproxy-1.11.0/mapproxy/test/system/fixture/combined_sources.yaml000066400000000000000000000053341320454472400255420ustar00rootroot00000000000000globals: cache: base_dir: /tmp/cache_data/ meta_size: [1, 1] meta_buffer: 0 image: paletted: False # resampling: 'bicubic' services: wms: layers: - name: combinable title: Uncached combined layers sources: [wms1, wms3, wms4, wms2] - name: uncombinable title: Uncached layers sources: [wms1, wms2, wms3] - name: single title: Uncached combined layers sources: [wms4] - name: cached title: Cached combined layers sources: [wms_cache] - name: opacity_base title: opacity test base layer sources: [wms_opacity1] - name: opacity_overlay title: opacity test overlay layer sources: [wms_opacity2] - name: layer_image_opts1 title: layer with transparent_color sources: [wms_iopts1] - name: layer_image_opts2 title: layer with transparent_color sources: [wms_iopts2] - name: layer_fwdparams1 title: Uncached layer with fwdparams 1 sources: [wms_fwdparams1, wms_fwdparams2] - name: layer_fwdparams2 title: Uncached layer with fwdparams 2 sources: [wms_fwdparams3] caches: wms_cache: grids: [GLOBAL_MERCATOR] sources: [wms1, wms3, wms2] sources: wms1: type: wms req: url: http://localhost:42423/service_a layers: a_one transparent: True wms2: type: wms req: url: http://localhost:42423/service_b layers: b_one transparent: True wms3: type: wms req: url: http://localhost:42423/service_a layers: a_two,a_three transparent: True wms4: type: wms req: url: http://localhost:42423/service_a layers: a_four transparent: True wms_opacity1: type: wms req: url: http://localhost:42423/service_a layers: a_one wms_opacity2: type: wms image: opacity: 0.5 req: url: http://localhost:42423/service_a layers: a_two wms_iopts1: type: wms image: transparent_color: [255, 0, 0] req: url: http://localhost:42423/service_a layers: a_iopts_one transparent: True wms_iopts2: type: wms image: transparent_color: [255, 0, 0] req: url: http://localhost:42423/service_a layers: a_iopts_two transparent: True wms_fwdparams1: type: wms forward_req_params: ['time'] req: url: http://localhost:42423/service_a layers: a_one transparent: True wms_fwdparams2: type: wms forward_req_params: ['time', 'vendor'] req: url: http://localhost:42423/service_a layers: a_two transparent: True wms_fwdparams3: type: wms forward_req_params: ['time', 'vendor'] req: url: http://localhost:42423/service_a layers: a_three,a_four transparent: True mapproxy-1.11.0/mapproxy/test/system/fixture/coverage.yaml000066400000000000000000000033601320454472400240070ustar00rootroot00000000000000globals: cache: base_dir: cache_data/ meta_size: [1, 1] meta_buffer: 0 image: paletted: False # resampling: 'bicubic' services: tms: kml: wms: md: title: MapProxy test fixture abstract: This is MapProxy. online_resource: http://mapproxy.org/ contact: person: Oliver Tonnhofer position: Technical Director organization: Omniscale address: Nadorster Str. 60 city: Oldenburg postcode: 26123 country: Germany phone: +49(0)441-9392774-0 fax: +49(0)441-9392774-9 email: info@omniscale.de access_constraints: Here be dragons. layers: - name: wms_cache title: WMS Cache Layer sources: [wms_cache] - name: tms_cache title: TMS Cache Layer sources: [tms_cache] - name: seed_only_cache title: Seed Only Layer sources: [seed_only_cache] caches: wms_cache: format: image/jpeg grids: [GLOBAL_MERCATOR, GLOBAL_GEODETIC] sources: [wms_cache] tms_cache: format: image/jpeg grids: [GLOBAL_MERCATOR] sources: [tms_cache] seed_only_cache: grids: [GLOBAL_MERCATOR] sources: [seed_only_source] sources: wms_cache: type: wms supported_srs: ['EPSG:900913', 'EPSG:4326'] coverage: bbox: [10, 15, 30, 31] bbox_srs: 'EPSG:4326' req: url: http://localhost:42423/service layers: foo,bar tms_cache: type: tile coverage: bbox: [12, 10, 35, 30] bbox_srs: 'EPSG:4326' url: http://localhost:42423/tms/1.0.0/foo/%(tms_path)s.jpeg seed_only_source: type: tile seed_only: true coverage: bbox: [14, 13, 24, 23] bbox_srs: 'EPSG:4326' url: http://localhost:42423/tms/1.0.0/foo/%(tms_path)s.jpeg mapproxy-1.11.0/mapproxy/test/system/fixture/disable_storage.yaml000066400000000000000000000005701320454472400253430ustar00rootroot00000000000000globals: cache: base_dir: cache_data/ services: tms: kml: wms: md: title: MapProxy test fixture layers: - name: tiles title: Tiles without cache (disable_storage) sources: [tile_cache] caches: tile_cache: disable_storage: true sources: [tile_source] sources: tile_source: type: tile url: http://localhost:42423/tile.png mapproxy-1.11.0/mapproxy/test/system/fixture/empty_ogrdata.geojson000066400000000000000000000000551320454472400255530ustar00rootroot00000000000000{"type": "FeatureCollection", "features": []}mapproxy-1.11.0/mapproxy/test/system/fixture/formats.yaml000066400000000000000000000033231320454472400236660ustar00rootroot00000000000000globals: cache: base_dir: cache_data/ meta_size: [1, 1] meta_buffer: 0 image: paletted: False # resampling: 'bicubic' services: tms: wms: md: title: MapProxy test fixture abstract: This is MapProxy. online_resource: http://mapproxy.org/ contact: person: Oliver Tonnhofer position: Technical Director organization: Omniscale address: Nadorster Str. 60 city: Oldenburg postcode: 26123 country: Germany phone: +49(0)441-9392774-0 fax: +49(0)441-9392774-9 email: info@omniscale.de access_constraints: Here be dragons. layers: - name: jpeg_cache_tiff_source title: JPEG cache with TIFF source sources: [jpeg_cache_tiff_source] - name: png_cache_all_source title: PNG cache with all source sources: [png_cache_all_source] - name: jpeg_cache_png_jpeg_source title: JPEG cache with png and jpeg source sources: [jpeg_cache_png_jpeg_source] caches: jpeg_cache_tiff_source: format: image/jpeg use_direct_from_level: 2 sources: [tiff_source] jpeg_cache_png_jpeg_source: format: image/jpeg use_direct_from_level: 2 sources: [png_jpeg_source] png_cache_all_source: format: image/png use_direct_from_level: 2 sources: [all_source] sources: all_source: type: wms req: url: http://localhost:42423/service layers: allsource png_jpeg_source: type: wms supported_formats: ['image/png', 'image/jpeg'] req: url: http://localhost:42423/service layers: pngjpegsource tiff_source: type: wms req: url: http://localhost:42423/service layers: tiffsource format: image/tiff mapproxy-1.11.0/mapproxy/test/system/fixture/inspire.yaml000066400000000000000000000053141320454472400236660ustar00rootroot00000000000000globals: cache: base_dir: cache_data/ meta_size: [1, 1] meta_buffer: 0 tile_lock_dir: defaulttilelockdir image: # resampling: 'bicubic' paletted: False formats: custom: format: image/jpeg png8: format: 'image/png; mode=8bit' colors: 256 services: tms: kml: wmts: wms: image_formats: ['image/png', 'image/jpeg', 'png8'] srs: ['EPSG:4326', 'EPSG:4258', 'CRS:84', 'EPSG:900913', 'EPSG:3857', 'EPSG:31466', 'EPSG:31467', 'EPSG:31468', 'EPSG:25831', 'EPSG:25832', 'EPSG:25833'] bbox_srs: - bbox: [2750000, 5000000, 4250000, 6500000] srs: 'EPSG:31467' - 'EPSG:3857' md: title: MapProxy test fixture ☃ abstract: This is MapProxy. online_resource: http://mapproxy.org/ contact: person: Oliver Tonnhofer position: Technical Director organization: Omniscale address: Nadorster Str. 60 city: Oldenburg postcode: 26123 country: Germany phone: +49(0)441-9392774-0 fax: +49(0)441-9392774-9 email: info@omniscale.de access_constraints: Here be dragons. inspire_md: type: linked languages: default: eng metadata_url: url: http://example.org/metadata media_type: application/vnd.iso.19139+xml layers: - name: inspire_example title: Example layer with Inspire View Service metadata sources: [direct] md: abstract: Some abstract keyword_list: - vocabulary: Name of the vocabulary keywords: [keyword1, keyword2] - vocabulary: Name of another vocabulary keywords: [keyword1, keyword2] - keywords: ["keywords without vocabulary"] attribution: title: My attribution title url: http://some.url/ logo: url: http://some.url/logo.jpg width: 100 height: 100 format: image/jpeg identifier: - url: http://some.url/ name: HKU1234 value: Some value metadata: - url: http://some.url/ type: INSPIRE format: application/xml - url: http://some.url/ type: ISO19115:2003 format: application/xml data: - url: http://some.url/datasets/test.shp format: application/octet-stream - url: http://some.url/datasets/test.gml format: text/xml; subtype=gml/3.2.1 feature_list: - url: http://some.url/datasets/test.pdf format: application/pdf sources: direct: type: wms req: url: http://localhost:42423/service layers: bar coverage: bbox: [-180, -80, 170, 80] srs: 'EPSG:4326' mapproxy-1.11.0/mapproxy/test/system/fixture/inspire_full.yaml000066400000000000000000000067711320454472400247200ustar00rootroot00000000000000globals: cache: base_dir: cache_data/ meta_size: [1, 1] meta_buffer: 0 tile_lock_dir: defaulttilelockdir image: # resampling: 'bicubic' paletted: False formats: custom: format: image/jpeg png8: format: 'image/png; mode=8bit' colors: 256 services: tms: kml: wmts: wms: image_formats: ['image/png', 'image/jpeg', 'png8'] srs: ['EPSG:4326', 'EPSG:4258', 'CRS:84', 'EPSG:900913', 'EPSG:3857', 'EPSG:31466', 'EPSG:31467', 'EPSG:31468', 'EPSG:25831', 'EPSG:25832', 'EPSG:25833'] bbox_srs: - bbox: [2750000, 5000000, 4250000, 6500000] srs: 'EPSG:31467' - 'EPSG:3857' md: title: MapProxy test fixture ☃ abstract: This is MapProxy. online_resource: http://mapproxy.org/ contact: person: Oliver Tonnhofer position: Technical Director organization: Omniscale address: Nadorster Str. 60 city: Oldenburg postcode: 26123 country: Germany phone: +49(0)441-9392774-0 fax: +49(0)441-9392774-9 email: info@omniscale.de access_constraints: Here be dragons. keyword_list: - vocabulary: GEMET keywords: [Orthoimagery] - keywords: ["INSPIRE View Service", MapProxy] inspire_md: type: embedded languages: default: eng resource_locators: - url: http://example.org/metadata media_type: application/vnd.iso.19139+xml temporal_reference: date_of_creation: "2015-05-01" # as string metadata_points_of_contact: - organisation_name: Example Inc. email: bar@example.org conformities: - title: test date_of_publication: 2010-12-08 resource_locators: - url: http://example.org/metadata media_type: application/vnd.iso.19139+xml degree: notEvaluated mandatory_keywords: ['infoMapAccessService'] keywords: - title: GEMET - INSPIRE themes date_of_publication: 2008-06-01 keyword_value: Orthoimagery metadata_date: 2015-07-23 # as datetime layers: - name: inspire_example title: Example layer with Inspire View Service metadata sources: [direct] md: abstract: Some abstract keyword_list: - vocabulary: Name of the vocabulary keywords: [keyword1, keyword2] - vocabulary: Name of another vocabulary keywords: [keyword1, keyword2] - keywords: ["keywords without vocabulary"] attribution: title: My attribution title url: http://some.url/ logo: url: http://some.url/logo.jpg width: 100 height: 100 format: image/jpeg identifier: - url: http://some.url/ name: HKU1234 value: Some value metadata: - url: http://some.url/ type: INSPIRE format: application/xml - url: http://some.url/ type: ISO19115:2003 format: application/xml data: - url: http://some.url/datasets/test.shp format: application/octet-stream - url: http://some.url/datasets/test.gml format: text/xml; subtype=gml/3.2.1 feature_list: - url: http://some.url/datasets/test.pdf format: application/pdf sources: direct: type: wms req: url: http://localhost:42423/service layers: bar coverage: bbox: [-180, -80, 170, 80] srs: 'EPSG:4326' mapproxy-1.11.0/mapproxy/test/system/fixture/kml_layer.yaml000066400000000000000000000024641320454472400241770ustar00rootroot00000000000000globals: cache: base_dir: cache_data/ meta_size: [1, 1] meta_buffer: 0 image: # resampling: 'bicubic' paletted: False formats: custom: format: image/jpeg png8: format: 'image/png; mode=8bit' colors: 256 services: kml: grids: webmercator: base: GLOBAL_MERCATOR origin: nw layers: - name: wms_cache title: WMS Cache Layer with direct access from level 8 sources: [wms_cache] - name: wms_cache_nw title: WMS Cache Layer with direct access from level 8 sources: [wms_cache_nw] - name: wms_cache_multi title: WMS Cache Multi Layer sources: [wms_cache_multi] caches: wms_cache: format: image/jpeg sources: [wms_cache] wms_cache_nw: format: image/jpeg grids: [webmercator] sources: [wms_cache] wms_cache_multi: format: custom grids: [GLOBAL_GEODETIC, GLOBAL_MERCATOR] sources: [wms_cache_130] sources: wms_cache: type: wms supported_srs: ['EPSG:900913', 'EPSG:4326'] wms_opts: featureinfo: True req: url: http://localhost:42423/service layers: foo,bar wms_cache_130: type: wms min_res: 250000000 max_res: 1 wms_opts: version: '1.3.0' featureinfo: True req: url: http://localhost:42423/service layers: foo,barmapproxy-1.11.0/mapproxy/test/system/fixture/layer.yaml000066400000000000000000000136141320454472400233330ustar00rootroot00000000000000globals: cache: base_dir: cache_data/ meta_size: [1, 1] meta_buffer: 0 tile_lock_dir: defaulttilelockdir image: # resampling: 'bicubic' paletted: False formats: custom: format: image/jpeg png8: format: 'image/png; mode=8bit' colors: 256 services: tms: kml: wmts: wms: image_formats: ['image/png', 'image/jpeg', 'png8'] srs: ['EPSG:4326', 'CRS:84', 'EPSG:900913', 'EPSG:3857', 'EPSG:31466', 'EPSG:31467', 'EPSG:31468', 'EPSG:25831', 'EPSG:25832', 'EPSG:25833'] bbox_srs: - 'EPSG:3857' - bbox: [-180, -70, 180, 90] srs: 'EPSG:4326' md: title: MapProxy test fixture ☃ abstract: This is MapProxy. online_resource: http://mapproxy.org/ contact: person: Oliver Tonnhofer position: Technical Director organization: Omniscale address: Nadorster Str. 60 city: Oldenburg postcode: 26123 country: Germany phone: +49(0)441-9392774-0 fax: +49(0)441-9392774-9 email: info@omniscale.de access_constraints: Here be dragons. layers: - name: direct title: Direct Layer sources: [direct] - name: direct_fwd_params title: Direct Forward Params Layer sources: [direct_fwd_params] - name: wms_cache title: WMS Cache Layer with direct access from level 8 sources: [wms_cache] md: abstract: Some abstract keyword_list: - vocabulary: Name of the vocabulary keywords: [keyword1, keyword2] - vocabulary: Name of another vocabulary keywords: [keyword1, keyword2] - keywords: ["keywords without vocabulary"] attribution: title: My attribution title url: http://some.url/ logo: url: http://some.url/logo.jpg width: 100 height: 100 format: image/jpeg identifier: - url: http://some.url/ name: HKU1234 value: Some value metadata: - url: http://some.url/ type: INSPIRE format: application/xml - url: http://some.url/ type: ISO19115:2003 format: application/xml data: - url: http://some.url/datasets/test.shp format: application/octet-stream - url: http://some.url/datasets/test.gml format: text/xml; subtype=gml/3.2.1 feature_list: - url: http://some.url/datasets/test.pdf format: application/pdf - name: wms_cache_transparent title: WMS Cache Layer with transparent data sources: [wms_cache_transparent] - name: wms_cache_link_single title: WMS Cache Layer (link single) sources: [wms_cache_link_single] - name: wms_cache_100 title: WMS Cache Layer sources: [wms_cache_100] - name: wms_cache_130 title: WMS Cache Layer sources: [wms_cache_130] - name: wms_cache_multi title: WMS Cache Multi Layer sources: [wms_cache_multi] - name: tms_cache title: TMS Cache Layer sources: [tms_cache] - name: tms_fi_cache title: TMS Cache Layer + FI # layer should be avail for cache services sources: [tms_cache, wms_fi_only] - name: wms_merge title: WMS Cache + Direct Layer sources: [direct, wms_cache] - name: wms_cache_110 title: WMS Cache Layer sources: [wms_cache_110] - name: watermark_cache title: TMS Cache + watermark sources: [watermark_cache] caches: wms_cache: format: image/jpeg use_direct_from_level: 8 sources: [wms_cache] cache: type: file tile_lock_dir: wmscachetilelockdir wms_cache_transparent: format: png8a sources: [wms_cache_transparent] wms_cache_link_single: format: png24 request_format: image/jpeg link_single_color_images: True sources: [wms_cache] wms_cache_100: format: image/jpeg request_format: image/tiff sources: [wms_cache_100] wms_cache_130: format: image/jpeg sources: [wms_cache_130] wms_cache_multi: format: custom grids: [GLOBAL_GEODETIC, GLOBAL_MERCATOR] sources: [wms_cache_130] tms_cache: sources: [tms_cache] wms_cache_110: format: image/jpeg sources: [wms_cache_110] watermark_cache: sources: [tms_cache] disable_storage: true watermark: text: '@ Omniscale' sources: direct: type: wms req: url: http://localhost:42423/service layers: bar coverage: bbox: [-180, -80, 170, 80] srs: 'EPSG:4326' direct_fwd_params: type: wms forward_req_params: ['time'] req: url: http://localhost:42423/service layers: bar coverage: # coverage in projection not in wms.srs, # should not be advertised in capabilities #288 bbox: [-180, -80, 170, 80] srs: 'EPSG:4258' wms_cache: type: wms supported_srs: ['EPSG:900913', 'EPSG:4326'] wms_opts: featureinfo: True req: url: http://localhost:42423/service layers: foo,bar wms_cache_transparent: type: wms supported_srs: ['EPSG:900913', 'EPSG:4326'] wms_opts: featureinfo: True req: url: http://localhost:42423/service layers: foo,bar transparent: true wms_cache_100: type: wms wms_opts: version: '1.0.0' featureinfo: True req: url: http://localhost:42423/service layers: foo,bar wms_cache_130: type: wms min_res: 250000000 max_res: 1 wms_opts: version: '1.3.0' featureinfo: True req: url: http://localhost:42423/service layers: foo,bar tms_cache: type: tile url: http://localhost:42423/tiles/%(tc_path)s.png wms_cache_110: type: wms wms_opts: version: '1.1.0' featureinfo: True req: url: http://localhost:42423/service layers: foo,bar wms_fi_only: type: wms wms_opts: featureinfo: True map: False req: url: http://localhost:42423/service layers: fi mapproxy-1.11.0/mapproxy/test/system/fixture/layergroups.yaml000066400000000000000000000020221320454472400245620ustar00rootroot00000000000000services: tms: kml: wms: md: title: 'My WMS' layers: - name: layer1 title: layer 1 sources: [dummy] layers: - name: layer1a title: layer 1a sources: [dummy] - name: layer1b title: layer 1b sources: [dummy_fi] - name: layer2 title: layer 2 layers: - name: layer2a title: layer 2a sources: [dummy] - name: layer2b title: layer 2b layers: - name: layer2b1 title: layer 2b1 sources: [dummy_fi] caches: dummy: grids: [GLOBAL_MERCATOR] sources: [dummy] dummy_fi: grids: [GLOBAL_MERCATOR] sources: [dummy_fi] sources: dummy: type: wms coverage: bbox: [179, 89, 180, 89.9] bbox_srs: 'EPSG:4326' req: url: http://localhost:42423/service dummy_fi: type: wms wms_opts: featureinfo: True coverage: bbox: [179, 89, 180, 89.9] bbox_srs: 'EPSG:4326' req: url: http://localhost:42423/service mapproxy-1.11.0/mapproxy/test/system/fixture/layergroups_root.yaml000066400000000000000000000032721320454472400256350ustar00rootroot00000000000000services: wms: layers: - name: root title: Root Layer layers: - name: layer1 title: layer 1 sources: [dummy] layers: - name: layer1a title: layer 1a sources: [dummy] - name: layer1b title: layer 1b sources: [dummy] - name: layer2 title: layer 2 sources: [dummy] sources: dummy: type: wms req: url: http://localhost:42423/service # # Now # layers: # - layer1: # title: layer1 # sources: [layer1] # - layer2: # title: layer1 # sources: [layer2] # # # or (unsorted) # layers: # layer1: # title: layer1 # sources: [layer1] # layer2: # title: layer1 # sources: [layer2] # # # # layers: # - root: # title: Root Layer # layers: # - layer1: # title: Layer 1 # sources: [layer1] # - layer2: # title: Layer 2 # sources: [layer2] # # # layers: # name: root # title: Root Layer # - layer1: # title: layer1 # sources: [layer1] # - layer2: # title: layer1 # sources: [layer2] # # # layers: # name: root # title: Root Layer # layers: # - layer1: # title: layer1 # sources: [layer1] # - layer2: # title: layer1 # sources: [layer2] # # # # # wms_layers: # - name: layer1 # title: layer 1 # sorces: [layer1] # layers: # - name: layer1a # title: layer 1a # sources: [layer 1a] # - name: layer1b # title: layer 1b # sources: [layer 1b] # - name: layer2 # title: layer 2 # sources: [layer2] # # # tile_layers: # - name: # title: # cache: # mapproxy-1.11.0/mapproxy/test/system/fixture/legendgraphic.yaml000066400000000000000000000043641320454472400250150ustar00rootroot00000000000000globals: cache: base_dir: cache_data/ meta_size: [1, 1] meta_buffer: 0 image: paletted: True services: tms: kml: wms: md: title: MapProxy test fixture abstract: This is MapProxy. online_resource: http://mapproxy.org/ contact: person: Oliver Tonnhofer position: Technical Director organization: Omniscale address: Nadorster Str. 60 city: Oldenburg postcode: 26123 country: Germany phone: +49(0)441-9392774-0 fax: +49(0)441-9392774-9 email: info@omniscale.de access_constraints: Here be dragons. layers: - name: wms_legend title: Layer with legendgraphic support sources: [legend_cache] - name: wms_mult_sources title: Layer with multiple sources sources: [legend_cache, legend_cache_2] - name: wms_no_legend title: Layer without legendgraphic support sources: [wms_cache] - name: wms_source_static_url title: Layer with a static LegendURL sources: [legendurl_static] - name: wms_layer_static_url title: Layer with a static LegendURL legendurl: http://localhost:42423/staticlegend_layer.png sources: [legendurl_static_2] sources: legend_cache: type: wms supported_srs: ['EPSG:900913', 'EPSG:4326'] wms_opts: version: '1.1.1' legendgraphic: True req: url: http://localhost:42423/service layers: foo,bar legend_cache_2: type: wms supported_srs: ['EPSG:900913', 'EPSG:4326'] wms_opts: version: '1.1.1' legendgraphic: True req: url: http://localhost:42423/service layers: spam legendurl_static: type: wms supported_srs: ['EPSG:900913', 'EPSG:4326'] wms_opts: version: '1.1.1' legendurl: http://localhost:42423/staticlegend_source.png req: url: http://localhost:42423/service layers: foo,bar legendurl_static_2: type: wms supported_srs: ['EPSG:900913', 'EPSG:4326'] wms_opts: version: '1.1.1' req: url: http://localhost:42423/service layers: foo,bar wms_cache: type: wms supported_srs: ['EPSG:900913', 'EPSG:4326'] wms_opts: version: '1.1.1' req: url: http://localhost:42423/service layers: foo mapproxy-1.11.0/mapproxy/test/system/fixture/mapnik_source.yaml000066400000000000000000000020351320454472400250510ustar00rootroot00000000000000services: wms: layers: - name: mapnik title: Mapnik Source sources: [mapnik] - name: mapnik_hq title: Mapnik Source with scale-factor 2 sources: [mapnik_hq] - name: mapnik_transparent title: Mapnik Source sources: [mapnik_transparent] - name: mapnik_unknown title: Mapnik Source sources: [mapnik_unknown] - name: mapnik_level title: Mapnik Source sources: [mapnik_level] sources: mapnik: type: mapnik mapfile: ./mapnik.xml coverage: bbox: [-170, -80, 180, 90] bbox_srs: 'EPSG:4326' mapnik_hq: type: mapnik mapfile: ./mapnik.xml scale_factor: 2 coverage: bbox: [-170, -80, 180, 90] bbox_srs: 'EPSG:4326' mapnik_transparent: type: mapnik mapfile: ./mapnik-transparent.xml coverage: bbox: [-170, -80, 180, 90] bbox_srs: 'EPSG:4326' mapnik_unknown: type: mapnik mapfile: ./unknown.xml mapnik_level: type: mapnik mapfile: ./mapnik-%(webmercator_level)0.2d.xml globals: image: paletted: Falsemapproxy-1.11.0/mapproxy/test/system/fixture/mapproxy_export.yaml000066400000000000000000000003011320454472400254640ustar00rootroot00000000000000globals: cache: meta_size: [1, 1] caches: tms_cache: sources: [tms_source] sources: tms_source: type: tile url: http://localhost:42423/tiles/%(z)s/%(x)s/%(y)s.png mapproxy-1.11.0/mapproxy/test/system/fixture/mapserver.yaml000066400000000000000000000005361320454472400242220ustar00rootroot00000000000000services: wms: layers: - name: ms title: MapServer CGI Test sources: [ms_cache] caches: ms_cache: grids: [GLOBAL_MERCATOR] meta_size: [1, 1] meta_buffer: 0 sources: ['ms_cgi:base'] sources: ms_cgi: type: mapserver req: map: ./foo.map mapserver: binary: ./cgi.py working_dir: ./tmp mapproxy-1.11.0/mapproxy/test/system/fixture/mixed_mode.yaml000066400000000000000000000017561320454472400243350ustar00rootroot00000000000000globals: cache: base_dir: cache_data/ meta_size: [2, 1] meta_buffer: 0 image: paletted: False # resampling: 'bicubic' services: tms: wmts: wms: md: title: MapProxy test fixture abstract: This is MapProxy. online_resource: http://mapproxy.org/ contact: person: Oliver Tonnhofer position: Technical Director organization: Omniscale address: Nadorster Str. 60 city: Oldenburg postcode: 26123 country: Germany phone: +49(0)441-9392774-0 fax: +49(0)441-9392774-9 email: info@omniscale.de access_constraints: Here be dragons. layers: - name: mixed_mode title: cache with PNG and JPEG sources: [mixed_cache] caches: mixed_cache: format: mixed sources: [mixed_source] request_format: image/png sources: mixed_source: type: wms req: url: http://localhost:42423/service layers: mixedsource transparent: true mapproxy-1.11.0/mapproxy/test/system/fixture/multi_cache_layers.yaml000066400000000000000000000043311320454472400260470ustar00rootroot00000000000000globals: cache: base_dir: cache_data/ meta_size: [1, 1] meta_buffer: 0 image: # resampling: 'bicubic' paletted: False services: tms: kml: wmts: restful_template: '/myrest/{{Layer}}/{{TileMatrixSet}}/{{TileMatrix}}/{{TileCol}}/{{TileRow}}.{{Format}}' wms: md: title: MapProxy test fixture abstract: This is MapProxy. online_resource: http://mapproxy.org/ contact: person: Oliver Tonnhofer position: Technical Director organization: Omniscale address: Nadorster Str. 60 city: Oldenburg postcode: 26123 country: Germany phone: +49(0)441-9392774-0 fax: +49(0)441-9392774-9 email: info@omniscale.de layers: - name: multi_cache title: WMTS only layer tile_sources: [utm_cache, webmerc_cache, gk_cache] - name: wms_only title: WMS only layer tile_sources: [] sources: [utm_cache] - name: cache title: single cache layer sources: [utm_cache] caches: utm_cache: grids: [utm32] sources: [wms_source] webmerc_cache: grids: [GLOBAL_WEBMERCATOR, wmts_incompatible_grid, crs84quad] sources: [wms_source] gk_cache: grids: [gk3] disable_storage: true sources: [utm_cache] sources: wms_source: type: wms req: url: http://localhost:42423/service layers: foo,bar grids: wmts_incompatible_grid: # shoud no be included in WMTS srs: 'EPSG:25832' bbox: [3000000, 5000000, 4000000, 6000000] res_factor: sqrt2 origin: 'll' crs84quad: name: InspireCrs84Quad srs: 'CRS:84' bbox: [-180, -90, 180, 90] origin: 'ul' min_res: 0.703125 gk3: bbox: [3400000, 5400000, 3600000, 5600000] srs: 'EPSG:31467' utm32: srs: 'EPSG:25832' res: - 4891.96981025128 - 2445.98490512564 - 1222.99245256282 - 611.49622628141 - 305.748113140705 - 152.874056570353 - 76.4370282851763 - 38.2185141425881 - 19.1092570712941 - 9.55462853564703 - 4.77731426782352 - 2.38865713391176 - 1.19432856695588 - 0.597164283477939 bbox: [-46133.17, 5048875.26857567, 1206211.10142433, 6301219.54] bbox_srs: 'EPSG:25832' origin: 'ul'mapproxy-1.11.0/mapproxy/test/system/fixture/multiapp1.yaml000066400000000000000000000004531320454472400241300ustar00rootroot00000000000000services: tms: demo: layers: - name: app1_layer title: WMS Cache Layer sources: [app1_cache] caches: app1_cache: grids: [GLOBAL_MERCATOR] sources: [app1_source] sources: app1_source: type: wms req: url: http://localhost:42423/service layers: foo,bar mapproxy-1.11.0/mapproxy/test/system/fixture/multiapp2.yaml000066400000000000000000000004431320454472400241300ustar00rootroot00000000000000services: tms: layers: - name: app2_layer title: WMS Cache Layer sources: [app2_cache] caches: app2_cache: grids: [GLOBAL_MERCATOR] sources: [app2_source] sources: app2_source: type: wms req: url: http://localhost:42423/service layers: foo,bar mapproxy-1.11.0/mapproxy/test/system/fixture/renderd_client.yaml000066400000000000000000000017471320454472400252040ustar00rootroot00000000000000globals: cache: base_dir: cache_data/ meta_size: [1, 1] meta_buffer: 0 renderd: address: http://localhost:42423 services: tms: kml: wmts: wms: md: title: MapProxy test fixture ☃ layers: - name: direct title: Direct Layer sources: [direct] - name: wms_cache title: WMS Cache Layer with direct access from level 8 sources: [wms_cache] - name: tms_cache title: TMS Cache Layer sources: [tms_cache] caches: wms_cache: format: image/jpeg use_direct_from_level: 8 sources: [wms_cache] meta_size: [3, 3] tms_cache: sources: [tms_cache] sources: direct: type: wms req: url: http://localhost:42423/service layers: bar wms_cache: type: wms supported_srs: ['EPSG:900913', 'EPSG:4326'] wms_opts: featureinfo: True req: url: http://localhost:42423/service layers: foo,bar tms_cache: type: tile url: http://localhost:42423/tiles/%(tc_path)s.png mapproxy-1.11.0/mapproxy/test/system/fixture/scalehints.yaml000066400000000000000000000030161320454472400243470ustar00rootroot00000000000000globals: cache: base_dir: cache_data/ meta_size: [1, 1] meta_buffer: 0 image: paletted: True services: tms: kml: wms: md: title: MapProxy test fixture abstract: This is MapProxy. online_resource: http://mapproxy.org/ contact: person: Oliver Tonnhofer position: Technical Director organization: Omniscale address: Nadorster Str. 60 city: Oldenburg postcode: 26123 country: Germany phone: +49(0)441-9392774-0 fax: +49(0)441-9392774-9 email: info@omniscale.de access_constraints: Here be dragons. layers: - name: res title: Cache Layer with min/max res sources: [res_cache] - name: scale title: Cache Layer with min/max scale sources: [scale_cache] - name: scale2 title: Cache Layer with min/max scale min_scale: 1000 max_scale: 10000 sources: [scale_cache] caches: res_cache: format: image/jpeg grids: [GLOBAL_MERCATOR, GLOBAL_GEODETIC] sources: [wms_res] scale_cache: format: image/jpeg grids: [GLOBAL_MERCATOR] sources: [wms_scale] sources: wms_res: type: wms supported_srs: ['EPSG:900913', 'EPSG:4326'] min_res: 10000 max_res: 10 req: url: http://localhost:42423/service layers: reslayer wms_scale: type: wms supported_srs: ['EPSG:900913', 'EPSG:4326'] max_scale: 1000000 min_scale: 10000 req: url: http://localhost:42423/service layers: scalelayer mapproxy-1.11.0/mapproxy/test/system/fixture/seed.yaml000066400000000000000000000033331320454472400231340ustar00rootroot00000000000000coverages: world: bbox: [-180, -90, 180, 90] bbox_srs: 'EPSG:4326' west: bbox: [-180, -90, 0, 90] bbox_srs: 'EPSG:4326' empty_geom: ogr_datasource: 'empty_ogrdata.geojson' ogr_srs: "EPSG:4326" seeds: one: caches: [one] grids: [GLOBAL_GEODETIC] levels: [0] refresh_before: days: 1 mbtile_cache: caches: [mbtile_cache] grids: [GLOBAL_GEODETIC] levels: [0] mbtile_cache_refresh: caches: [mbtile_cache] grids: [GLOBAL_GEODETIC] levels: [0] refresh_before: days: 1 with_empty_coverage: caches: [mbtile_cache] grids: [GLOBAL_GEODETIC] coverages: [empty_geom] levels: [0] refresh_from_file: caches: [one] grids: [GLOBAL_GEODETIC] levels: [0] refresh_before: mtime: 'seed.yaml' cleanups: cleanup: caches: [one] grids: [GLOBAL_GEODETIC] levels: [0, 1, 3] # to prevent timing issues remove_before: minutes: -1 remove_all: caches: [one] grids: [GLOBAL_GEODETIC] levels: [1] remove_all: true sqlite_cache: caches: [sqlite_cache] grids: [GLOBAL_GEODETIC] levels: [3] # to prevent timing issues remove_before: minutes: -1 sqlite_cache_remove_all: caches: [sqlite_cache] grids: [GLOBAL_GEODETIC] levels: [2] remove_all: true with_coverage: caches: [one] coverages: [west] grids: [GLOBAL_GEODETIC] levels: [0, 1, 3] # to prevent timing issues remove_before: minutes: -1 cleanup_mbtile_cache: caches: [mbtile_cache] grids: [GLOBAL_GEODETIC] levels: [0, 1, 3] remove_from_file: caches: [one] grids: [GLOBAL_GEODETIC] levels: [0] remove_before: mtime: 'seed.yaml'mapproxy-1.11.0/mapproxy/test/system/fixture/seed_mapproxy.yaml000066400000000000000000000011401320454472400250650ustar00rootroot00000000000000globals: cache: base_dir: './cache' caches: one: sources: [source_a] grids: [GLOBAL_GEODETIC] mbtile_cache: sources: [source_b] grids: [GLOBAL_GEODETIC] cache: type: mbtiles sqlite_cache: sources: [source_c] grids: [GLOBAL_GEODETIC] cache: type: sqlite sources: source_a: type: wms req: url: http://localhost:42423/service? layers: foo source_b: type: wms req: url: http://localhost:42423/service? layers: bar source_c: type: wms req: url: http://localhost:42423/service? layers: bazmapproxy-1.11.0/mapproxy/test/system/fixture/seed_old.yaml000066400000000000000000000002601320454472400237660ustar00rootroot00000000000000views: one: bbox: [-180, -90, 180, 90] bbox_srs: 'EPSG:4326' srs: ['EPSG:4326'] level: [0, 0] seeds: one: views: [one] remove_before: days: 1mapproxy-1.11.0/mapproxy/test/system/fixture/seed_timeouts.yaml000066400000000000000000000003301320454472400250570ustar00rootroot00000000000000seeds: test: caches: [wms_cache] grids: [GLOBAL_GEODETIC] coverages: [world] levels: to: 2 coverages: world: bbox: [-180, -90, 180, 90] bbox_srs: 'EPSG:4326'mapproxy-1.11.0/mapproxy/test/system/fixture/seed_timeouts_mapproxy.yaml000066400000000000000000000006531320454472400270260ustar00rootroot00000000000000globals: cache: base_dir: cache_data/ image: # resampling: 'bicubic' paletted: False layers: - name: wms_cache title: WMS Cache Layer sources: [wms_cache] caches: wms_cache: sources: [wms_cache] grids: [GLOBAL_GEODETIC] sources: wms_cache: type: wms req: url: http://localhost:42423/service layers: foo concurrent_requests: 1 http: client_timeout: 0.2mapproxy-1.11.0/mapproxy/test/system/fixture/seedonly.yaml000066400000000000000000000020521320454472400240330ustar00rootroot00000000000000globals: cache: base_dir: cache_data/ meta_size: [1, 1] meta_buffer: 0 image: # resampling: 'bicubic' paletted: False services: tms: kml: wms: md: title: MapProxy test fixture abstract: This is MapProxy. online_resource: http://mapproxy.org/ contact: person: Oliver Tonnhofer position: Technical Director organization: Omniscale address: Nadorster Str. 60 city: Oldenburg postcode: 26123 country: Germany phone: +49(0)441-9392774-0 fax: +49(0)441-9392774-9 email: info@omniscale.de access_constraints: Here be dragons. layers: - name: wms_cache title: WMS Cache Layer sources: [wms_cache] caches: wms_cache: format: image/jpeg use_direct_from_level: 8 sources: [wms_cache] sources: wms_cache: type: wms seed_only: true supported_srs: ['EPSG:900913', 'EPSG:4326'] wms_opts: featureinfo: True req: url: http://localhost:42423/service layers: foo,barmapproxy-1.11.0/mapproxy/test/system/fixture/sld.yaml000066400000000000000000000012251320454472400227740ustar00rootroot00000000000000services: wms: layers: - name: sld_url title: Layer with sld sources: [sld_url_wms] - name: sld_file title: Layer with file sources: [sld_file_wms] - name: sld_body title: Layer with sld body sources: [sld_body_wms] sources: sld_url_wms: type: wms req: url: http://localhost:42423/service sld: http://example.org/sld.xml sld_file_wms: type: wms http: method: GET req: url: http://localhost:42423/service sld: file://mysld.xml sld_body_wms: type: wms req: url: http://localhost:42423/service sld_body: mapproxy-1.11.0/mapproxy/test/system/fixture/source_errors.yaml000066400000000000000000000032741320454472400251140ustar00rootroot00000000000000globals: cache: base_dir: ./cache_data/ meta_size: [1, 1] meta_buffer: 0 image: paletted: False # resampling: 'bicubic' services: wms: on_source_errors: notify tms: layers: - name: online title: all sources online sources: [wms1] - name: all_offline title: all sources offline sources: [wms2, wms3] - name: mixed title: on- and offline layers sources: [wms1, wms2, wms3] - name: tilesource title: Tilesource with 404/204 handling sources: [tilesource_cache] - name: tilesource_catchall title: Tilesource with 'other' on_error handling sources: [tilesource_catchall_cache] caches: wms_cache: grids: [GLOBAL_MERCATOR] sources: [wms1, wms2, wms3] tilesource_cache: grids: [GLOBAL_GEODETIC] sources: [tilesource] tilesource_catchall_cache: grids: [GLOBAL_GEODETIC] sources: [tilesource_catchall] sources: wms1: type: wms req: url: http://localhost:42423/service_a layers: a_one transparent: True wms2: type: wms req: url: http://localhost:99998/service_b layers: b_one transparent: True wms3: type: wms req: url: http://localhost:99999/service_c layers: c_one transparent: True tilesource: type: tile url: http://localhost:42423/foo/%(tms_path)s.png grid: GLOBAL_GEODETIC on_error: 404: response: '#ff0080' cache: False 204: response: [100, 200, 50, 250] cache: True tilesource_catchall: type: tile url: http://localhost:42423/foo/%(tms_path)s.png grid: GLOBAL_GEODETIC on_error: other: response: [100, 50, 50] cache: False mapproxy-1.11.0/mapproxy/test/system/fixture/source_errors_raise.yaml000066400000000000000000000032731320454472400262760ustar00rootroot00000000000000globals: cache: base_dir: ./cache_data/ meta_size: [1, 1] meta_buffer: 0 image: paletted: False # resampling: 'bicubic' services: wms: on_source_errors: raise tms: layers: - name: online title: all sources online sources: [wms1] - name: all_offline title: all sources offline sources: [wms2, wms3] - name: mixed title: on- and offline layers sources: [wms1, wms2, wms3] - name: tilesource title: Tilesource with 404/204 handling sources: [tilesource_cache] - name: tilesource_catchall title: Tilesource with 'other' on_error handling sources: [tilesource_catchall_cache] caches: wms_cache: grids: [GLOBAL_MERCATOR] sources: [wms1, wms2, wms3] tilesource_cache: grids: [GLOBAL_GEODETIC] sources: [tilesource] tilesource_catchall_cache: grids: [GLOBAL_GEODETIC] sources: [tilesource_catchall] sources: wms1: type: wms req: url: http://localhost:42423/service_a layers: a_one transparent: True wms2: type: wms req: url: http://localhost:99998/service_b layers: b_one transparent: True wms3: type: wms req: url: http://localhost:99999/service_c layers: c_one transparent: True tilesource: type: tile url: http://localhost:42423/foo/%(tms_path)s.png grid: GLOBAL_GEODETIC on_error: 404: response: '#ff0080' cache: False 204: response: [100, 200, 50, 250] cache: True tilesource_catchall: type: tile url: http://localhost:42423/foo/%(tms_path)s.png grid: GLOBAL_GEODETIC on_error: other: response: [100, 50, 50] cache: False mapproxy-1.11.0/mapproxy/test/system/fixture/tileservice_origin.yaml000066400000000000000000000005701320454472400261010ustar00rootroot00000000000000globals: cache: base_dir: cache_data/ meta_size: [1, 1] meta_buffer: 0 services: tms: origin: 'nw' layers: - name: wms_cache title: Direct Layer sources: [wms_cache] caches: wms_cache: format: image/jpeg sources: [wms_source] sources: wms_source: type: wms req: url: http://localhost:42423/service layers: bar mapproxy-1.11.0/mapproxy/test/system/fixture/tilesource_minmax_res.yaml000066400000000000000000000007001320454472400266070ustar00rootroot00000000000000services: tms: layers: - name: tms_cache title: min_res/max_res source sources: [tms_cache] caches: tms_cache: grids: [GLOBAL_MERCATOR] sources: [tms_source_a, tms_source_b] sources: tms_source_a: type: tile url: http://localhost:42423/tiles_a/%(tc_path)s.png max_res: 1222.99245256282 tms_source_b: type: tile url: http://localhost:42423/tiles_b/%(tc_path)s.png min_res: 1222.99245256282 mapproxy-1.11.0/mapproxy/test/system/fixture/util-conf-base-grids.yaml000066400000000000000000000001411320454472400261240ustar00rootroot00000000000000grids: webmercator: base: GLOBAL_WEBMERCATOR geodetic: base: GLOBAL_GEODETIC mapproxy-1.11.0/mapproxy/test/system/fixture/util-conf-overwrite.yaml000066400000000000000000000003401320454472400261330ustar00rootroot00000000000000caches: __all__: cache: type: sqlite sources: osm____: req: param: 42 ____roads_wms: supported_srs: ['EPSG:3857'] coverage: bbox: [0, 0, 90, 90] mapproxy-1.11.0/mapproxy/test/system/fixture/util-conf-wms-111-cap.xml000066400000000000000000000072101320454472400256150ustar00rootroot00000000000000 ]> OGC:WMS Omniscale OpenStreetMap WMS Omniscale OpenStreetMap WMS (powered by MapProxy) Oliver Tonnhofer Omniscale Technical Director postal
Nadorster Str. 60
Oldenburg 26123 Germany
+49(0)441-9392774-0 +49(0)441-9392774-9 osm@omniscale.de
none Here be dragons.
application/vnd.ogc.wms_xml image/jpeg image/png image/gif image/GeoTIFF image/tiff text/plain text/html application/vnd.ogc.gml application/vnd.ogc.se_xml application/vnd.ogc.se_inimage application/vnd.ogc.se_blank Omniscale OpenStreetMap WMS EPSG:4326 EPSG:4258 CRS:84 EPSG:900913 EPSG:31466 EPSG:31467 EPSG:31468 EPSG:25831 EPSG:25832 EPSG:25833 EPSG:3857 osm OpenStreetMap (complete map) osm_roads OpenStreetMap (streets only)
mapproxy-1.11.0/mapproxy/test/system/fixture/util_grids.yaml000066400000000000000000000012351320454472400243600ustar00rootroot00000000000000services: demo: layers: - name: grid_layer title: Grid Layer sources: [test_cache] caches: test_cache: grids: [global_geodetic_sqrt2, grid_full_example, another_grid_full_example] sources: [] grids: global_geodetic_sqrt2: base: GLOBAL_GEODETIC res_factor: 'sqrt2' grid_full_example: tile_size: [512, 512] srs: 'EPSG:900913' bbox: [5, 45, 15, 55] bbox_srs: 'EPSG:4326' min_res: 2000 #m/px max_res: 50 #m/px align_resolutions_with: GLOBAL_MERCATOR another_grid_full_example: srs: 'EPSG:900913' bbox: [5, 45, 15, 55] bbox_srs: 'EPSG:4326' res_factor: 1.5 num_levels: 25mapproxy-1.11.0/mapproxy/test/system/fixture/util_wms_capabilities111.xml000066400000000000000000000134111320454472400266470ustar00rootroot00000000000000 ]> OGC:WMS MapProxy WMS Proxy This is the fantastic MapProxy. Your Name Here Technical Director postal
Fakestreet 123
Somewhere 12345 Germany
+49(0)000-000000-0 +49(0)000-000000-0 info@omniscale.de
None Here be dragons.
application/vnd.ogc.wms_xml image/gif image/png image/tiff image/jpeg image/GeoTIFF text/plain text/html application/vnd.ogc.gml application/vnd.ogc.se_xml application/vnd.ogc.se_inimage application/vnd.ogc.se_blank MapProxy WMS Proxy EPSG:31467 EPSG:31466 EPSG:4326 EPSG:25831 EPSG:25833 EPSG:25832 EPSG:31468 EPSG:900913 CRS:84 EPSG:4258 osm Omniscale OSM WMS - osm.omniscale.net root Root Layer layer1 Title of Layer 1 layer1a Title of Layer 1a layer1b Title of Layer 1b layer2 Title of Layer 2
mapproxy-1.11.0/mapproxy/test/system/fixture/util_wms_capabilities130.xml000066400000000000000000000102651320454472400266540ustar00rootroot00000000000000 WMS MapProxy WMS Proxy This is the fantastic MapProxy. Your Name Here Technical Director postal
Fakestreet 123
Somewhere 12345 Germany
+49(0)000-000000-0 +49(0)000-000000-0 info@omniscale.de
None Here be dragons.
text/xml image/gif image/png image/tiff image/jpeg image/GeoTIFF text/plain text/html text/xml XML INIMAGE BLANK MapProxy WMS Proxy EPSG:900913 EPSG:4326 EPSG:4258 CRS:84 EPSG:3857 -180 180 -85.0511287798 85.0511287798 osm Omniscale OSM WMS - osm.omniscale.net -180 180 -85.0511287798 85.0511287798
mapproxy-1.11.0/mapproxy/test/system/fixture/util_wms_capabilities_service_exception.xml000066400000000000000000000004601320454472400322220ustar00rootroot00000000000000 unknown WMS request type 'GetCapabilitie' mapproxy-1.11.0/mapproxy/test/system/fixture/watermark.yaml000066400000000000000000000015071320454472400242120ustar00rootroot00000000000000globals: cache: base_dir: cache_data/ meta_size: [1, 1] meta_buffer: 0 image: paletted: False # resampling: 'bicubic' services: tms: layers: watermark: title: Layer with watermark sources: [wms_cache] watermark_transp: title: Layer with watermark sources: [wms_transp_cache] caches: wms_cache: grids: [GLOBAL_GEODETIC] sources: [wms_source] watermark: text: foo opacity: 100 font_size: 30 wms_transp_cache: grids: [GLOBAL_GEODETIC] sources: [wms_source] watermark: text: foo opacity: 100 font_size: 30 sources: wms_source: type: wms req: url: http://localhost:42423/service layers: blank wms_source_transp: type: wms req: url: http://localhost:42423/service layers: blank mapproxy-1.11.0/mapproxy/test/system/fixture/wms_srs_extent.yaml000066400000000000000000000017011320454472400252750ustar00rootroot00000000000000services: wms: image_formats: ['image/png', 'image/jpeg'] srs: ['EPSG:4326', 'EPSG:4258', 'CRS:84', 'EPSG:900913', 'EPSG:3857', 'EPSG:31467', 'EPSG:31468', 'EPSG:25831', 'EPSG:25832', 'EPSG:25833'] bbox_srs: - bbox: [0.0, 3500000.0, 1000000.0, 8500000.0] srs: 'EPSG:25832' - bbox: [2750000, 5000000, 4250000, 6500000] srs: 'EPSG:31467' - bbox: [2750000, 5000000, 4250000, 6500000] srs: 'EPSG:31466' - 'EPSG:3857' md: title: MapProxy test fixture ☃ layers: - name: direct title: Direct Layer sources: [direct] - name: direct_coverage title: Direct Layer with Coverage sources: [direct_coverage] sources: direct: type: wms req: url: http://localhost:42423/service layers: bar direct_coverage: type: wms req: url: http://localhost:42423/service layers: bar coverage: bbox: [5, 50, 10, 55] srs: 'EPSG:4326' mapproxy-1.11.0/mapproxy/test/system/fixture/wms_versions.yaml000066400000000000000000000017031320454472400247510ustar00rootroot00000000000000services: tms: kml: wmts: wms: versions: ['1.1.0', '1.1.1'] image_formats: ['image/png', 'image/jpeg', 'png8'] srs: ['EPSG:4326', 'EPSG:4258', 'CRS:84', 'EPSG:900913', 'EPSG:3857', 'EPSG:31466', 'EPSG:31467', 'EPSG:31468', 'EPSG:25831', 'EPSG:25832', 'EPSG:25833'] md: title: MapProxy test fixture ☃ abstract: This is MapProxy. online_resource: http://mapproxy.org/ contact: person: Oliver Tonnhofer position: Technical Director organization: Omniscale address: Nadorster Str. 60 city: Oldenburg postcode: 26123 country: Germany phone: +49(0)441-9392774-0 fax: +49(0)441-9392774-9 email: info@omniscale.de access_constraints: Here be dragons. layers: - name: direct title: Direct Layer sources: [direct] sources: direct: type: wms req: url: http://localhost:42423/service layers: bar mapproxy-1.11.0/mapproxy/test/system/fixture/wmts.yaml000066400000000000000000000046051320454472400232110ustar00rootroot00000000000000globals: cache: base_dir: cache_data/ meta_size: [1, 1] meta_buffer: 0 image: # resampling: 'bicubic' paletted: False services: tms: kml: wmts: restful_template: '/myrest/{{Layer}}/{{TileMatrixSet}}/{{TileMatrix}}/{{TileCol}}/{{TileRow}}.{{Format}}' wms: md: title: MapProxy test fixture abstract: This is MapProxy. online_resource: http://mapproxy.org/ contact: person: Oliver Tonnhofer position: Technical Director organization: Omniscale address: Nadorster Str. 60 city: Oldenburg postcode: 26123 country: Germany phone: +49(0)441-9392774-0 fax: +49(0)441-9392774-9 email: info@omniscale.de access_constraints: Here be dragons. layers: - name: wms_cache title: WMS Cache Layer sources: [wms_cache] - name: wms_cache_multi title: WMS Cache Multi Layer sources: [wms_cache_multi] - name: tms_cache title: TMS Cache Layer sources: [tms_cache] - name: tms_cache_ul title: TMS Cache Layer sources: [tms_cache_ul] - name: gk3_cache title: GK3 Cache Layer sources: [gk3_cache] caches: wms_cache: format: image/jpeg sources: [wms_cache] wms_cache_multi: format: image/jpeg grids: [CustomGridSet, GoogleMapsCompatible] sources: [wms_cache_130] tms_cache: sources: [tms_cache] tms_cache_ul: grids: [ulgrid] sources: [tms_cache] gk3_cache: grids: [gk3] sources: [wms_cache] sources: wms_cache: type: wms supported_srs: ['EPSG:900913', 'EPSG:4326'] wms_opts: featureinfo: True req: url: http://localhost:42423/service layers: foo,bar wms_cache_100: type: wms wms_opts: version: '1.0.0' featureinfo: True req: url: http://localhost:42423/service layers: foo,bar wms_cache_130: type: wms min_res: 250000000 max_res: 1 wms_opts: version: '1.3.0' featureinfo: True req: url: http://localhost:42423/service layers: foo,bar tms_cache: type: tile url: http://localhost:42423/tiles/%(tc_path)s.png grids: gk3: srs: 'EPSG:31467' bbox: [3000000, 5000000, 4000000, 6000000] origin: 'ul' GoogleMapsCompatible: base: GLOBAL_MERCATOR CustomGridSet: base: GLOBAL_GEODETIC min_res: 0.703125 ulgrid: base: GLOBAL_MERCATOR origin: ulmapproxy-1.11.0/mapproxy/test/system/fixture/wmts_dimensions.yaml000066400000000000000000000023571320454472400254430ustar00rootroot00000000000000services: wmts: restful: true kvp: true restful_template: '/{Layer}/{TileMatrixSet}/{Time}/{Elevation}/{TileMatrix}/{TileCol}/{TileRow}.{Format}' layers: - name: dimension_layer title: layer with dimensions sources: [cache1] dimensions: tiME: values: - "2012-11-12T00:00:00" - "2012-11-13T00:00:00" - "2012-11-14T00:00:00" - "2012-11-15T00:00:00" Elevation: values: - 0 - 1000 - 3000 default: "0" - name: no_dimension_layer title: layer without dimensions sources: [cache2] caches: cache1: grids: [GLOBAL_MERCATOR] disable_storage: true meta_size: [1, 1] meta_buffer: 0 sources: [wms_source1] cache2: grids: [GLOBAL_MERCATOR] disable_storage: true meta_size: [1, 1] meta_buffer: 0 sources: [wms_source2] sources: wms_source1: type: wms req: url: http://localhost:42423/service1 layers: foo,bar forward_req_params: ['TIME', 'ElEvaTION'] wms_source2: type: wms req: url: http://localhost:42423/service2 layers: foo,bar forward_req_params: ['time', 'elevation'] mapproxy-1.11.0/mapproxy/test/system/fixture/xslt_featureinfo.yaml000066400000000000000000000022221320454472400255710ustar00rootroot00000000000000services: wms: featureinfo_xslt: html: ./fi_out_html.xsl xml: ./fi_out.xsl layers: - name: fi_layer title: Layer with fi source sources: [fi_wms1] - name: fi_without_xslt_layer title: Layer with fi source sources: [fi_without_xslt] - name: fi_multi_layer title: Layer with fi source sources: [fi_wms1, fi_wms2, fi_wms3] sources: fi_wms1: type: wms wms_opts: version: 1.3.0 featureinfo: true featureinfo_xslt: ./fi_in.xsl featureinfo_format: text/xml req: url: http://localhost:42423/service_a layers: a_one fi_wms2: type: wms wms_opts: featureinfo: true featureinfo_xslt: ./fi_in.xsl featureinfo_format: text/xml req: url: http://localhost:42423/service_b layers: b_one fi_wms3: type: wms wms_opts: featureinfo: true featureinfo_xslt: ./fi_in_html.xsl featureinfo_format: text/html req: url: http://localhost:42423/service_d layers: d_one fi_without_xslt: type: wms wms_opts: featureinfo: true req: url: http://localhost:42423/service_c layers: c_onemapproxy-1.11.0/mapproxy/test/system/test_arcgis.py000066400000000000000000000124411320454472400225230ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division from io import BytesIO from mapproxy.request.wms import WMS111FeatureInfoRequest from mapproxy.test.image import is_png, create_tmp_image from mapproxy.test.http import mock_httpd from mapproxy.test.system import module_setup, module_teardown, SystemTest from nose.tools import eq_ test_config = {} def setup_module(): module_setup(test_config, 'arcgis.yaml') def teardown_module(): module_teardown(test_config) transp = create_tmp_image((512, 512), mode='RGBA', color=(0, 0, 0, 0)) class TestArcgisSource(SystemTest): config = test_config def setup(self): SystemTest.setup(self) self.common_fi_req = WMS111FeatureInfoRequest(url='/service?', param=dict(x='10', y='20', width='200', height='200', layers='app2_with_layers_fi_layer', format='image/png', query_layers='app2_with_layers_fi_layer', styles='', bbox='1000,400,2000,1400', srs='EPSG:3857', info_format='application/json')) def test_get_tile(self): expected_req = [({'path': '/arcgis/rest/services/ExampleLayer/ImageServer/exportImage?f=image&format=png&imageSR=900913&bboxSR=900913&bbox=-20037508.342789244,-20037508.342789244,20037508.342789244,20037508.342789244&size=512,512'}, {'body': transp, 'headers': {'content-type': 'image/png'}}), ] with mock_httpd(('localhost', 42423), expected_req, bbox_aware_query_comparator=True): resp = self.app.get('/tms/1.0.0/app2_layer/0/0/1.png') eq_(resp.content_type, 'image/png') eq_(resp.content_length, len(resp.body)) data = BytesIO(resp.body) assert is_png(data) def test_get_tile_with_layer(self): expected_req = [({'path': '/arcgis/rest/services/ExampleLayer/MapServer/export?f=image&format=png&layers=show:0,1&imageSR=900913&bboxSR=900913&bbox=-20037508.342789244,-20037508.342789244,20037508.342789244,20037508.342789244&size=512,512'}, {'body': transp, 'headers': {'content-type': 'image/png'}}), ] with mock_httpd(('localhost', 42423), expected_req, bbox_aware_query_comparator=True): resp = self.app.get('/tms/1.0.0/app2_with_layers_layer/0/0/1.png') eq_(resp.content_type, 'image/png') eq_(resp.content_length, len(resp.body)) data = BytesIO(resp.body) assert is_png(data) def test_get_tile_from_missing_arcgis_layer(self): expected_req = [({'path': '/arcgis/rest/services/NonExistentLayer/ImageServer/exportImage?f=image&format=png&imageSR=900913&bboxSR=900913&bbox=-20037508.342789244,-20037508.342789244,20037508.342789244,20037508.342789244&size=512,512'}, {'body': b'', 'status': 400}), ] with mock_httpd(('localhost', 42423), expected_req, bbox_aware_query_comparator=True): resp = self.app.get('/tms/1.0.0/app2_wrong_url_layer/0/0/1.png', status=500) eq_(resp.status_code, 500) def test_identify(self): expected_req = [( {'path': '/arcgis/rest/services/ExampleLayer/MapServer/identify?f=json&' 'geometry=1050.000000,1300.000000&returnGeometry=true&imageDisplay=200,200,96' '&mapExtent=1000.0,400.0,2000.0,1400.0&layers=show:1,2,3' '&tolerance=10&geometryType=esriGeometryPoint&sr=3857' }, {'body': b'{"results": []}', 'headers': {'content-type': 'application/json'}}), ] with mock_httpd(('localhost', 42423), expected_req, bbox_aware_query_comparator=True): resp = self.app.get(self.common_fi_req) eq_(resp.content_type, 'application/json') eq_(resp.content_length, len(resp.body)) eq_(resp.body, b'{"results": []}') def test_transformed_identify(self): expected_req = [( {'path': '/arcgis/rest/services/ExampleLayer/MapServer/identify?f=json&' 'geometry=573295.377585,6927820.884193&returnGeometry=true&imageDisplay=200,321,96' '&mapExtent=556597.453966,6446275.84102,890555.926346,6982997.92039&layers=show:1,2,3' '&tolerance=10&geometryType=esriGeometryPoint&sr=3857' }, {'body': b'{"results": []}', 'headers': {'content-type': 'application/json'}}), ] with mock_httpd(('localhost', 42423), expected_req): self.common_fi_req.params.bbox = '5,50,8,53' self.common_fi_req.params.srs = 'EPSG:4326' resp = self.app.get(self.common_fi_req) eq_(resp.content_type, 'application/json') eq_(resp.content_length, len(resp.body)) eq_(resp.body, b'{"results": []}') mapproxy-1.11.0/mapproxy/test/system/test_auth.py000066400000000000000000001015201320454472400222110ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division from mapproxy.test.system import module_setup, module_teardown, SystemTest from mapproxy.test.image import img_from_buf, create_tmp_image, is_transparent from mapproxy.test.http import MockServ from nose.tools import eq_ from mapproxy.util.geom import geom_support from mapproxy.srs import bbox_equals test_config = {} def setup_module(): module_setup(test_config, 'auth.yaml') def teardown_module(): module_teardown(test_config) TESTSERVER_ADDRESS = 'localhost', 42423 CAPABILITIES_REQ = "/service?request=GetCapabilities&service=WMS&Version=1.1.1" MAP_REQ = ("/service?request=GetMap&service=WMS&Version=1.1.1&SRS=EPSG:4326" "&BBOX=-80,-40,0,0&WIDTH=200&HEIGHT=100&styles=&FORMAT=image/png&") FI_REQ = ("/service?request=GetFeatureInfo&service=WMS&Version=1.1.1&SRS=EPSG:4326" "&BBOX=-80,-40,0,0&WIDTH=200&HEIGHT=100&styles=&FORMAT=image/png&X=10&Y=10&") if not geom_support: from nose.plugins.skip import SkipTest raise SkipTest('requires Shapely') class TestWMSAuth(SystemTest): config = test_config # ### # see mapproxy.test.unit.test_auth for WMS GetMap request tests # ### def test_capabilities_authorize_all(self): def auth(service, layers, **kw): eq_(service, 'wms.capabilities') eq_(len(layers), 8) return {'authorized': 'full'} resp = self.app.get(CAPABILITIES_REQ, extra_environ={'mapproxy.authorize': auth}) xml = resp.lxml eq_(xml.xpath('//Layer/Name/text()'), ['layer1', 'layer1a', 'layer1b', 'layer2', 'layer2a', 'layer2b', 'layer2b1', 'layer3']) def test_capabilities_authorize_none(self): def auth(service, layers, **kw): eq_(service, 'wms.capabilities') eq_(len(layers), 8) return {'authorized': 'none'} self.app.get(CAPABILITIES_REQ, extra_environ={'mapproxy.authorize': auth}, status=403) def test_capabilities_unauthenticated(self): def auth(service, layers, **kw): eq_(service, 'wms.capabilities') eq_(len(layers), 8) return {'authorized': 'unauthenticated'} self.app.get(CAPABILITIES_REQ, extra_environ={'mapproxy.authorize': auth}, status=401) def test_capabilities_authorize_partial(self): def auth(service, layers, **kw): eq_(service, 'wms.capabilities') eq_(len(layers), 8) return { 'authorized': 'partial', 'layers': { 'layer1a': {'map': True}, 'layer2': {'map': True}, 'layer2b': {'map': True}, 'layer2b1': {'map': True}, } } resp = self.app.get(CAPABILITIES_REQ, extra_environ={'mapproxy.authorize': auth}) xml = resp.lxml # layer1a not included cause root layer (layer1) is not permitted eq_(xml.xpath('//Layer/Name/text()'), ['layer2', 'layer2b', 'layer2b1']) def test_capabilities_authorize_partial_limited_to(self): def auth(service, layers, **kw): eq_(service, 'wms.capabilities') eq_(len(layers), 8) return { 'authorized': 'partial', 'layers': { 'layer1a': {'map': True}, 'layer2': {'map': True, 'limited_to': {'srs': 'EPSG:4326', 'geometry': [-40.0, -50.0, 0.0, 5.0]}}, 'layer2b': {'map': True}, 'layer2b1': {'map': True}, } } resp = self.app.get(CAPABILITIES_REQ, extra_environ={'mapproxy.authorize': auth}) xml = resp.lxml # layer1a not included cause root layer (layer1) is not permitted eq_(xml.xpath('//Layer/Name/text()'), ['layer2', 'layer2b', 'layer2b1']) limited_bbox = xml.xpath('//Layer/LatLonBoundingBox')[1] eq_(float(limited_bbox.attrib['minx']), -40.0) eq_(float(limited_bbox.attrib['miny']), -50.0) eq_(float(limited_bbox.attrib['maxx']), 0.0) eq_(float(limited_bbox.attrib['maxy']), 5.0) def test_capabilities_authorize_partial_global_limited(self): def auth(service, layers, **kw): eq_(service, 'wms.capabilities') eq_(len(layers), 8) return { 'authorized': 'partial', 'limited_to': {'srs': 'EPSG:4326', 'geometry': [-40.0, -50.0, 0.0, 5.0]}, 'layers': { 'layer1': {'map': True}, 'layer1a': {'map': True}, 'layer2': {'map': True}, 'layer2b': {'map': True}, 'layer2b1': {'map': True}, } } resp = self.app.get(CAPABILITIES_REQ, extra_environ={'mapproxy.authorize': auth}) xml = resp.lxml # print resp.body # layer2/2b/2b1 not included because coverage of 2b1 is outside of global limited_to eq_(xml.xpath('//Layer/Name/text()'), ['layer1', 'layer1a']) limited_bbox = xml.xpath('//Layer/LatLonBoundingBox')[1] eq_(float(limited_bbox.attrib['minx']), -40.0) eq_(float(limited_bbox.attrib['miny']), -50.0) eq_(float(limited_bbox.attrib['maxx']), 0.0) eq_(float(limited_bbox.attrib['maxy']), 5.0) def test_capabilities_authorize_partial_with_fi(self): def auth(service, layers, **kw): eq_(service, 'wms.capabilities') eq_(len(layers), 8) return { 'authorized': 'partial', 'layers': { 'layer1': {'map': True}, 'layer1a': {'map': True}, 'layer2': {'map': True, 'featureinfo': True}, 'layer2b': {'map': True, 'featureinfo': True}, 'layer2b1': {'map': True, 'featureinfo': True}, } } resp = self.app.get(CAPABILITIES_REQ, extra_environ={'mapproxy.authorize': auth}) xml = resp.lxml eq_(xml.xpath('//Layer/Name/text()'), ['layer1', 'layer1a', 'layer2', 'layer2b', 'layer2b1']) layers = xml.xpath('//Layer') assert layers[3][0].text == 'layer2' assert layers[3].attrib['queryable'] == '1' assert layers[4][0].text == 'layer2b' assert layers[4].attrib['queryable'] == '1' assert layers[5][0].text == 'layer2b1' assert layers[5].attrib['queryable'] == '1' def test_get_map_authorized(self): def auth(service, layers, query_extent, **kw): eq_(query_extent, ('EPSG:4326', (-80.0, -40.0, 0.0, 0.0))) eq_(service, 'wms.map') eq_(len(layers), 1) return { 'authorized': 'partial', 'layers': { 'layer1': {'map': True}, } } resp = self.app.get(MAP_REQ + 'layers=layer1', extra_environ={'mapproxy.authorize': auth}) eq_(resp.content_type, 'image/png') def test_get_map_authorized_limited(self): def auth(service, layers, query_extent, **kw): eq_(query_extent, ('EPSG:4326', (-80.0, -40.0, 0.0, 0.0))) eq_(service, 'wms.map') eq_(len(layers), 1) return { 'authorized': 'partial', 'layers': { 'layer1': { 'map': True, 'limited_to': {'srs': 'EPSG:4326', 'geometry': [-40.0, -40.0, 0.0, 0.0]}, }, } } resp = self.app.get(MAP_REQ + 'layers=layer1', extra_environ={'mapproxy.authorize': auth}) eq_(resp.content_type, 'image/png') img = img_from_buf(resp.body) # left part not authorized, only bgcolor assert len(img.crop((0, 0, 100, 100)).getcolors()) == 1 # right part authorized, bgcolor + text assert len(img.crop((100, 0, 200, 100)).getcolors()) >= 2 def test_get_map_authorized_global_limited(self): def auth(service, layers, query_extent, **kw): eq_(query_extent, ('EPSG:4326', (-80.0, -40.0, 0.0, 0.0))) eq_(service, 'wms.map') eq_(len(layers), 1) return { 'authorized': 'partial', 'limited_to': {'srs': 'EPSG:4326', 'geometry': [-20.0, -40.0, 0.0, 0.0]}, 'layers': { 'layer1': { 'map': True, 'limited_to': {'srs': 'EPSG:4326', 'geometry': [-40.0, -40.0, 0.0, 0.0]}, }, } } resp = self.app.get(MAP_REQ + 'layers=layer1', extra_environ={'mapproxy.authorize': auth}) eq_(resp.content_type, 'image/png') img = img_from_buf(resp.body) # left part not authorized, only bgcolor assert len(img.crop((0, 0, 100, 100)).getcolors()) == 1 # right part authorized, bgcolor + text assert len(img.crop((100, 0, 200, 100)).getcolors()) >= 2 def test_get_map_authorized_none(self): def auth(service, layers, query_extent, **kw): eq_(query_extent, ('EPSG:4326', (-80.0, -40.0, 0.0, 0.0))) eq_(service, 'wms.map') eq_(len(layers), 1) return { 'authorized': 'partial', 'layers': { 'layer1': {'map': False}, } } self.app.get(MAP_REQ + 'layers=layer1', extra_environ={'mapproxy.authorize': auth}, status=403) def test_get_featureinfo_limited_to_inside(self): def auth(service, layers, query_extent, **kw): eq_(query_extent, ('EPSG:4326', (-80.0, -40.0, 0.0, 0.0))) eq_(service, 'wms.featureinfo') eq_(len(layers), 1) return { 'authorized': 'partial', 'layers': { 'layer1b': {'featureinfo': True, 'limited_to': {'srs': 'EPSG:4326', 'geometry': [-80.0, -40.0, 0.0, 0.0]}}, } } serv = MockServ(port=42423) serv.expects('/service?request=GetFeatureInfo&service=WMS&Version=1.1.1&SRS=EPSG:4326' '&BBOX=-80.0,-40.0,0.0,0.0&WIDTH=200&HEIGHT=100&styles=&FORMAT=image/png&X=10&Y=10' '&query_layers=fi&layers=fi') serv.returns(b'infoinfo') with serv: resp = self.app.get(FI_REQ + 'query_layers=layer1b&layers=layer1b', extra_environ={'mapproxy.authorize': auth}) eq_(resp.body, b'infoinfo') def test_get_featureinfo_limited_to_outside(self): def auth(service, layers, query_extent, **kw): eq_(query_extent, ('EPSG:4326', (-80.0, -40.0, 0.0, 0.0))) eq_(service, 'wms.featureinfo') eq_(len(layers), 1) return { 'authorized': 'partial', 'layers': { 'layer1b': {'featureinfo': True, 'limited_to': {'srs': 'EPSG:4326', 'geometry': [-80.0, -40.0, 0.0, -10.0]}}, } } resp = self.app.get(FI_REQ + 'query_layers=layer1b&layers=layer1b', extra_environ={'mapproxy.authorize': auth}) # empty response, FI request is outside of limited_to geometry eq_(resp.body, b'') def test_get_featureinfo_global_limited(self): def auth(service, layers, query_extent, **kw): eq_(query_extent, ('EPSG:4326', (-80.0, -40.0, 0.0, 0.0))) eq_(service, 'wms.featureinfo') eq_(len(layers), 1) return { 'authorized': 'partial', 'limited_to': {'srs': 'EPSG:4326', 'geometry': [-40.0, -40.0, 0.0, 0.0]}, 'layers': { 'layer1b': {'featureinfo': True}, }, } resp = self.app.get(FI_REQ + 'query_layers=layer1b&layers=layer1b', extra_environ={'mapproxy.authorize': auth}) # empty response, FI request is outside of limited_to geometry eq_(resp.body, b'') TMS_CAPABILITIES_REQ = '/tms/1.0.0' class TestTMSAuth(SystemTest): config = test_config def test_capabilities_authorize_all(self): def auth(service, layers, environ, **kw): eq_(environ['PATH_INFO'], '/tms/1.0.0') eq_(service, 'tms') eq_(len(layers), 6) return {'authorized': 'full'} resp = self.app.get(TMS_CAPABILITIES_REQ, extra_environ={'mapproxy.authorize': auth}) xml = resp.lxml eq_(xml.xpath('//TileMap/@title'), ['layer 1a', 'layer 1b', 'layer 1', 'layer 2a', 'layer 2b1', 'layer 3']) def test_capabilities_authorize_none(self): def auth(service, layers, **kw): eq_(service, 'tms') eq_(len(layers), 6) return {'authorized': 'none'} self.app.get(TMS_CAPABILITIES_REQ, extra_environ={'mapproxy.authorize': auth}, status=403) def test_capabilities_unauthenticated(self): def auth(service, layers, **kw): eq_(service, 'tms') eq_(len(layers), 6) return {'authorized': 'unauthenticated'} self.app.get(TMS_CAPABILITIES_REQ, extra_environ={'mapproxy.authorize': auth}, status=401) def test_capabilities_authorize_partial(self): def auth(service, layers, **kw): eq_(service, 'tms') eq_(len(layers), 6) return { 'authorized': 'partial', 'layers': { 'layer1a': {'tile': True}, 'layer1b': {'tile': False}, 'layer2': {'tile': True}, 'layer2b': {'tile': True}, 'layer2b1': {'tile': True}, } } resp = self.app.get(TMS_CAPABILITIES_REQ, extra_environ={'mapproxy.authorize': auth}) xml = resp.lxml eq_(xml.xpath('//TileMap/@title'), ['layer 1a', 'layer 2b1']) def test_layer_capabilities_authorize_none(self): def auth(service, layers, **kw): eq_(service, 'tms') eq_(len(layers), 1) return { 'authorized': 'none', } self.app.get(TMS_CAPABILITIES_REQ + '/layer1', extra_environ={'mapproxy.authorize': auth}, status=403) def test_layer_capabilities_authorize_all(self): def auth(service, layers, **kw): eq_(service, 'tms') eq_(len(layers), 1) return { 'authorized': 'full', } resp = self.app.get(TMS_CAPABILITIES_REQ + '/layer1', extra_environ={'mapproxy.authorize': auth}) xml = resp.lxml eq_(xml.xpath('//TileMap/Title/text()'), ['layer 1']) def test_layer_capabilities_authorize_partial(self): def auth(service, layers, **kw): eq_(service, 'tms') eq_(len(layers), 1) return { 'authorized': 'partial', 'layers': { 'layer1': {'tile': True}, } } resp = self.app.get(TMS_CAPABILITIES_REQ + '/layer1', extra_environ={'mapproxy.authorize': auth}) xml = resp.lxml eq_(xml.xpath('//TileMap/Title/text()'), ['layer 1']) def test_layer_capabilities_deny_partial(self): def auth(service, layers, **kw): eq_(service, 'tms') eq_(len(layers), 1) return { 'authorized': 'partial', 'layers': { 'layer1': {'tile': False}, } } self.app.get(TMS_CAPABILITIES_REQ + '/layer1', extra_environ={'mapproxy.authorize': auth}, status=403) def test_get_tile(self): def auth(service, layers, environ, query_extent, **kw): eq_(environ['PATH_INFO'], '/tms/1.0.0/layer1_EPSG900913/0/0/0.png') eq_(service, 'tms') eq_(query_extent[0], 'EPSG:900913') assert bbox_equals(query_extent[1], (-20037508.342789244, -20037508.342789244, 0, 0)) eq_(len(layers), 1) return { 'authorized': 'partial', 'layers': { 'layer1': {'tile': True}, } } resp = self.app.get(TMS_CAPABILITIES_REQ + '/layer1_EPSG900913/0/0/0.png', extra_environ={'mapproxy.authorize': auth}) eq_(resp.content_type, 'image/png') assert resp.content_length > 1000 def test_get_tile_global_limited_to(self): # check with limited_to for all layers auth_dict = { 'authorized': 'partial', 'limited_to': { 'geometry': [-180, -89, -90, 89], 'srs': 'EPSG:4326', }, 'layers': { 'layer3': {'tile': True}, } } self.check_get_tile_limited_to(auth_dict) def test_get_tile_layer_limited_to(self): # check with limited_to for one layer auth_dict = { 'authorized': 'partial', 'layers': { 'layer3': { 'tile': True, 'limited_to': { 'geometry': [-180, -89, -90, 89], 'srs': 'EPSG:4326', } }, } } self.check_get_tile_limited_to(auth_dict) def check_get_tile_limited_to(self, auth_dict): def auth(service, layers, environ, query_extent, **kw): eq_(environ['PATH_INFO'], '/tms/1.0.0/layer3/0/0/0.jpeg') eq_(service, 'tms') eq_(len(layers), 1) eq_(query_extent[0], 'EPSG:900913') assert bbox_equals(query_extent[1], (-20037508.342789244, -20037508.342789244, 0, 0)) return auth_dict serv = MockServ(port=42423) serv.expects('/1/0/0.png') serv.returns(create_tmp_image((256, 256), color=(255, 0, 0)), headers={'content-type': 'image/png'}) with serv: resp = self.app.get(TMS_CAPABILITIES_REQ + '/layer3/0/0/0.jpeg', extra_environ={'mapproxy.authorize': auth}) eq_(resp.content_type, 'image/png') img = img_from_buf(resp.body) img = img.convert('RGBA') # left part authorized, red eq_(img.crop((0, 0, 127, 255)).getcolors()[0], (127*255, (255, 0, 0, 255))) # right part not authorized, transparent eq_(img.crop((129, 0, 255, 255)).getcolors()[0][1][3], 0) def test_get_tile_authorize_none(self): def auth(service, layers, **kw): eq_(service, 'tms') eq_(len(layers), 1) return { 'authorized': 'none', } self.app.get(TMS_CAPABILITIES_REQ + '/layer1/0/0/0.png', extra_environ={'mapproxy.authorize': auth}, status=403) class TestKMLAuth(SystemTest): config = test_config def test_superoverlay_authorize_all(self): def auth(service, layers, environ, **kw): eq_(environ['PATH_INFO'], '/kml/layer1/0/0/0.kml') eq_(service, 'kml') eq_(len(layers), 1) return {'authorized': 'full'} resp = self.app.get('/kml/layer1/0/0/0.kml', extra_environ={'mapproxy.authorize': auth}) xml = resp.lxml eq_(xml.xpath('kml:Document/kml:name/text()', namespaces={'kml': 'http://www.opengis.net/kml/2.2'}), ['layer1']) def test_superoverlay_authorize_none(self): def auth(service, layers, **kw): eq_(service, 'kml') eq_(len(layers), 1) return {'authorized': 'none'} self.app.get('/kml/layer1/0/0/0.kml', extra_environ={'mapproxy.authorize': auth}, status=403) def test_superoverlay_unauthenticated(self): def auth(service, layers, **kw): eq_(service, 'kml') eq_(len(layers), 1) return {'authorized': 'unauthenticated'} self.app.get('/kml/layer1/0/0/0.kml', extra_environ={'mapproxy.authorize': auth}, status=401) def test_superoverlay_authorize_partial(self): def auth(service, layers, query_extent, **kw): eq_(service, 'kml') eq_(len(layers), 1) eq_(query_extent[0], 'EPSG:900913') assert bbox_equals(query_extent[1], (-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244)) return { 'authorized': 'partial', 'layers': { 'layer1': {'tile': True}, } } resp = self.app.get('/kml/layer1/0/0/0.kml', extra_environ={'mapproxy.authorize': auth}) xml = resp.lxml eq_(xml.xpath('kml:Document/kml:name/text()', namespaces={'kml': 'http://www.opengis.net/kml/2.2'}), ['layer1']) def test_superoverlay_deny_partial(self): def auth(service, layers, **kw): eq_(service, 'kml') eq_(len(layers), 1) return { 'authorized': 'partial', 'layers': { 'layer1': {'tile': False}, } } self.app.get('/kml/layer1/0/0/0.kml', extra_environ={'mapproxy.authorize': auth}, status=403) def test_get_tile_global_limited_to(self): # check with limited_to for all layers auth_dict = { 'authorized': 'partial', 'limited_to': { 'geometry': [-180, -89, -90, 89], 'srs': 'EPSG:4326', }, 'layers': { 'layer3': {'tile': True}, } } self.check_get_tile_limited_to(auth_dict) def test_get_tile_layer_limited_to(self): # check with limited_to for one layer auth_dict = { 'authorized': 'partial', 'layers': { 'layer3': { 'tile': True, 'limited_to': { 'geometry': [-180, -89, -90, 89], 'srs': 'EPSG:4326', } }, } } self.check_get_tile_limited_to(auth_dict) def check_get_tile_limited_to(self, auth_dict): def auth(service, layers, environ, query_extent, **kw): eq_(environ['PATH_INFO'], '/kml/layer3_EPSG900913/1/0/0.jpeg') eq_(service, 'kml') eq_(len(layers), 1) eq_(query_extent[0], 'EPSG:900913') assert bbox_equals(query_extent[1], (-20037508.342789244, -20037508.342789244, 0, 0)) return auth_dict serv = MockServ(port=42423) serv.expects('/1/0/0.png') serv.returns(create_tmp_image((256, 256), color=(255, 0, 0)), headers={'content-type': 'image/png'}) with serv: resp = self.app.get('/kml/layer3_EPSG900913/1/0/0.jpeg', extra_environ={'mapproxy.authorize': auth}) eq_(resp.content_type, 'image/png') img = img_from_buf(resp.body) img = img.convert('RGBA') # left part authorized, red eq_(img.crop((0, 0, 127, 255)).getcolors()[0], (127*255, (255, 0, 0, 255))) # right part not authorized, transparent eq_(img.crop((129, 0, 255, 255)).getcolors()[0][1][3], 0) WMTS_CAPABILITIES_REQ = '/wmts/1.0.0/WMTSCapabilities.xml' class TestWMTSAuth(SystemTest): config = test_config def test_capabilities_authorize_all(self): def auth(service, layers, environ, **kw): eq_(environ['PATH_INFO'], '/wmts/1.0.0/WMTSCapabilities.xml') eq_(service, 'wmts') eq_(len(layers), 6) return {'authorized': 'full'} resp = self.app.get(WMTS_CAPABILITIES_REQ, extra_environ={'mapproxy.authorize': auth}) xml = resp.lxml eq_(set(xml.xpath('//wmts:Layer/ows:Title/text()', namespaces={'wmts': 'http://www.opengis.net/wmts/1.0', 'ows': 'http://www.opengis.net/ows/1.1'})), set(['layer 1b', 'layer 1a', 'layer 2a', 'layer 2b1', 'layer 1', 'layer 3'])) def test_capabilities_authorize_none(self): def auth(service, layers, **kw): eq_(service, 'wmts') eq_(len(layers), 6) return {'authorized': 'none'} self.app.get(WMTS_CAPABILITIES_REQ, extra_environ={'mapproxy.authorize': auth}, status=403) def test_capabilities_unauthenticated(self): def auth(service, layers, **kw): eq_(service, 'wmts') eq_(len(layers), 6) return {'authorized': 'unauthenticated'} self.app.get(WMTS_CAPABILITIES_REQ, extra_environ={'mapproxy.authorize': auth}, status=401) def test_capabilities_authorize_partial(self): def auth(service, layers, **kw): eq_(service, 'wmts') eq_(len(layers), 6) return { 'authorized': 'partial', 'layers': { 'layer1a': {'tile': True}, 'layer1b': {'tile': False}, 'layer2': {'tile': True}, 'layer2b': {'tile': True}, 'layer2b1': {'tile': True}, } } resp = self.app.get(WMTS_CAPABILITIES_REQ, extra_environ={'mapproxy.authorize': auth}) xml = resp.lxml eq_(set(xml.xpath('//wmts:Layer/ows:Title/text()', namespaces={'wmts': 'http://www.opengis.net/wmts/1.0', 'ows': 'http://www.opengis.net/ows/1.1'})), set(['layer 1a', 'layer 2b1'])) def test_get_tile(self): def auth(service, layers, environ, query_extent, **kw): eq_(environ['PATH_INFO'], '/wmts/layer1/GLOBAL_MERCATOR/0/0/0.png') eq_(service, 'wmts') eq_(len(layers), 1) eq_(query_extent[0], 'EPSG:900913') assert bbox_equals(query_extent[1], (-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244)) return { 'authorized': 'partial', 'layers': { 'layer1': {'tile': True}, } } resp = self.app.get('/wmts/layer1/GLOBAL_MERCATOR/0/0/0.png', extra_environ={'mapproxy.authorize': auth}) eq_(resp.content_type, 'image/png') assert resp.content_length > 1000 def test_get_tile_global_limited_to(self): # check with limited_to for all layers auth_dict = { 'authorized': 'partial', 'limited_to': { 'geometry': [-180, -89, -90, 89], 'srs': 'EPSG:4326', }, 'layers': { 'layer3': {'tile': True}, } } self.check_get_tile_limited_to(auth_dict) def test_get_tile_layer_limited_to(self): # check with limited_to for one layer auth_dict = { 'authorized': 'partial', 'layers': { 'layer3': { 'tile': True, 'limited_to': { 'geometry': [-180, -89, -90, 89], 'srs': 'EPSG:4326', } }, } } self.check_get_tile_limited_to(auth_dict) def check_get_tile_limited_to(self, auth_dict): def auth(service, layers, environ, query_extent, **kw): eq_(environ['PATH_INFO'], '/wmts/layer3/GLOBAL_MERCATOR/1/0/0.jpeg') eq_(service, 'wmts') eq_(len(layers), 1) eq_(query_extent[0], 'EPSG:900913') assert bbox_equals(query_extent[1], (-20037508.342789244, 0, 0, 20037508.342789244)) return auth_dict serv = MockServ(port=42423) serv.expects('/1/0/1.png') serv.returns(create_tmp_image((256, 256), color=(255, 0, 0)), headers={'content-type': 'image/png'}) with serv: resp = self.app.get('/wmts/layer3/GLOBAL_MERCATOR/1/0/0.jpeg', extra_environ={'mapproxy.authorize': auth}) eq_(resp.content_type, 'image/png') img = img_from_buf(resp.body) img = img.convert('RGBA') # left part authorized, red eq_(img.crop((0, 0, 127, 255)).getcolors()[0], (127*255, (255, 0, 0, 255))) # right part not authorized, transparent eq_(img.crop((129, 0, 255, 255)).getcolors()[0][1][3], 0) def test_get_tile_limited_to_outside(self): def auth(service, layers, environ, **kw): eq_(environ['PATH_INFO'], '/wmts/layer3/GLOBAL_MERCATOR/2/0/0.jpeg') eq_(service, 'wmts') eq_(len(layers), 1) return { 'authorized': 'partial', 'limited_to': { 'geometry': [0, -89, 90, 89], 'srs': 'EPSG:4326', }, 'layers': { 'layer3': {'tile': True}, } } resp = self.app.get('/wmts/layer3/GLOBAL_MERCATOR/2/0/0.jpeg', extra_environ={'mapproxy.authorize': auth}) eq_(resp.content_type, 'image/png') is_transparent(resp.body) def test_get_tile_limited_to_inside(self): def auth(service, layers, environ, **kw): eq_(environ['PATH_INFO'], '/wmts/layer3/GLOBAL_MERCATOR/1/0/0.jpeg') eq_(service, 'wmts') eq_(len(layers), 1) return { 'authorized': 'partial', 'limited_to': { 'geometry': [-180, -89, 180, 89], 'srs': 'EPSG:4326', }, 'layers': { 'layer3': {'tile': True}, } } serv = MockServ(port=42423) serv.expects('/1/0/1.png') serv.returns(create_tmp_image((256, 256), color=(255, 0, 0)), headers={'content-type': 'image/png'}) with serv: resp = self.app.get('/wmts/layer3/GLOBAL_MERCATOR/1/0/0.jpeg', extra_environ={'mapproxy.authorize': auth}) eq_(resp.content_type, 'image/jpeg') img = img_from_buf(resp.body) eq_(img.getcolors()[0], (256*256, (255, 0, 0))) def test_get_tile_kvp(self): def auth(service, layers, environ, **kw): eq_(environ['PATH_INFO'], '/service') eq_(service, 'wmts') eq_(len(layers), 1) return { 'authorized': 'partial', 'layers': { 'layer1': {'tile': True}, } } resp = self.app.get('/service?service=WMTS&version=1.0.0&layer=layer1&request=GetTile&' 'style=&tilematrixset=GLOBAL_MERCATOR&tilematrix=00&tilerow=0&tilecol=0&format=image/png', extra_environ={'mapproxy.authorize': auth}) eq_(resp.content_type, 'image/png') def test_get_tile_authorize_none(self): def auth(service, layers, **kw): eq_(service, 'wmts') eq_(len(layers), 1) return { 'authorized': 'none', } self.app.get('/wmts/layer1/GLOBAL_MERCATOR/0/0/0.png', extra_environ={'mapproxy.authorize': auth}, status=403) def test_get_tile_authorize_none_kvp(self): def auth(service, layers, environ, **kw): eq_(environ['PATH_INFO'], '/service') eq_(service, 'wmts') eq_(len(layers), 1) return { 'authorized': 'partial', 'layers': { 'layer1': {'tile': False}, } } self.app.get('/service?service=WMTS&version=1.0.0&layer=layer1&request=GetTile&' 'style=&tilematrixset=GLOBAL_MERCATOR&tilematrix=00&tilerow=0&tilecol=0&format=image/png', extra_environ={'mapproxy.authorize': auth}, status=403) class TestDemoAuth(SystemTest): config = test_config def test_authorize_all(self): def auth(service, layers, environ, **kw): return {'authorized': 'full'} self.app.get('/demo', extra_environ={'mapproxy.authorize': auth}) def test_authorize_none(self): def auth(service, layers, environ, **kw): return {'authorized': 'none'} self.app.get('/demo', extra_environ={'mapproxy.authorize': auth}, status=403) def test_unauthenticated(self): def auth(service, layers, environ, **kw): return {'authorized': 'unauthenticated'} self.app.get('/demo', extra_environ={'mapproxy.authorize': auth}, status=401) def test_superoverlay_authorize_none(self): def auth(service, layers, **kw): eq_(service, 'kml') eq_(len(layers), 1) return {'authorized': 'none'} self.app.get('/kml/layer1/0/0/0.kml', extra_environ={'mapproxy.authorize': auth}, status=403) def test_superoverlay_unauthenticated(self): def auth(service, layers, **kw): eq_(service, 'kml') eq_(len(layers), 1) return {'authorized': 'unauthenticated'} self.app.get('/kml/layer1/0/0/0.kml', extra_environ={'mapproxy.authorize': auth}, status=401) mapproxy-1.11.0/mapproxy/test/system/test_behind_proxy.py000066400000000000000000000067341320454472400237550ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division from mapproxy.test.system import module_setup, module_teardown, SystemTest, make_base_config test_config = {} base_config = make_base_config(test_config) def setup_module(): module_setup(test_config, 'layer.yaml', with_cache_data=True) def teardown_module(): module_teardown(test_config) class TestWMSBehindProxy(SystemTest): """ Check WMS OnlineResources for requests behind HTTP proxies. """ config = test_config def test_no_proxy(self): resp = self.app.get('http://localhost/service?SERVICE=WMS&REQUEST=GetCapabilities' '&VERSION=1.1.0') assert '"http://localhost/service' in resp def test_with_script_name(self): resp = self.app.get('http://localhost/service?SERVICE=WMS&REQUEST=GetCapabilities' '&VERSION=1.1.0', extra_environ={'HTTP_X_SCRIPT_NAME': '/foo'}) assert '"http://localhost/service' not in resp assert '"http://localhost/foo/service' in resp def test_with_host(self): resp = self.app.get('http://localhost/service?SERVICE=WMS&REQUEST=GetCapabilities' '&VERSION=1.1.0', extra_environ={'HTTP_HOST': 'example.org'}) assert '"http://localhost/service' not in resp assert '"http://example.org/service' in resp def test_with_host_and_script_name(self): resp = self.app.get('http://localhost/service?SERVICE=WMS&REQUEST=GetCapabilities' '&VERSION=1.1.0', extra_environ={'HTTP_X_SCRIPT_NAME': '/foo', 'HTTP_HOST': 'example.org'}) assert '"http://localhost/service' not in resp assert '"http://example.org/foo/service' in resp def test_with_forwarded_host(self): resp = self.app.get('http://localhost/service?SERVICE=WMS&REQUEST=GetCapabilities' '&VERSION=1.1.0', extra_environ={'HTTP_X_FORWARDED_HOST': 'example.org, bar.org'}) assert '"http://localhost/service' not in resp assert '"http://example.org/service' in resp def test_with_forwarded_host_and_script_name(self): resp = self.app.get('http://localhost/service?SERVICE=WMS&REQUEST=GetCapabilities' '&VERSION=1.1.0', extra_environ={'HTTP_X_FORWARDED_HOST': 'example.org', 'HTTP_X_SCRIPT_NAME': '/foo'}) assert '"http://localhost/service' not in resp assert '"http://example.org/foo/service' in resp def test_with_forwarded_proto_and_script_name_and_host(self): resp = self.app.get('http://localhost/service?SERVICE=WMS&REQUEST=GetCapabilities' '&VERSION=1.1.0', extra_environ={ 'HTTP_X_FORWARDED_PROTO': 'https', 'HTTP_X_SCRIPT_NAME': '/foo', 'HTTP_HOST': 'example.org:443' }) assert '"http://localhost/service' not in resp assert '"https://example.org/foo/service' in resp mapproxy-1.11.0/mapproxy/test/system/test_cache_band_merge.py000066400000000000000000000071211320454472400244600ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2016 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.request.wms import WMS111MapRequest from mapproxy.request.wmts import WMTS100CapabilitiesRequest from mapproxy.test.image import img_from_buf from mapproxy.test.system import module_setup, module_teardown, SystemTest, make_base_config from nose.tools import eq_ test_config = {} base_config = make_base_config(test_config) def setup_module(): module_setup(test_config, 'cache_band_merge.yaml', with_cache_data=True) def teardown_module(): module_teardown(test_config) class TestCacheSource(SystemTest): # test various band merge configurations with # cached base tile 0/0/0.png (R: 50 G: 100 B: 200) config = test_config def setup(self): SystemTest.setup(self) self.common_cap_req = WMTS100CapabilitiesRequest(url='/service?', param=dict(service='WMTS', version='1.0.0', request='GetCapabilities')) self.common_map_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1', bbox='-180,0,0,80', width='100', height='100', layers='dop_l', srs='EPSG:4326', format='image/png', styles='', request='GetMap')) def test_capabilities(self): req = str(self.common_cap_req) resp = self.app.get(req) eq_(resp.content_type, 'application/xml') def test_get_tile_021(self): resp = self.app.get('/wmts/dop_021/GLOBAL_WEBMERCATOR/0/0/0.png') eq_(resp.content_type, 'image/png') img = img_from_buf(resp.body) eq_(img.mode, 'RGB') eq_(img.getpixel((0, 0)), (50, 200, 100)) def test_get_tile_l(self): resp = self.app.get('/wmts/dop_l/GLOBAL_WEBMERCATOR/0/0/0.png') eq_(resp.content_type, 'image/png') img = img_from_buf(resp.body) eq_(img.mode, 'L') eq_(img.getpixel((0, 0)), int(50*0.25+0.7*100+0.05*200)) def test_get_tile_0(self): resp = self.app.get('/wmts/dop_0/GLOBAL_WEBMERCATOR/0/0/0.png') eq_(resp.content_type, 'image/png') img = img_from_buf(resp.body) eq_(img.mode, 'RGB') # forced with image.mode eq_(img.getpixel((0, 0)), (50, 50, 50)) def test_get_tile_0122(self): resp = self.app.get('/wmts/dop_0122/GLOBAL_WEBMERCATOR/0/0/0.png') eq_(resp.content_type, 'image/png') img = img_from_buf(resp.body) eq_(img.mode, 'RGBA') eq_(img.getpixel((0, 0)), (50, 100, 200, 50)) def test_get_map_l(self): resp = self.app.get(str(self.common_map_req)) eq_(resp.content_type, 'image/png') img = img_from_buf(resp.body) eq_(img.mode, 'L') eq_(img.getpixel((0, 0)), int(50*0.25+0.7*100+0.05*200)) def test_get_map_l_jpeg(self): self.common_map_req.params.format = 'image/jpeg' resp = self.app.get(str(self.common_map_req)) eq_(resp.content_type, 'image/jpeg') img = img_from_buf(resp.body) eq_(img.mode, 'RGB') # L converted to RGB for jpeg eq_(img.getpixel((0, 0)), (92, 92, 92)) mapproxy-1.11.0/mapproxy/test/system/test_cache_geopackage.py000066400000000000000000000112371320454472400244660ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import os import shutil from io import BytesIO from mapproxy.request.wms import WMS111MapRequest from mapproxy.test.http import MockServ from mapproxy.test.image import is_png, create_tmp_image from mapproxy.test.system import prepare_env, create_app, module_teardown, SystemTest from mapproxy.cache.geopackage import GeopackageCache from mapproxy.grid import TileGrid from nose.tools import eq_ import sqlite3 test_config = {} def setup_module(): prepare_env(test_config, 'cache_geopackage.yaml') shutil.copy(os.path.join(test_config['fixture_dir'], 'cache.gpkg'), test_config['base_dir']) create_app(test_config) def teardown_module(): module_teardown(test_config) class TestGeopackageCache(SystemTest): config = test_config table_name = 'cache' def setup(self): SystemTest.setup(self) self.common_map_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1', bbox='-180,-80,0,0', width='200', height='200', layers='gpkg', srs='EPSG:4326', format='image/png', styles='', request='GetMap')) def test_get_map_cached(self): resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) def test_get_map_uncached(self): assert os.path.exists(os.path.join(test_config['base_dir'], 'cache.gpkg')) # already created on startup self.common_map_req.params.bbox = '-180,0,0,80' serv = MockServ(port=42423) serv.expects('/tiles/01/000/000/000/000/000/001.png') serv.returns(create_tmp_image((256, 256))) with serv: resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) # now cached resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) def test_bad_config_geopackage_no_gpkg_contents(self): gpkg_file = os.path.join(test_config['base_dir'], 'cache.gpkg') table_name = 'no_gpkg_contents' with sqlite3.connect(gpkg_file) as db: cur = db.execute('''SELECT name FROM sqlite_master WHERE type='table' AND name=?''', (table_name,)) content = cur.fetchone() assert content[0] == table_name with sqlite3.connect(gpkg_file) as db: cur = db.execute('''SELECT table_name FROM gpkg_contents WHERE table_name=?''', (table_name,)) content = cur.fetchone() assert not content GeopackageCache(gpkg_file, TileGrid(srs=4326), table_name=table_name) with sqlite3.connect(gpkg_file) as db: cur = db.execute('''SELECT table_name FROM gpkg_contents WHERE table_name=?''', (table_name,)) content = cur.fetchone() assert content[0] == table_name def test_bad_config_geopackage_no_spatial_ref_sys(self): gpkg_file = os.path.join(test_config['base_dir'], 'cache.gpkg') organization_coordsys_id = 3785 table_name='no_gpkg_spatial_ref_sys' with sqlite3.connect(gpkg_file) as db: cur = db.execute('''SELECT organization_coordsys_id FROM gpkg_spatial_ref_sys WHERE organization_coordsys_id=?''', (organization_coordsys_id,)) content = cur.fetchone() assert not content GeopackageCache(gpkg_file, TileGrid(srs=3785), table_name=table_name) with sqlite3.connect(gpkg_file) as db: cur = db.execute( '''SELECT organization_coordsys_id FROM gpkg_spatial_ref_sys WHERE organization_coordsys_id=?''', (organization_coordsys_id,)) content = cur.fetchone() assert content[0] == organization_coordsys_id mapproxy-1.11.0/mapproxy/test/system/test_cache_grid_names.py000066400000000000000000000102411320454472400245020ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2012 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os from mapproxy.test.image import tmp_image from mapproxy.test.http import mock_httpd from mapproxy.test.system import module_setup, module_teardown, SystemTest, make_base_config from nose.tools import eq_ test_config = {} base_config = make_base_config(test_config) def setup_module(): module_setup(test_config, 'cache_grid_names.yaml') def teardown_module(): module_teardown(test_config) class TestCacheGridNames(SystemTest): config = test_config def test_tms_capabilities(self): resp = self.app.get('/tms/1.0.0/') assert 'Cached Layer' in resp assert 'wms_cache/utm32n' in resp assert 'wms_cache_utm32n' not in resp xml = resp.lxml assert xml.xpath('count(//TileMap)') == 2 def test_tms_layer_capabilities(self): resp = self.app.get('/tms/1.0.0/wms_cache/utm32n') assert 'Cached Layer' in resp assert 'wms_cache/utm32n' in resp assert 'wms_cache_utm32n' not in resp xml = resp.lxml eq_(xml.xpath('count(//TileSet)'), 12) def test_kml(self): resp = self.app.get('/kml/wms_cache/utm32n/4/2/2.kml') assert b'wms_cache/utm32n' in resp.body def test_get_tile(self): with tmp_image((256, 256), format='jpeg') as img: expected_req = ({'path': r'/service?LAYERs=bar&SERVICE=WMS&FORMAT=image%2Fjpeg' '&REQUEST=GetMap&HEIGHT=256&SRS=EPSG%3A25832&styles=' '&VERSION=1.1.1&BBOX=283803.311362,5609091.90862,319018.942566,5644307.53982' '&WIDTH=256'}, {'body': img.read(), 'headers': {'content-type': 'image/jpeg'}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): resp = self.app.get('/tms/1.0.0/wms_cache/utm32n/4/2/2.jpeg') eq_(resp.content_type, 'image/jpeg') self.created_tiles.append('wms_cache/utm32n/04/000/000/002/000/000/002.jpeg') def test_get_tile_no_grid_name(self): # access tiles with grid name from TMS but cache still uses old SRS-code path with tmp_image((256, 256), format='jpeg') as img: expected_req = ({'path': r'/service?LAYERs=bar&SERVICE=WMS&FORMAT=image%2Fjpeg' '&REQUEST=GetMap&HEIGHT=256&SRS=EPSG%3A25832&styles=' '&VERSION=1.1.1&BBOX=283803.311362,5609091.90862,319018.942566,5644307.53982' '&WIDTH=256'}, {'body': img.read(), 'headers': {'content-type': 'image/jpeg'}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): resp = self.app.get('/tms/1.0.0/wms_cache_no_grid_name/utm32n/4/2/2.jpeg') eq_(resp.content_type, 'image/jpeg') self.created_tiles.append('wms_cache_no_grid_name_EPSG25832/04/000/000/002/000/000/002.jpeg') def created_tiles_filenames(self): base_dir = base_config().cache.base_dir for filename in self.created_tiles: yield os.path.join(base_dir, filename) def check_created_tiles(self): for filename in self.created_tiles_filenames(): if not os.path.exists(filename): assert False, "didn't found tile " + filename def teardown(self): self.check_created_tiles() for filename in self.created_tiles_filenames(): if os.path.exists(filename): os.remove(filename) mapproxy-1.11.0/mapproxy/test/system/test_cache_mbtiles.py000066400000000000000000000050641320454472400240400ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import os import shutil from io import BytesIO from mapproxy.request.wms import WMS111MapRequest from mapproxy.test.http import MockServ from mapproxy.test.image import is_png, create_tmp_image from mapproxy.test.system import prepare_env, create_app, module_teardown, SystemTest from nose.tools import eq_ test_config = {} def setup_module(): prepare_env(test_config, 'cache_mbtiles.yaml') shutil.copy(os.path.join(test_config['fixture_dir'], 'cache.mbtiles'), test_config['base_dir']) create_app(test_config) def teardown_module(): module_teardown(test_config) class TestMBTilesCache(SystemTest): config = test_config def setup(self): SystemTest.setup(self) self.common_map_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1', bbox='-180,-80,0,0', width='200', height='200', layers='mb', srs='EPSG:4326', format='image/png', styles='', request='GetMap')) def test_get_map_cached(self): resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) def test_get_map_uncached(self): mbtiles_file = os.path.join(test_config['base_dir'], 'cache.mbtiles') assert os.path.exists(mbtiles_file) # already created on startup self.common_map_req.params.bbox = '-180,0,0,80' serv = MockServ(port=42423) serv.expects('/tiles/01/000/000/000/000/000/001.png') serv.returns(create_tmp_image((256, 256))) with serv: resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) # now cached resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) mapproxy-1.11.0/mapproxy/test/system/test_cache_s3.py000066400000000000000000000071401320454472400227230ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2016 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division from io import BytesIO from mapproxy.request.wms import WMS111MapRequest from mapproxy.test.image import is_png, create_tmp_image from mapproxy.test.system import prepare_env, create_app, module_teardown, SystemTest from nose.tools import eq_ from nose.plugins.skip import SkipTest try: import boto3 from moto import mock_s3 except ImportError: boto3 = None mock_s3 = None test_config = {} _mock = None def setup_module(): if not mock_s3 or not boto3: raise SkipTest("boto3 and moto required for S3 tests") global _mock _mock = mock_s3() _mock.start() boto3.client("s3").create_bucket(Bucket="default_bucket") boto3.client("s3").create_bucket(Bucket="tiles") boto3.client("s3").create_bucket(Bucket="reversetiles") prepare_env(test_config, 'cache_s3.yaml') create_app(test_config) def teardown_module(): module_teardown(test_config) _mock.stop() class TestS3Cache(SystemTest): config = test_config table_name = 'cache' def setup(self): SystemTest.setup(self) self.common_map_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1', bbox='-150,-40,-140,-30', width='100', height='100', layers='default', srs='EPSG:4326', format='image/png', styles='', request='GetMap')) def test_get_map_cached(self): # mock_s3 interferes with MockServ, use boto to manually upload tile tile = create_tmp_image((256, 256)) boto3.client("s3").upload_fileobj( BytesIO(tile), Bucket='default_bucket', Key='default_cache/WebMerc/4/1/9.png', ) resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) def test_get_map_cached_quadkey(self): # mock_s3 interferes with MockServ, use boto to manually upload tile tile = create_tmp_image((256, 256)) boto3.client("s3").upload_fileobj( BytesIO(tile), Bucket='tiles', Key='quadkeytiles/2003.png', ) self.common_map_req.params.layers = 'quadkey' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) def test_get_map_cached_reverse_tms(self): # mock_s3 interferes with MockServ, use boto to manually upload tile tile = create_tmp_image((256, 256)) boto3.client("s3").upload_fileobj( BytesIO(tile), Bucket='tiles', Key='reversetiles/9/1/4.png', ) self.common_map_req.params.layers = 'reverse' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) mapproxy-1.11.0/mapproxy/test/system/test_cache_source.py000066400000000000000000000115561320454472400237040ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2012 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os from mapproxy.request.wms import WMS111MapRequest from mapproxy.test.image import tmp_image from mapproxy.test.http import mock_httpd from mapproxy.test.system import module_setup, module_teardown, SystemTest, make_base_config from nose.tools import eq_ test_config = {} base_config = make_base_config(test_config) def setup_module(): module_setup(test_config, 'cache_source.yaml') def teardown_module(): module_teardown(test_config) class TestCacheSource(SystemTest): config = test_config def test_tms_capabilities(self): resp = self.app.get('/tms/1.0.0/') assert 'transformed tile source' in resp xml = resp.lxml assert xml.xpath('count(//TileMap)') == 3 def test_get_map_through_cache(self): map_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1', width='100', height='100', bbox='432890.564641,5872387.45834,466833.867667,5928359.08814', layers='tms_transf', srs='EPSG:25832', format='image/png', styles='', request='GetMap')) expected_reqs = [] with tmp_image((256, 256), format='jpeg') as img: img = img.read() # tms_cache_out has meta_size of [2, 2] but we need larger extent for transformation for tile in [(132, 172, 8), (133, 172, 8), (134, 172, 8), (132, 173, 8), (133, 173, 8), (134, 173, 8), (132, 174, 8), (133, 174, 8), (134, 174, 8)]: expected_reqs.append( ({'path': r'/tiles/%02d/000/000/%03d/000/000/%03d.png' % (tile[2], tile[0], tile[1])}, {'body': img, 'headers': {'content-type': 'image/png'}})) with mock_httpd(('localhost', 42423), expected_reqs, unordered=True): resp = self.app.get(map_req) eq_(resp.content_type, 'image/png') def test_get_tile_through_cache(self): # request tile from tms_transf, # should get tile from tms_source via tms_cache_in/out expected_reqs = [] with tmp_image((256, 256), format='jpeg') as img: for tile in [(8, 11, 4), (8, 10, 4)]: expected_reqs.append( ({'path': r'/tiles/%02d/000/000/%03d/000/000/%03d.png' % (tile[2], tile[0], tile[1])}, {'body': img.read(), 'headers': {'content-type': 'image/png'}})) with mock_httpd(('localhost', 42423), expected_reqs, unordered=True): resp = self.app.get('/tms/1.0.0/tms_transf/EPSG25832/0/0/0.png') eq_(resp.content_type, 'image/png') self.created_tiles.append('tms_cache_out_EPSG25832/00/000/000/000/000/000/000.png') def test_get_tile_from_sub_grid(self): # create tile in old cache tile_filename = os.path.join(self.config['cache_dir'], 'old_cache_EPSG3857/01/000/000/001/000/000/000.png') os.makedirs(os.path.dirname(tile_filename)) # use text to check that mapproxy does not access the tile as image open(tile_filename, 'wb').write(b'foo') # access new cache, should get existing tile from old cache resp = self.app.get('/tiles/new_cache_EPSG3857/0/0/0.png') eq_(resp.content_type, 'image/png') eq_(resp.body, b'foo') self.created_tiles.append('old_cache_EPSG3857/01/000/000/001/000/000/000.png') self.created_tiles.append('new_cache_EPSG3857/00/000/000/000/000/000/000.png') def test_get_tile_combined_cache(self): # request from cache with two cache sources where only one # is compatible (supports tiled_only) expected_reqs = [] with tmp_image((256, 256), format='jpeg') as img: img = img.read() for tile in [ r'/tiles/04/000/000/008/000/000/011.png', r'/tiles/04/000/000/008/000/000/010.png', r'/tiles/utm/00/000/000/000/000/000/000.png', ]: expected_reqs.append( ({'path': tile}, {'body': img, 'headers': {'content-type': 'image/png'}})) with mock_httpd(('localhost', 42423), expected_reqs, unordered=True): resp = self.app.get('/tms/1.0.0/combined/EPSG25832/0/0/0.png') eq_(resp.content_type, 'image/png') mapproxy-1.11.0/mapproxy/test/system/test_combined_sources.py000066400000000000000000000313231320454472400245760ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division from io import BytesIO from mapproxy.request.wms import WMS111MapRequest from mapproxy.compat.image import Image from mapproxy.test.image import is_png, tmp_image, create_tmp_image from mapproxy.test.http import mock_httpd from mapproxy.test.system import module_setup, module_teardown, SystemTest from nose.tools import eq_ test_config = {} def setup_module(): module_setup(test_config, 'combined_sources.yaml') def teardown_module(): module_teardown(test_config) class TestCoverageWMS(SystemTest): config = test_config def setup(self): SystemTest.setup(self) self.common_map_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1', bbox='9,50,10,51', width='200', height='200', layers='combinable', srs='EPSG:4326', format='image/png', styles='', request='GetMap')) def test_combined(self): common_params = (r'?SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=200&SRS=EPSG%3A4326&styles=' '&VERSION=1.1.1&BBOX=9.0,50.0,10.0,51.0' '&WIDTH=200&transparent=True') with tmp_image((200, 200), format='png') as img: img = img.read() expected_req = [({'path': '/service_a' + common_params + '&layers=a_one,a_two,a_three,a_four'}, {'body': img, 'headers': {'content-type': 'image/png'}}), ({'path': '/service_b' + common_params + '&layers=b_one'}, {'body': img, 'headers': {'content-type': 'image/png'}}) ] with mock_httpd(('localhost', 42423), expected_req): self.common_map_req.params.layers = 'combinable' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) def test_uncombined(self): common_params = (r'?SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=200&SRS=EPSG%3A4326&styles=' '&VERSION=1.1.1&BBOX=9.0,50.0,10.0,51.0' '&WIDTH=200&transparent=True') with tmp_image((200, 200), format='png') as img: img = img.read() expected_req = [({'path': '/service_a' + common_params + '&layers=a_one'}, {'body': img, 'headers': {'content-type': 'image/png'}}), ({'path': '/service_b' + common_params + '&layers=b_one'}, {'body': img, 'headers': {'content-type': 'image/png'}}), ({'path': '/service_a' + common_params + '&layers=a_two,a_three'}, {'body': img, 'headers': {'content-type': 'image/png'}}) ] with mock_httpd(('localhost', 42423), expected_req): self.common_map_req.params.layers = 'uncombinable' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) def test_combined_layers(self): common_params = (r'?SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=200&SRS=EPSG%3A4326&styles=' '&VERSION=1.1.1&BBOX=9.0,50.0,10.0,51.0' '&WIDTH=200&transparent=True') with tmp_image((200, 200), format='png') as img: img = img.read() expected_req = [ ({'path': '/service_a' + common_params + '&layers=a_one'}, {'body': img, 'headers': {'content-type': 'image/png'}}), ({'path': '/service_b' + common_params + '&layers=b_one'}, {'body': img, 'headers': {'content-type': 'image/png'}}), ({'path': '/service_a' + common_params + '&layers=a_two,a_three,a_four'}, {'body': img, 'headers': {'content-type': 'image/png'}}), ] with mock_httpd(('localhost', 42423), expected_req): self.common_map_req.params.layers = 'uncombinable,single' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) def test_layers_with_opacity(self): # overlay with opacity -> request should not be combined common_params = (r'?SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=200&SRS=EPSG%3A4326&styles=' '&VERSION=1.1.1&BBOX=9.0,50.0,10.0,51.0' '&WIDTH=200') img_bg = create_tmp_image((200, 200), color=(0, 0, 0)) img_fg = create_tmp_image((200, 200), color=(255, 0, 128)) expected_req = [ ({'path': '/service_a' + common_params + '&layers=a_one'}, {'body': img_bg, 'headers': {'content-type': 'image/png'}}), ({'path': '/service_a' + common_params + '&layers=a_two'}, {'body': img_fg, 'headers': {'content-type': 'image/png'}}), ] with mock_httpd(('localhost', 42423), expected_req): self.common_map_req.params.layers = 'opacity_base,opacity_overlay' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) img = Image.open(data) eq_(img.getcolors()[0], ((200*200),(127, 0, 64))) def test_combined_transp_color(self): # merged to one request because both layers share the same transparent_color common_params = (r'?SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=200&SRS=EPSG%3A4326&styles=' '&VERSION=1.1.1&BBOX=9.0,50.0,10.0,51.0' '&WIDTH=200&transparent=True') with tmp_image((200, 200), color=(255, 0, 0), format='png') as img: img = img.read() expected_req = [({'path': '/service_a' + common_params + '&layers=a_iopts_one,a_iopts_two'}, {'body': img, 'headers': {'content-type': 'image/png'}}), ] with mock_httpd(('localhost', 42423), expected_req): self.common_map_req.params.layers = 'layer_image_opts1,layer_image_opts2' self.common_map_req.params.transparent = True resp = self.app.get(self.common_map_req) resp.content_type = 'image/png' data = BytesIO(resp.body) assert is_png(data) img = Image.open(data) eq_(img.getcolors()[0], ((200*200),(255, 0, 0, 0))) def test_combined_mixed_transp_color(self): # not merged to one request because only one layer has transparent_color common_params = (r'?SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=200&SRS=EPSG%3A4326&styles=' '&VERSION=1.1.1&BBOX=9.0,50.0,10.0,51.0' '&WIDTH=200&transparent=True') with tmp_image((200, 200), color=(255, 0, 0), format='png') as img: img = img.read() expected_req = [({'path': '/service_a' + common_params + '&layers=a_four'}, {'body': img, 'headers': {'content-type': 'image/png'}}), ({'path': '/service_a' + common_params + '&layers=a_iopts_one'}, {'body': img, 'headers': {'content-type': 'image/png'}}), ] with mock_httpd(('localhost', 42423), expected_req): self.common_map_req.params.layers = 'single,layer_image_opts1' self.common_map_req.params.transparent = True resp = self.app.get(self.common_map_req) resp.content_type = 'image/png' data = BytesIO(resp.body) assert is_png(data) def test_combined_same_fwd_req_params(self): # merged to one request because all layers share the same time param in # fwd_req_params config with tmp_image((200, 200), format='png') as img: img = img.read() expected_req = [({'path': '/service_a?SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=200&SRS=EPSG%3A4326&styles=' '&VERSION=1.1.1&BBOX=9.0,50.0,10.0,51.0' '&WIDTH=200&transparent=True&TIME=20041012' '&layers=a_one,a_two,a_three,a_four'}, {'body': img, 'headers': {'content-type': 'image/png'}}), ] with mock_httpd(('localhost', 42423), expected_req): self.common_map_req.params.layers = 'layer_fwdparams1,layer_fwdparams2' self.common_map_req.params['time'] = '20041012' self.common_map_req.params.transparent = True resp = self.app.get(self.common_map_req) resp.content_type = 'image/png' data = BytesIO(resp.body) assert is_png(data) def test_combined_no_fwd_req_params(self): # merged to one request because no vendor param is set with tmp_image((200, 200), format='png') as img: img = img.read() expected_req = [({'path': '/service_a?SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=200&SRS=EPSG%3A4326&styles=' '&VERSION=1.1.1&BBOX=9.0,50.0,10.0,51.0' '&WIDTH=200&transparent=True' '&layers=a_one,a_two,a_four'}, {'body': img, 'headers': {'content-type': 'image/png'}}), ] with mock_httpd(('localhost', 42423), expected_req): self.common_map_req.params.layers = 'layer_fwdparams1,single' self.common_map_req.params.transparent = True resp = self.app.get(self.common_map_req) resp.content_type = 'image/png' data = BytesIO(resp.body) assert is_png(data) def test_combined_mixed_fwd_req_params(self): # not merged to one request because fwd_req_params are different common_params = (r'/service_a?SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=200&SRS=EPSG%3A4326&styles=' '&VERSION=1.1.1&BBOX=9.0,50.0,10.0,51.0' '&WIDTH=200&transparent=True') with tmp_image((200, 200), format='png') as img: img = img.read() expected_req = [({'path': common_params + '&layers=a_one&TIME=20041012'}, {'body': img, 'headers': {'content-type': 'image/png'}}), ({'path': common_params + '&layers=a_two&TIME=20041012&VENDOR=foo'}, {'body': img, 'headers': {'content-type': 'image/png'}}), ({'path': common_params + '&layers=a_four'}, {'body': img, 'headers': {'content-type': 'image/png'}}), ] with mock_httpd(('localhost', 42423), expected_req): self.common_map_req.params.layers = 'layer_fwdparams1,single' self.common_map_req.params['time'] = '20041012' self.common_map_req.params['vendor'] = 'foo' self.common_map_req.params.transparent = True resp = self.app.get(self.common_map_req) resp.content_type = 'image/png' data = BytesIO(resp.body) assert is_png(data) mapproxy-1.11.0/mapproxy/test/system/test_coverage.py000066400000000000000000000126051320454472400230500ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division from io import BytesIO from mapproxy.request.wms import WMS111MapRequest from mapproxy.compat.image import Image from mapproxy.test.image import is_png, tmp_image from mapproxy.test.http import mock_httpd from mapproxy.test.system import module_setup, module_teardown, SystemTest from nose.tools import eq_ test_config = {} def setup_module(): module_setup(test_config, 'coverage.yaml') def teardown_module(): module_teardown(test_config) class TestCoverageWMS(SystemTest): config = test_config def setup(self): SystemTest.setup(self) self.common_map_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1', bbox='-180,0,0,80', width='200', height='200', layers='wms_cache', srs='EPSG:4326', format='image/png', styles='', request='GetMap')) def test_capababilities(self): resp = self.app.get('/service?request=GetCapabilities&service=WMS&version=1.1.1') xml = resp.lxml # First: combined root, second: wms_cache, third: tms_cache, last: seed_only eq_(xml.xpath('//LatLonBoundingBox/@minx'), ['10', '10', '12', '14']) eq_(xml.xpath('//LatLonBoundingBox/@miny'), ['10', '15', '10', '13']) eq_(xml.xpath('//LatLonBoundingBox/@maxx'), ['35', '30', '35', '24']) eq_(xml.xpath('//LatLonBoundingBox/@maxy'), ['31', '31', '30', '23']) def test_get_map_outside(self): self.common_map_req.params.bbox = -90, 0, 0, 90 self.common_map_req.params['bgcolor'] = '0xff0005' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) img = Image.open(data) eq_(img.mode, 'RGB') eq_(img.getcolors(), [(200*200, (255, 0, 5))]) def test_get_map_outside_transparent(self): self.common_map_req.params.bbox = -90, 0, 0, 90 self.common_map_req.params.transparent = True resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) img = Image.open(data) eq_(img.mode, 'RGBA') eq_(img.getcolors()[0][0], 200*200) eq_(img.getcolors()[0][1][3], 0) # transparent def test_get_map_intersection(self): self.created_tiles.append('wms_cache_EPSG4326/03/000/000/004/000/000/002.jpeg') with tmp_image((256, 256), format='jpeg') as img: expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fjpeg' '&REQUEST=GetMap&HEIGHT=91&SRS=EPSG%3A4326&styles=' '&VERSION=1.1.1&BBOX=10,15,30,31' '&WIDTH=114'}, {'body': img.read(), 'headers': {'content-type': 'image/jpeg'}}) with mock_httpd(('localhost', 42423), [expected_req]): self.common_map_req.params.bbox = 0, 0, 40, 40 self.common_map_req.params.transparent = True resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) eq_(Image.open(data).mode, 'RGBA') class TestCoverageTMS(SystemTest): config = test_config def test_get_tile_intersections(self): with tmp_image((256, 256), format='jpeg') as img: expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fjpeg' '&REQUEST=GetMap&HEIGHT=25&SRS=EPSG%3A900913&styles=' '&VERSION=1.1.1&BBOX=1113194.90793,1689200.13961,3339584.7238,3632749.14338' '&WIDTH=28'}, {'body': img.read(), 'headers': {'content-type': 'image/jpeg'}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): resp = self.app.get('/tms/1.0.0/wms_cache/0/1/1.jpeg') eq_(resp.content_type, 'image/jpeg') self.created_tiles.append('wms_cache_EPSG900913/01/000/000/001/000/000/001.jpeg') def test_get_tile_intersection_tms(self): with tmp_image((256, 256), format='jpeg') as img: expected_req = ({'path': r'/tms/1.0.0/foo/1/1/1.jpeg'}, {'body': img.read(), 'headers': {'content-type': 'image/jpeg'}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): resp = self.app.get('/tms/1.0.0/tms_cache/0/1/1.jpeg') eq_(resp.content_type, 'image/jpeg') self.created_tiles.append('tms_cache_EPSG900913/01/000/000/001/000/000/001.jpeg') mapproxy-1.11.0/mapproxy/test/system/test_decorate_img.py000066400000000000000000000151211320454472400236730ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.test.system import module_setup, module_teardown, SystemTest from mapproxy.test.system import make_base_config from mapproxy.test.image import is_png, is_jpeg from mapproxy.request.wms import WMS111MapRequest from mapproxy.request.wmts import WMTS100TileRequest from io import BytesIO from mapproxy.compat.image import Image from mapproxy.image import ImageSource from nose.tools import eq_ test_config = {} base_config = make_base_config(test_config) def setup_module(): module_setup(test_config, 'layer.yaml', with_cache_data=True) def teardown_module(): module_teardown(test_config) def to_greyscale(image, service, layers, **kw): img = image.as_image() if (hasattr(image.image_opts, 'transparent') and image.image_opts.transparent): img = img.convert('LA').convert('RGBA') else: img = img.convert('L').convert('RGB') return ImageSource(img, image.image_opts) class TestDecorateImg(SystemTest): config = test_config def setup(self): SystemTest.setup(self) self.common_tile_req = WMTS100TileRequest(url='/service?', param=dict(service='WMTS', version='1.0.0', tilerow='0', tilecol='0', tilematrix='01', tilematrixset='GLOBAL_MERCATOR', layer='wms_cache', format='image/jpeg', style='', request='GetTile')) def test_wms(self): req = WMS111MapRequest( url='/service?', param=dict( service='WMS', version='1.1.1', bbox='-180,0,0,80', width='200', height='200', layers='wms_cache', srs='EPSG:4326', format='image/png', styles='', request='GetMap' ) ) resp = self.app.get( req, extra_environ={'mapproxy.decorate_img': to_greyscale} ) data = BytesIO(resp.body) assert is_png(data) img = Image.open(data) eq_(img.mode, 'RGB') def test_wms_transparent(self): req = WMS111MapRequest( url='/service?', param=dict( service='WMS', version='1.1.1', bbox='-180,0,0,80', width='200', height='200', layers='wms_cache_transparent', srs='EPSG:4326', format='image/png', styles='', request='GetMap', transparent='True' ) ) resp = self.app.get( req, extra_environ={'mapproxy.decorate_img': to_greyscale} ) data = BytesIO(resp.body) assert is_png(data) img = Image.open(data) eq_(img.mode, 'RGBA') def test_wms_bgcolor(self): req = WMS111MapRequest( url='/service?', param=dict( service='WMS', version='1.1.1', bbox='-180,0,0,80', width='200', height='200', layers='wms_cache_transparent', srs='EPSG:4326', format='image/png', styles='', request='GetMap', bgcolor='0xff00a0' ) ) resp = self.app.get( req, extra_environ={'mapproxy.decorate_img': to_greyscale} ) data = BytesIO(resp.body) assert is_png(data) img = Image.open(data) eq_(img.mode, 'RGB') eq_(sorted(img.getcolors())[-1][1], (94, 94, 94)) def test_wms_args(self): req = WMS111MapRequest( url='/service?', param=dict( service='WMS', version='1.1.1', bbox='-180,0,0,80', width='200', height='200', layers='wms_cache,wms_cache_transparent', srs='EPSG:4326', format='image/png', styles='', request='GetMap', transparent='True' ) ) def callback(img_src, service, layers, environ, query_extent): assert isinstance(img_src, ImageSource) eq_('wms.map', service) eq_(len(layers), 2) assert 'wms_cache_transparent' in layers assert 'wms_cache' in layers assert isinstance(environ, dict) assert len(query_extent) == 2 assert len(query_extent[1]) == 4 eq_(query_extent[0], 'EPSG:4326') return img_src resp = self.app.get( req, extra_environ={'mapproxy.decorate_img': callback} ) def test_tms(self): resp = self.app.get( '/tms/1.0.0/wms_cache/0/0/1.jpeg', extra_environ={'mapproxy.decorate_img': to_greyscale} ) eq_(resp.content_type, 'image/jpeg') eq_(resp.content_length, len(resp.body)) data = BytesIO(resp.body) assert is_jpeg(data) def test_tms_args(self): def callback(img_src, service, layers, environ, query_extent): assert isinstance(img_src, ImageSource) eq_('tms', service) eq_('wms_cache', layers[0]) assert isinstance(environ, dict) assert len(query_extent) == 2 assert len(query_extent[1]) == 4 eq_(query_extent[0], 'EPSG:900913') return img_src resp = self.app.get( '/tms/1.0.0/wms_cache/0/0/1.jpeg', extra_environ={'mapproxy.decorate_img': callback} ) def test_wmts(self): resp = self.app.get( str(self.common_tile_req), extra_environ={'mapproxy.decorate_img': to_greyscale} ) eq_(resp.content_type, 'image/jpeg') eq_(resp.content_length, len(resp.body)) data = BytesIO(resp.body) assert is_jpeg(data) def test_wmts_args(self): def callback(img_src, service, layers, environ, query_extent): assert isinstance(img_src, ImageSource) eq_('wmts', service) eq_('wms_cache', layers[0]) assert isinstance(environ, dict) assert len(query_extent) == 2 assert len(query_extent[1]) == 4 eq_(query_extent[0], 'EPSG:900913') return img_src resp = self.app.get( str(self.common_tile_req), extra_environ={'mapproxy.decorate_img': callback} ) mapproxy-1.11.0/mapproxy/test/system/test_disable_storage.py000066400000000000000000000041071320454472400244020ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os from mapproxy.test.image import is_png, tmp_image from mapproxy.test.http import mock_httpd from mapproxy.test.system import module_setup, module_teardown, SystemTest, make_base_config from nose.tools import eq_ test_config = {} base_config = make_base_config(test_config) def setup_module(): module_setup(test_config, 'disable_storage.yaml', with_cache_data=False) def teardown_module(): module_teardown(test_config) class TestDisableStorage(SystemTest): config = test_config def test_get_tile_without_caching(self): with tmp_image((256, 256), format='png') as img: expected_req = ({'path': r'/tile.png'}, {'body': img.read(), 'headers': {'content-type': 'image/png'}}) with mock_httpd(('localhost', 42423), [expected_req]): resp = self.app.get('/tms/1.0.0/tiles/0/0/0.png') eq_(resp.content_type, 'image/png') is_png(resp.body) assert not os.path.exists(test_config['cache_dir']) with tmp_image((256, 256), format='png') as img: expected_req = ({'path': r'/tile.png'}, {'body': img.read(), 'headers': {'content-type': 'image/png'}}) with mock_httpd(('localhost', 42423), [expected_req]): resp = self.app.get('/tms/1.0.0/tiles/0/0/0.png') eq_(resp.content_type, 'image/png') is_png(resp.body) mapproxy-1.11.0/mapproxy/test/system/test_formats.py000066400000000000000000000210551320454472400227270ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import os from io import BytesIO from mapproxy.request.wms import WMS111MapRequest, WMS111FeatureInfoRequest from mapproxy.test.image import tmp_image, check_format from mapproxy.test.http import mock_httpd from mapproxy.test.system import module_setup, module_teardown, SystemTest, make_base_config from nose.tools import eq_ test_config = {} base_config = make_base_config(test_config) def setup_module(): module_setup(test_config, 'formats.yaml', with_cache_data=True) def teardown_module(): module_teardown(test_config) class TilesTest(SystemTest): config = test_config def created_tiles_filenames(self): base_dir = base_config().cache.base_dir for filename, format in self.created_tiles: yield os.path.join(base_dir, filename), format def _test_created_tiles(self): for filename, format in self.created_tiles_filenames(): if not os.path.exists(filename): assert False, "didn't found tile " + filename else: check_format(open(filename, 'rb'), format) def teardown(self): self._test_created_tiles() for filename, _format in self.created_tiles_filenames(): if os.path.exists(filename): os.remove(filename) class TestWMS111(TilesTest): def setup(self): TilesTest.setup(self) self.common_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1')) self.common_map_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1', bbox='0,0,180,80', width='200', height='200', layers='wms_cache', srs='EPSG:4326', format='image/png', styles='', request='GetMap')) self.common_direct_map_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1', bbox='0,0,10,10', width='200', height='200', layers='wms_cache', srs='EPSG:4326', format='image/png', styles='', request='GetMap')) self.common_fi_req = WMS111FeatureInfoRequest(url='/service?', param=dict(x='10', y='20', width='200', height='200', layers='wms_cache', format='image/png', query_layers='wms_cache', styles='', bbox='1000,400,2000,1400', srs='EPSG:900913')) self.expected_base_path = '/service?SERVICE=WMS&REQUEST=GetMap&HEIGHT=256' \ '&SRS=EPSG%3A900913&styles=&VERSION=1.1.1&WIDTH=256' \ '&BBOX=0.0,0.0,20037508.3428,20037508.3428' self.expected_direct_base_path = '/service?SERVICE=WMS&REQUEST=GetMap&HEIGHT=200' \ '&SRS=EPSG%3A4326&styles=&VERSION=1.1.1&WIDTH=200' \ '&BBOX=0.0,0.0,10.0,10.0' def test_cache_formats(self): yield self.check_get_cached, 'jpeg_cache_tiff_source', 'tiffsource', 'png', 'jpeg', 'tiff' yield self.check_get_cached, 'jpeg_cache_tiff_source', 'tiffsource', 'jpeg', 'jpeg', 'tiff' yield self.check_get_cached, 'jpeg_cache_tiff_source', 'tiffsource', 'tiff', 'jpeg', 'tiff' yield self.check_get_cached, 'jpeg_cache_tiff_source', 'tiffsource', 'gif', 'jpeg', 'tiff' yield self.check_get_cached, 'png_cache_all_source', 'allsource', 'png', 'png', 'png' yield self.check_get_cached, 'png_cache_all_source', 'allsource', 'jpeg', 'png', 'png' yield self.check_get_cached, 'jpeg_cache_png_jpeg_source', 'pngjpegsource', 'jpeg', 'jpeg', 'jpeg' yield self.check_get_cached, 'jpeg_cache_png_jpeg_source', 'pngjpegsource', 'png', 'jpeg', 'jpeg' def test_direct_formats(self): yield self.check_get_direct, 'jpeg_cache_tiff_source', 'tiffsource', 'gif', 'tiff' yield self.check_get_direct, 'jpeg_cache_tiff_source', 'tiffsource', 'jpeg', 'tiff' yield self.check_get_direct, 'jpeg_cache_tiff_source', 'tiffsource', 'png', 'tiff' yield self.check_get_direct, 'png_cache_all_source', 'allsource', 'gif', 'gif' yield self.check_get_direct, 'png_cache_all_source', 'allsource', 'png', 'png' yield self.check_get_direct, 'png_cache_all_source', 'allsource', 'tiff', 'tiff' yield self.check_get_direct, 'jpeg_cache_png_jpeg_source', 'pngjpegsource', 'jpeg', 'jpeg' yield self.check_get_direct, 'jpeg_cache_png_jpeg_source', 'pngjpegsource', 'png', 'png' yield self.check_get_direct, 'jpeg_cache_png_jpeg_source', 'pngjpegsource', 'tiff', 'png' yield self.check_get_direct, 'jpeg_cache_png_jpeg_source', 'pngjpegsource', 'gif', 'png' def check_get_cached(self, layer, source, wms_format, cache_format, req_format): self.created_tiles.append((layer+'_EPSG900913/01/000/000/001/000/000/001.'+cache_format, cache_format)) with tmp_image((256, 256), format=req_format) as img: expected_req = ({'path': self.expected_base_path + '&layers=' + source + '&format=image%2F' + req_format}, {'body': img.read(), 'headers': {'content-type': 'image/'+req_format}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): self.common_map_req.params['layers'] = layer self.common_map_req.params['format'] = 'image/'+wms_format resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/'+wms_format) check_format(BytesIO(resp.body), wms_format) def check_get_direct(self, layer, source, wms_format, req_format): with tmp_image((256, 256), format=req_format) as img: expected_req = ({'path': self.expected_direct_base_path + '&layers=' + source + '&format=image%2F' + req_format}, {'body': img.read(), 'headers': {'content-type': 'image/'+req_format}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): self.common_direct_map_req.params['layers'] = layer self.common_direct_map_req.params['format'] = 'image/'+wms_format resp = self.app.get(self.common_direct_map_req) eq_(resp.content_type, 'image/'+wms_format) check_format(BytesIO(resp.body), wms_format) class TestTMS(TilesTest): def setup(self): TilesTest.setup(self) self.expected_base_path = '/service?SERVICE=WMS&REQUEST=GetMap&HEIGHT=256' \ '&SRS=EPSG%3A900913&styles=&VERSION=1.1.1&WIDTH=256' \ '&BBOX=0.0,0.0,20037508.3428,20037508.3428' self.expected_direct_base_path = '/service?SERVICE=WMS&REQUEST=GetMap&HEIGHT=200' \ '&SRS=EPSG%3A4326&styles=&VERSION=1.1.1&WIDTH=200' \ '&BBOX=0.0,0.0,10.0,10.0' def test_cache_formats(self): yield self.check_get_cached, 'jpeg_cache_tiff_source', 'tiffsource', 'jpeg', 'jpeg', 'tiff' yield self.check_get_cached, 'png_cache_all_source', 'allsource', 'png', 'png', 'png' yield self.check_get_cached, 'jpeg_cache_png_jpeg_source', 'pngjpegsource', 'jpeg', 'jpeg', 'jpeg' def check_get_cached(self, layer, source, tms_format, cache_format, req_format): self.created_tiles.append((layer+'_EPSG900913/01/000/000/001/000/000/001.'+cache_format, cache_format)) with tmp_image((256, 256), format=req_format) as img: expected_req = ({'path': self.expected_base_path + '&layers=' + source + '&format=image%2F' + req_format}, {'body': img.read(), 'headers': {'content-type': 'image/'+req_format}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): resp = self.app.get('/tms/1.0.0/%s/0/1/1.%s' % (layer, tms_format)) eq_(resp.content_type, 'image/'+tms_format) # check_format(BytesIO(resp.body), tms_format) mapproxy-1.11.0/mapproxy/test/system/test_inspire_vs.py000066400000000000000000000123061320454472400234340ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2015 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function, division import functools from mapproxy.request.wms import WMS130CapabilitiesRequest from mapproxy.test.helper import validate_with_xsd from nose.tools import eq_ from mapproxy.test.system import module_setup, module_teardown, SystemTest, make_base_config test_config = {} test_linked_config = {} base_config = make_base_config(test_config) def setup_module(): module_setup(test_linked_config, 'inspire.yaml', with_cache_data=True) module_setup(test_config, 'inspire_full.yaml', with_cache_data=True) def teardown_module(): module_teardown(test_linked_config) module_teardown(test_config) def is_inpire_vs_capa(xml): return validate_with_xsd(xml, xsd_name='inspire/inspire_vs/1.0/inspire_vs.xsd') def bbox_srs_from_boundingbox(bbox_elem): return [ float(bbox_elem.attrib['minx']), float(bbox_elem.attrib['miny']), float(bbox_elem.attrib['maxx']), float(bbox_elem.attrib['maxy']), ] ns130 = {'wms': 'http://www.opengis.net/wms', 'ogc': 'http://www.opengis.net/ogc', 'sld': 'http://www.opengis.net/sld', 'xlink': 'http://www.w3.org/1999/xlink', 'ic': 'http://inspire.ec.europa.eu/schemas/common/1.0', 'iv': 'http://inspire.ec.europa.eu/schemas/inspire_vs/1.0', } def eq_xpath(xml, xpath, expected, namespaces=None): elems = xml.xpath(xpath, namespaces=namespaces) assert len(elems) == 1, elems eq_(elems[0], expected) def xpath_130(xml, xpath): return xml.xpath(xpath, namespaces=ns130) eq_xpath_wms130 = functools.partial(eq_xpath, namespaces=ns130) class TestLinkedMD(SystemTest): config = test_linked_config def setup(self): SystemTest.setup(self) def test_wms_capabilities(self): req = WMS130CapabilitiesRequest(url='/service?') resp = self.app.get(req) eq_(resp.content_type, 'text/xml') print(resp.body) xml = resp.lxml assert is_inpire_vs_capa(xml) ext_cap =xpath_130(xml, '/wms:WMS_Capabilities/wms:Capability/iv:ExtendedCapabilities') assert len(ext_cap) == 1, ext_cap ext_cap = ext_cap[0] eq_xpath_wms130(ext_cap, './ic:MetadataUrl/ic:URL/text()', u'http://example.org/metadata') eq_xpath_wms130(ext_cap, './ic:MetadataUrl/ic:MediaType/text()', u'application/vnd.iso.19139+xml') eq_xpath_wms130(ext_cap, './ic:SupportedLanguages/ic:DefaultLanguage/ic:Language/text()', u'eng') eq_xpath_wms130(ext_cap, './ic:ResponseLanguage/ic:Language/text()', u'eng') # test for extended layer metadata eq_xpath_wms130(xml, '/wms:WMS_Capabilities/wms:Capability/wms:Layer/wms:Attribution/wms:Title/text()', u'My attribution title') layer_names = set(xml.xpath('//wms:Layer/wms:Name/text()', namespaces=ns130)) expected_names = set(['inspire_example']) eq_(layer_names, expected_names) class TestFullMD(SystemTest): config = test_config def setup(self): SystemTest.setup(self) def test_wms_capabilities(self): req = WMS130CapabilitiesRequest(url='/service?') resp = self.app.get(req) eq_(resp.content_type, 'text/xml') print(resp.body) xml = resp.lxml assert is_inpire_vs_capa(xml) ext_cap =xpath_130(xml, '/wms:WMS_Capabilities/wms:Capability/iv:ExtendedCapabilities') assert len(ext_cap) == 1, ext_cap ext_cap = ext_cap[0] eq_xpath_wms130(ext_cap, './ic:ResourceLocator/ic:URL/text()', u'http://example.org/metadata') eq_xpath_wms130(ext_cap, './ic:ResourceLocator/ic:MediaType/text()', u'application/vnd.iso.19139+xml') eq_xpath_wms130(ext_cap, './ic:Keyword/ic:OriginatingControlledVocabulary/ic:Title/text()', u'GEMET - INSPIRE themes') eq_xpath_wms130(ext_cap, './ic:SupportedLanguages/ic:DefaultLanguage/ic:Language/text()', u'eng') eq_xpath_wms130(ext_cap, './ic:ResponseLanguage/ic:Language/text()', u'eng') # check dates from string and datetime eq_xpath_wms130(ext_cap, './ic:TemporalReference/ic:DateOfCreation/text()', u'2015-05-01') eq_xpath_wms130(ext_cap, './ic:MetadataDate/text()', u'2015-07-23') # test for extended layer metadata eq_xpath_wms130(xml, '/wms:WMS_Capabilities/wms:Capability/wms:Layer/wms:Attribution/wms:Title/text()', u'My attribution title') layer_names = set(xml.xpath('//wms:Layer/wms:Name/text()', namespaces=ns130)) expected_names = set(['inspire_example']) eq_(layer_names, expected_names) mapproxy-1.11.0/mapproxy/test/system/test_kml.py000066400000000000000000000244031320454472400220370ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010-2012 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import hashlib from io import BytesIO from mapproxy.srs import bbox_equals from mapproxy.util.times import format_httpdate from mapproxy.test.image import is_jpeg, tmp_image from mapproxy.test.http import mock_httpd from mapproxy.test.helper import validate_with_xsd from nose.tools import eq_ ns = {'kml': 'http://www.opengis.net/kml/2.2'} from mapproxy.test.system import module_setup, module_teardown, SystemTest, make_base_config test_config = {} base_config = make_base_config(test_config) def setup_module(): module_setup(test_config, 'kml_layer.yaml', with_cache_data=True) def teardown_module(): module_teardown(test_config) class TestKML(SystemTest): config = test_config def test_get_out_of_bounds_tile(self): for coord in [(0, 0, -1), (-1, 0, 0), (0, -1, 0), (4, 2, 1), (1, 3, 0)]: yield self.check_out_of_bounds, coord def check_out_of_bounds(self, coord): x, y, z = coord url = '/kml/wms_cache/%d/%d/%d.kml' % (z, x, y) resp = self.app.get(url , status=404) assert 'outside the bounding box' in resp def test_invalid_layer(self): resp = self.app.get('/kml/inVAlid/0/0/0.png', status=404) eq_(resp.content_type, 'text/plain') assert 'unknown layer: inVAlid' in resp def test_invalid_format(self): resp = self.app.get('/kml/wms_cache/0/0/1.png', status=404) eq_(resp.content_type, 'text/plain') assert 'invalid format' in resp def test_get_tile_tile_source_error(self): resp = self.app.get('/kml/wms_cache/0/0/0.jpeg', status=500) eq_(resp.content_type, 'text/plain') assert 'No response from URL' in resp def _check_tile_resp(self, resp): eq_(resp.content_type, 'image/jpeg') eq_(resp.content_length, len(resp.body)) data = BytesIO(resp.body) assert is_jpeg(data) def _update_timestamp(self): timestamp = 1234567890.0 size = 10214 base_dir = base_config().cache.base_dir os.utime(os.path.join(base_dir, 'wms_cache_EPSG900913/01/000/000/000/000/000/001.jpeg'), (timestamp, timestamp)) max_age = base_config().tiles.expires_hours * 60 * 60 etag = hashlib.md5((str(timestamp) + str(size)).encode('ascii')).hexdigest() return etag, max_age def _check_cache_control_headers(self, resp, etag, max_age, timestamp=1234567890.0): eq_(resp.headers['ETag'], etag) if timestamp is None: assert 'Last-modified' not in resp.headers else: eq_(resp.headers['Last-modified'], format_httpdate(timestamp)) eq_(resp.headers['Cache-control'], 'public, max-age=%d, s-maxage=%d' % (max_age, max_age)) def test_get_cached_tile(self): etag, max_age = self._update_timestamp() resp = self.app.get('/kml/wms_cache/1/0/1.jpeg') self._check_cache_control_headers(resp, etag, max_age) self._check_tile_resp(resp) def test_if_none_match(self): etag, max_age = self._update_timestamp() resp = self.app.get('/kml/wms_cache/1/0/1.jpeg', headers={'If-None-Match': etag}) eq_(resp.status, '304 Not Modified') self._check_cache_control_headers(resp, etag, max_age) resp = self.app.get('/kml/wms_cache/1/0/1.jpeg', headers={'If-None-Match': etag + 'foo'}) self._check_cache_control_headers(resp, etag, max_age) eq_(resp.status, '200 OK') self._check_tile_resp(resp) def test_get_kml(self): resp = self.app.get('/kml/wms_cache/0/0/0.kml') xml = resp.lxml assert validate_with_xsd(xml, 'kml/2.2.0/ogckml22.xsd') assert bbox_equals( self._bbox(xml.xpath('/kml:kml/kml:Document', namespaces=ns)[0]), (-180, -90, 180, 90) ) assert bbox_equals( self._bbox(xml.xpath('/kml:kml/kml:Document/kml:GroundOverlay', namespaces=ns)[0]), (-180, 0, 0, 90) ) eq_(xml.xpath('/kml:kml/kml:Document/kml:GroundOverlay/kml:Icon/kml:href/text()', namespaces=ns), ['http://localhost/kml/wms_cache/EPSG900913/1/0/1.jpeg', 'http://localhost/kml/wms_cache/EPSG900913/1/1/1.jpeg', 'http://localhost/kml/wms_cache/EPSG900913/1/0/0.jpeg', 'http://localhost/kml/wms_cache/EPSG900913/1/1/0.jpeg'] ) eq_(xml.xpath('/kml:kml/kml:Document/kml:NetworkLink/kml:Link/kml:href/text()', namespaces=ns), ['http://localhost/kml/wms_cache/EPSG900913/1/0/1.kml', 'http://localhost/kml/wms_cache/EPSG900913/1/1/1.kml', 'http://localhost/kml/wms_cache/EPSG900913/1/0/0.kml', 'http://localhost/kml/wms_cache/EPSG900913/1/1/0.kml'] ) etag = hashlib.md5(resp.body).hexdigest() max_age = base_config().tiles.expires_hours * 60 * 60 self._check_cache_control_headers(resp, etag, max_age, None) resp = self.app.get('/kml/wms_cache/0/0/0.kml', headers={'If-None-Match': etag}) eq_(resp.status, '304 Not Modified') def test_get_kml_init(self): resp = self.app.get('/kml/wms_cache') xml = resp.lxml assert validate_with_xsd(xml, 'kml/2.2.0/ogckml22.xsd') eq_(xml.xpath('/kml:kml/kml:Document/kml:GroundOverlay/kml:Icon/kml:href/text()', namespaces=ns), ['http://localhost/kml/wms_cache/EPSG900913/1/0/1.jpeg', 'http://localhost/kml/wms_cache/EPSG900913/1/1/1.jpeg', 'http://localhost/kml/wms_cache/EPSG900913/1/0/0.jpeg', 'http://localhost/kml/wms_cache/EPSG900913/1/1/0.jpeg'] ) eq_(xml.xpath('/kml:kml/kml:Document/kml:NetworkLink/kml:Link/kml:href/text()', namespaces=ns), ['http://localhost/kml/wms_cache/EPSG900913/1/0/1.kml', 'http://localhost/kml/wms_cache/EPSG900913/1/1/1.kml', 'http://localhost/kml/wms_cache/EPSG900913/1/0/0.kml', 'http://localhost/kml/wms_cache/EPSG900913/1/1/0.kml'] ) def test_get_kml_nw(self): resp = self.app.get('/kml/wms_cache_nw/1/0/0.kml') xml = resp.lxml assert validate_with_xsd(xml, 'kml/2.2.0/ogckml22.xsd') assert bbox_equals( self._bbox(xml.xpath('/kml:kml/kml:Document', namespaces=ns)[0]), (-180, -90, 0, 0) ) assert bbox_equals( self._bbox(xml.xpath('/kml:kml/kml:Document/kml:GroundOverlay', namespaces=ns)[0]), (-180, -66.51326, -90, 0) ) eq_(xml.xpath('/kml:kml/kml:Document/kml:GroundOverlay/kml:Icon/kml:href/text()', namespaces=ns), ['http://localhost/kml/wms_cache_nw/EPSG900913/2/0/1.jpeg', 'http://localhost/kml/wms_cache_nw/EPSG900913/2/1/1.jpeg', 'http://localhost/kml/wms_cache_nw/EPSG900913/2/0/0.jpeg', 'http://localhost/kml/wms_cache_nw/EPSG900913/2/1/0.jpeg'] ) eq_(xml.xpath('/kml:kml/kml:Document/kml:NetworkLink/kml:Link/kml:href/text()', namespaces=ns), ['http://localhost/kml/wms_cache_nw/EPSG900913/2/0/1.kml', 'http://localhost/kml/wms_cache_nw/EPSG900913/2/1/1.kml', 'http://localhost/kml/wms_cache_nw/EPSG900913/2/0/0.kml', 'http://localhost/kml/wms_cache_nw/EPSG900913/2/1/0.kml'] ) def test_get_kml2(self): resp = self.app.get('/kml/wms_cache/1/0/1.kml') xml = resp.lxml assert validate_with_xsd(xml, 'kml/2.2.0/ogckml22.xsd') def test_get_kml_multi_layer(self): resp = self.app.get('/kml/wms_cache_multi/1/0/0.kml') xml = resp.lxml assert validate_with_xsd(xml, 'kml/2.2.0/ogckml22.xsd') eq_(xml.xpath('/kml:kml/kml:Document/kml:GroundOverlay/kml:Icon/kml:href/text()', namespaces=ns), ['http://localhost/kml/wms_cache_multi/EPSG4326/2/0/1.jpeg', 'http://localhost/kml/wms_cache_multi/EPSG4326/2/1/1.jpeg', 'http://localhost/kml/wms_cache_multi/EPSG4326/2/0/0.jpeg', 'http://localhost/kml/wms_cache_multi/EPSG4326/2/1/0.jpeg'] ) eq_(xml.xpath('/kml:kml/kml:Document/kml:NetworkLink/kml:Link/kml:href/text()', namespaces=ns), ['http://localhost/kml/wms_cache_multi/EPSG4326/2/0/1.kml', 'http://localhost/kml/wms_cache_multi/EPSG4326/2/1/1.kml', 'http://localhost/kml/wms_cache_multi/EPSG4326/2/0/0.kml', 'http://localhost/kml/wms_cache_multi/EPSG4326/2/1/0.kml'] ) def test_get_tile(self): with tmp_image((256, 256), format='jpeg') as img: expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fjpeg' '&REQUEST=GetMap&HEIGHT=256&SRS=EPSG%3A900913&styles=' '&VERSION=1.1.1&BBOX=-20037508.3428,-20037508.3428,0.0,0.0' '&WIDTH=256'}, {'body': img.read(), 'headers': {'content-type': 'image/jpeg'}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): resp = self.app.get('/kml/wms_cache/1/0/0.jpeg') eq_(resp.content_type, 'image/jpeg') self.created_tiles.append('wms_cache_EPSG900913/01/000/000/000/000/000/000.jpeg') def _bbox(self, elem): elems = elem.xpath('kml:Region/kml:LatLonAltBox', namespaces=ns)[0] n, s, e, w = [float(elem.text) for elem in elems.getchildren()] return w, s, e, n mapproxy-1.11.0/mapproxy/test/system/test_layergroups.py000066400000000000000000000131301320454472400236230ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division from mapproxy.test.system import module_setup, module_teardown, SystemTest from mapproxy.test.system.test_wms import is_111_capa, is_110_capa, is_100_capa, is_130_capa, ns130 from nose.tools import eq_ test_config = {} test_config_with_root = {} def setup_module(): module_setup(test_config, 'layergroups.yaml') module_setup(test_config_with_root, 'layergroups_root.yaml') def teardown_module(): module_teardown(test_config) module_teardown(test_config_with_root) TESTSERVER_ADDRESS = 'localhost', 42423 class TestWMSWithRoot(SystemTest): config = test_config_with_root def setup(self): SystemTest.setup(self) def _check_layernames(self, xml): eq_(xml.xpath('//Capability/Layer/Title/text()'), ['Root Layer']) eq_(xml.xpath('//Capability/Layer/Name/text()'), ['root']) eq_(xml.xpath('//Capability/Layer/Layer/Name/text()'), ['layer1', 'layer2']) eq_(xml.xpath('//Capability/Layer/Layer[1]/Layer/Name/text()'), ['layer1a', 'layer1b']) def _check_layernames_with_namespace(self, xml, namespaces=None): eq_(xml.xpath('//wms:Capability/wms:Layer/wms:Title/text()', namespaces=namespaces), ['Root Layer']) eq_(xml.xpath('//wms:Capability/wms:Layer/wms:Name/text()', namespaces=namespaces), ['root']) eq_(xml.xpath('//wms:Capability/wms:Layer/wms:Layer/wms:Name/text()', namespaces=namespaces), ['layer1', 'layer2']) eq_(xml.xpath('//wms:Capability/wms:Layer/wms:Layer[1]/wms:Layer/wms:Name/text()', namespaces=namespaces), ['layer1a', 'layer1b']) def test_100_capa(self): resp = self.app.get("/service?request=GetCapabilities&service=WMS&wmtver=1.0.0") xml = resp.lxml assert is_100_capa(xml) self._check_layernames(xml) def test_110_capa(self): resp = self.app.get("/service?request=GetCapabilities&service=WMS&version=1.1.0") xml = resp.lxml assert is_110_capa(xml) self._check_layernames(xml) def test_111_capa(self): resp = self.app.get("/service?request=GetCapabilities&service=WMS&version=1.1.1") xml = resp.lxml assert is_111_capa(xml) self._check_layernames(xml) def test_130_capa(self): resp = self.app.get("/service?request=GetCapabilities&service=WMS&version=1.3.0") xml = resp.lxml assert is_130_capa(xml) self._check_layernames_with_namespace(xml, ns130) class TestWMSWithoutRoot(SystemTest): config = test_config def setup(self): SystemTest.setup(self) def _check_layernames(self, xml): eq_(xml.xpath('//Capability/Layer/Title/text()'), ['My WMS']) eq_(xml.xpath('//Capability/Layer/Name/text()'), []) eq_(xml.xpath('//Capability/Layer/Layer/Name/text()'), ['layer1', 'layer2']) eq_(xml.xpath('//Capability/Layer/Layer[1]/Layer/Name/text()'), ['layer1a', 'layer1b']) eq_(xml.xpath('//Capability/Layer/Layer[2]/Layer/Name/text()'), ['layer2a', 'layer2b']) eq_(xml.xpath('//Capability/Layer/Layer[2]/Layer/Layer[1]/Name/text()'), ['layer2b1']) def _check_layernames_with_namespace(self, xml, namespaces=None): eq_(xml.xpath('//wms:Capability/wms:Layer/wms:Title/text()', namespaces=namespaces), ['My WMS']) eq_(xml.xpath('//wms:Capability/wms:Layer/wms:Name/text()', namespaces=namespaces), []) eq_(xml.xpath('//wms:Capability/wms:Layer/wms:Layer/wms:Name/text()', namespaces=namespaces), ['layer1', 'layer2']) eq_(xml.xpath('//wms:Capability/wms:Layer/wms:Layer[1]/wms:Layer/wms:Name/text()', namespaces=namespaces), ['layer1a', 'layer1b']) eq_(xml.xpath('//wms:Capability/wms:Layer/wms:Layer[2]/wms:Layer/wms:Name/text()', namespaces=namespaces), ['layer2a', 'layer2b']) eq_(xml.xpath('//wms:Capability/wms:Layer/wms:Layer[2]/wms:Layer/wms:Layer[1]/wms:Name/text()', namespaces=namespaces), ['layer2b1']) def test_100_capa(self): resp = self.app.get("/service?request=GetCapabilities&service=WMS&wmtver=1.0.0") xml = resp.lxml assert is_100_capa(xml) self._check_layernames(xml) def test_110_capa(self): resp = self.app.get("/service?request=GetCapabilities&service=WMS&version=1.1.0") xml = resp.lxml assert is_110_capa(xml) self._check_layernames(xml) def test_111_capa(self): resp = self.app.get("/service?request=GetCapabilities&service=WMS&version=1.1.1") xml = resp.lxml assert is_111_capa(xml) self._check_layernames(xml) def test_130_capa(self): resp = self.app.get("/service?request=GetCapabilities&service=WMS&version=1.3.0") xml = resp.lxml assert is_130_capa(xml) self._check_layernames_with_namespace(xml, ns130)mapproxy-1.11.0/mapproxy/test/system/test_legendgraphic.py000066400000000000000000000245111320454472400240500ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division from io import BytesIO from mapproxy.compat.image import Image from mapproxy.request.wms import ( WMS111MapRequest, WMS111CapabilitiesRequest, WMS130CapabilitiesRequest, WMS111LegendGraphicRequest, WMS130LegendGraphicRequest ) from mapproxy.test.system import module_setup, module_teardown, make_base_config, SystemTest from mapproxy.test.image import is_png, tmp_image from mapproxy.test.http import mock_httpd from mapproxy.test.helper import validate_with_dtd, validate_with_xsd from mapproxy.test.system.test_wms import is_111_capa, eq_xpath_wms130, ns130 from nose.tools import eq_ test_config = {} def setup_module(): module_setup(test_config, 'legendgraphic.yaml', with_cache_data=False) def teardown_module(): module_teardown(test_config) base_config = make_base_config(test_config) def is_130_capa(xml): return validate_with_xsd(xml, xsd_name='sld/1.1.0/sld_capabilities.xsd') class TestWMS(SystemTest): config = test_config def setup(self): SystemTest.setup(self) self.common_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1')) self.common_lg_req_111 = WMS111LegendGraphicRequest(url='/service?', param=dict(format='image/png', layer='wms_legend', sld_version='1.1.0')) self.common_lg_req_130 = WMS130LegendGraphicRequest(url='/service?', param=dict(format='image/png', layer='wms_legend', sld_version='1.1.0')) #test_00, test_01, test_02 need to run first in order to run the other tests properly def test_00_get_legendgraphic_multiple_sources_111(self): self.common_lg_req_111.params['layer'] = 'wms_mult_sources' with tmp_image((256, 256), format='png') as img: img_data = img.read() expected_req1 = ({'path': r'/service?LAYER=foo&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetLegendGraphic&' '&VERSION=1.1.1&SLD_VERSION=1.1.0'}, {'body': img_data, 'headers': {'content-type': 'image/png'}}) expected_req2 = ({'path': r'/service?LAYER=bar&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetLegendGraphic&' '&VERSION=1.1.1&SLD_VERSION=1.1.0'}, {'body': img_data, 'headers': {'content-type': 'image/png'}}) expected_req3 = ({'path': r'/service?LAYER=spam&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetLegendGraphic&' '&VERSION=1.1.1&SLD_VERSION=1.1.0'}, {'body': img_data, 'headers': {'content-type': 'image/png'}}) with mock_httpd(('localhost', 42423), [expected_req1, expected_req2, expected_req3]): resp = self.app.get(self.common_lg_req_111) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) assert Image.open(data).size == (256,768) def test_01_get_legendgraphic_source_static_url(self): self.common_lg_req_111.params['layer'] = 'wms_source_static_url' with tmp_image((256, 256), format='png') as img: img_data = img.read() expected_req1 = ({'path': r'/staticlegend_source.png'}, {'body': img_data, 'headers': {'content-type': 'image/png'}}) with mock_httpd(('localhost', 42423), [expected_req1]): resp = self.app.get(self.common_lg_req_111) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) assert Image.open(data).size == (256,256) def test_02_get_legendgraphic_layer_static_url(self): self.common_lg_req_111.params['layer'] = 'wms_layer_static_url' with tmp_image((256, 256), format='png') as img: img_data = img.read() expected_req1 = ({'path': r'/staticlegend_layer.png'}, {'body': img_data, 'headers': {'content-type': 'image/png'}}) with mock_httpd(('localhost', 42423), [expected_req1]): resp = self.app.get(self.common_lg_req_111) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) assert Image.open(data).size == (256,256) def test_capabilities_111(self): req = WMS111CapabilitiesRequest(url='/service?').copy_with_request_params(self.common_req) resp = self.app.get(req) xml = resp.lxml eq_(xml.xpath('//Request/GetLegendGraphic')[0].tag, 'GetLegendGraphic') legend_sizes = (xml.xpath('//Layer/Style/LegendURL/@width'), xml.xpath('//Layer/Style/LegendURL/@height')) assert legend_sizes == (['256', '256', '256', '256'],['512', '768', '256', '256']) layer_urls = xml.xpath('//Layer/Style/LegendURL/OnlineResource/@xlink:href', namespaces=ns130) for layer_url in layer_urls: assert layer_url.startswith('http://') assert 'GetLegendGraphic' in layer_url assert is_111_capa(xml) def test_capabilities_130(self): req = WMS130CapabilitiesRequest(url='/service?').copy_with_request_params(self.common_req) resp = self.app.get(req) xml = resp.lxml eq_(xml.xpath('//wms:Request/sld:GetLegendGraphic', namespaces=ns130)[0].tag, '{%s}GetLegendGraphic'%(ns130['sld'])) layer_urls = xml.xpath('//Layer/Style/LegendURL/OnlineResource/@xlink:href', namespaces=ns130) for layer_url in layer_urls: assert layer_url.startswith('http://') assert 'GetLegendGraphic' in layer_url assert is_130_capa(xml) def test_get_legendgraphic_111(self): self.common_lg_req_111.params['scale'] = '5.0' with tmp_image((256, 256), format='png') as img: img_data = img.read() expected_req1 = ({'path': r'/service?LAYER=foo&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetLegendGraphic&SCALE=5.0&' '&VERSION=1.1.1&SLD_VERSION=1.1.0'}, {'body': img_data, 'headers': {'content-type': 'image/png'}}) expected_req2 = ({'path': r'/service?LAYER=bar&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetLegendGraphic&SCALE=5.0&' '&VERSION=1.1.1&SLD_VERSION=1.1.0'}, {'body': img_data, 'headers': {'content-type': 'image/png'}}) with mock_httpd(('localhost', 42423), [expected_req1, expected_req2]): resp = self.app.get(self.common_lg_req_111) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) assert Image.open(data).size == (256,512) def test_get_legendgraphic_no_legend_111(self): self.common_lg_req_111.params['layer'] = 'wms_no_legend' resp = self.app.get(self.common_lg_req_111) eq_(resp.content_type, 'application/vnd.ogc.se_xml') xml = resp.lxml assert 'wms_no_legend has no legend graphic' in xml.xpath('//ServiceException/text()')[0] assert validate_with_dtd(xml, 'wms/1.1.1/exception_1_1_1.dtd') def test_get_legendgraphic_missing_params_111(self): req = str(self.common_lg_req_111).replace('sld_version', 'invalid').replace('format', 'invalid') resp = self.app.get(req) eq_(resp.content_type, 'application/vnd.ogc.se_xml') xml = resp.lxml assert 'missing parameters' in xml.xpath('//ServiceException/text()')[0] assert validate_with_dtd(xml, 'wms/1.1.1/exception_1_1_1.dtd') def test_get_legendgraphic_invalid_sld_version_111(self): req = str(self.common_lg_req_111).replace('sld_version=1.1.0', 'sld_version=1.0.0') resp = self.app.get(req) eq_(resp.content_type, 'application/vnd.ogc.se_xml') xml = resp.lxml assert 'invalid sld_version' in xml.xpath('//ServiceException/text()')[0] assert validate_with_dtd(xml, 'wms/1.1.1/exception_1_1_1.dtd') def test_get_legendgraphic_no_legend_130(self): self.common_lg_req_130.params['layer'] = 'wms_no_legend' resp = self.app.get(self.common_lg_req_130) eq_(resp.content_type, 'text/xml') xml = resp.lxml eq_xpath_wms130(xml, '/ogc:ServiceExceptionReport/@version', '1.3.0') eq_xpath_wms130(xml, '//ogc:ServiceException/text()', 'layer wms_no_legend has no legend graphic') assert validate_with_xsd(xml, xsd_name='wms/1.3.0/exceptions_1_3_0.xsd') def test_get_legendgraphic_missing_params_130(self): req = str(self.common_lg_req_130).replace('format', 'invalid') resp = self.app.get(req) eq_(resp.content_type, 'text/xml') xml = resp.lxml eq_xpath_wms130(xml, '/ogc:ServiceExceptionReport/@version', '1.3.0') eq_xpath_wms130(xml, '//ogc:ServiceException/text()', "missing parameters ['format']") assert validate_with_xsd(xml, xsd_name='wms/1.3.0/exceptions_1_3_0.xsd') def test_get_legendgraphic_invalid_sld_version_130(self): req = str(self.common_lg_req_130).replace('sld_version=1.1.0', 'sld_version=1.0.0') resp = self.app.get(req) eq_(resp.content_type, 'text/xml') xml = resp.lxml eq_xpath_wms130(xml, '/ogc:ServiceExceptionReport/@version', '1.3.0') eq_xpath_wms130(xml, '//ogc:ServiceException/text()', 'invalid sld_version 1.0.0') assert validate_with_xsd(xml, xsd_name='wms/1.3.0/exceptions_1_3_0.xsd') mapproxy-1.11.0/mapproxy/test/system/test_mapnik.py000066400000000000000000000123631320454472400225350ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import os from mapproxy.test.system import module_setup, module_teardown, SystemTest from mapproxy.compat.image import Image from io import BytesIO from nose.tools import eq_ test_config = {} mapnik_xml = b""" marker ogr test_point.geojson OGRGeoJSON """.strip() test_point_geojson = b""" {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-45, -45]}, "properties": {}} """.strip() mapnik_transp_xml = b""" """.strip() def setup_module(): try: import mapnik mapnik except ImportError: try: import mapnik2 as mapnik mapnik except ImportError: from nose.plugins.skip import SkipTest raise SkipTest('requires mapnik') module_setup(test_config, 'mapnik_source.yaml') with open(os.path.join(test_config['base_dir'], 'test_point.geojson'), 'wb') as f: f.write(test_point_geojson) with open(os.path.join(test_config['base_dir'], 'mapnik.xml'), 'wb') as f: f.write(mapnik_xml) with open(os.path.join(test_config['base_dir'], 'mapnik-transparent.xml'), 'wb') as f: f.write(mapnik_transp_xml) def teardown_module(): module_teardown(test_config) class TestMapnikSource(SystemTest): config = test_config def test_get_map(self): req = (r'/service?LAYERs=mapnik&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=200&SRS=EPSG%3A4326' '&VERSION=1.1.1&BBOX=-90,-90,0,0&styles=' '&WIDTH=200&') resp = self.app.get(req) data = BytesIO(resp.body) img = Image.open(data) colors = sorted(img.getcolors(), reverse=True) # map bg color + black marker assert 39700 < colors[0][0] < 39900, colors[0][0] eq_(colors[0][1], (255, 0, 0, 255)) assert 50 < colors[1][0] < 150, colors[1][0] eq_(colors[1][1], (0, 0, 0, 255)) def test_get_map_hq(self): req = (r'/service?LAYERs=mapnik_hq&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=200&SRS=EPSG%3A4326' '&VERSION=1.1.1&BBOX=-90,-90,0,0&styles=' '&WIDTH=200&') resp = self.app.get(req) data = BytesIO(resp.body) img = Image.open(data) colors = sorted(img.getcolors(), reverse=True) # map bg color + black marker (like above, but marker is scaled up) assert 39500 < colors[0][0] < 39600, colors[0][0] eq_(colors[0][1], (255, 0, 0, 255)) assert 250 < colors[1][0] < 350, colors[1][0] eq_(colors[1][1], (0, 0, 0, 255)) def test_get_map_outside_coverage(self): req = (r'/service?LAYERs=mapnik&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=200&SRS=EPSG%3A4326' '&VERSION=1.1.1&BBOX=-175,-85,-172,-82&styles=' '&WIDTH=200&&BGCOLOR=0x00ff00') resp = self.app.get(req) data = BytesIO(resp.body) img = Image.open(data) colors = sorted(img.getcolors(), reverse=True) # wms request bg color eq_(colors[0], (40000, (0, 255, 0))) def test_get_map_unknown_file(self): req = (r'/service?LAYERs=mapnik_unknown&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=200&SRS=EPSG%3A4326' '&VERSION=1.1.1&BBOX=-90,-90,0,0&styles=' '&WIDTH=200&&BGCOLOR=0x00ff00') resp = self.app.get(req) assert 'unknown.xml' in resp.body, resp.body def test_get_map_transparent(self): req = (r'/service?LAYERs=mapnik_transparent&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=200&SRS=EPSG%3A4326' '&VERSION=1.1.1&BBOX=-90,-90,0,0&styles=' '&WIDTH=200&transparent=True') resp = self.app.get(req) data = BytesIO(resp.body) img = Image.open(data) colors = sorted(img.getcolors(), reverse=True) eq_(colors[0], (40000, (0, 0, 0, 0))) mapproxy-1.11.0/mapproxy/test/system/test_mapserver.py000066400000000000000000000043461320454472400232640ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import os import stat import platform import shutil from io import BytesIO from mapproxy.request.wms import WMS111MapRequest from mapproxy.compat.image import Image from mapproxy.test.image import is_png from mapproxy.test.system import prepare_env, create_app, module_teardown, SystemTest from nose.tools import eq_ from nose.plugins.skip import SkipTest test_config = {} def setup_module(): if platform.system() == 'Windows': raise SkipTest('CGI test only works on Unix (for now)') prepare_env(test_config, 'mapserver.yaml') shutil.copy(os.path.join(test_config['fixture_dir'], 'cgi.py'), test_config['base_dir']) os.chmod(os.path.join(test_config['base_dir'], 'cgi.py'), stat.S_IXUSR | stat.S_IRUSR | stat.S_IWUSR) os.mkdir(os.path.join(test_config['base_dir'], 'tmp')) create_app(test_config) def teardown_module(): module_teardown(test_config) class TestMapServerCGI(SystemTest): config = test_config def setup(self): SystemTest.setup(self) self.common_map_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1', bbox='-180,0,0,80', width='200', height='200', layers='ms', srs='EPSG:4326', format='image/png', styles='', request='GetMap')) def test_get_map(self): resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) img = Image.open(data) img = img.convert('RGB') eq_(img.getcolors(), [(200*200, (255, 0, 0))]) mapproxy-1.11.0/mapproxy/test/system/test_mixed_mode_format.py000066400000000000000000000156211320454472400247400ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import os from io import BytesIO from mapproxy.compat.image import ( Image, ImageDraw, ImageColor, ) from mapproxy.request.wms import WMS111MapRequest from mapproxy.request.wmts import WMTS100TileRequest from mapproxy.test.image import check_format, is_transparent from mapproxy.test.http import mock_httpd from mapproxy.test.system import module_setup, module_teardown, SystemTest, make_base_config from nose.tools import eq_ from contextlib import contextmanager test_config = {} base_config = make_base_config(test_config) def setup_module(): module_setup(test_config, 'mixed_mode.yaml', with_cache_data=True) def teardown_module(): module_teardown(test_config) class TestWMS(SystemTest): config = test_config def setup(self): SystemTest.setup(self) self.common_map_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1', bbox='0,0,180,80', width='200', height='200', layers='mixed_mode', srs='EPSG:4326', format='image/png', styles='', request='GetMap', transparent='true')) self.expected_base_path = '/service?SERVICE=WMS&REQUEST=GetMap&HEIGHT=256' \ '&SRS=EPSG%3A900913&styles=&VERSION=1.1.1&WIDTH=512' \ '&BBOX=-20037508.3428,0.0,20037508.3428,20037508.3428' def test_mixed_mode(self): req_format = 'png' transparent = 'True' with create_mixed_mode_img((512, 256)) as img: expected_req = ({'path': self.expected_base_path + '&layers=mixedsource' + '&format=image%2F' + req_format + '&transparent=' + transparent}, {'body': img.read(), 'headers': {'content-type': 'image/'+req_format}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): self.common_map_req.params['format'] = 'image/'+req_format resp = self.app.get(self.common_map_req) self.created_tiles.append('mixed_cache_EPSG900913/01/000/000/000/000/000/001.mixed') self.created_tiles.append('mixed_cache_EPSG900913/01/000/000/001/000/000/001.mixed') eq_(resp.content_type, 'image/'+req_format) check_format(BytesIO(resp.body), req_format) # GetMap Request is fully within the opaque tile assert not is_transparent(resp.body) # check cache formats cache_dir = base_config().cache.base_dir check_format(open(os.path.join(cache_dir, self.created_tiles[0]), 'rb'), 'png') check_format(open(os.path.join(cache_dir, self.created_tiles[1]), 'rb'), 'jpeg') class TestTMS(SystemTest): config = test_config def setup(self): SystemTest.setup(self) self.expected_base_path = '/service?SERVICE=WMS&REQUEST=GetMap&HEIGHT=256' \ '&SRS=EPSG%3A900913&styles=&VERSION=1.1.1&WIDTH=512' \ '&BBOX=-20037508.3428,-20037508.3428,20037508.3428,0.0' def test_mixed_mode(self): with create_mixed_mode_img((512, 256)) as img: expected_req = ({'path': self.expected_base_path + '&layers=mixedsource' + '&format=image%2Fpng' + '&transparent=True'}, {'body': img.read(), 'headers': {'content-type': 'image/png'}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): resp = self.app.get('/tms/1.0.0/mixed_mode/0/0/0.png') eq_(resp.content_type, 'image/png') assert is_transparent(resp.body) resp = self.app.get('/tms/1.0.0/mixed_mode/0/1/0.png') eq_(resp.content_type, 'image/jpeg') self.created_tiles.append('mixed_cache_EPSG900913/01/000/000/000/000/000/000.mixed') self.created_tiles.append('mixed_cache_EPSG900913/01/000/000/001/000/000/000.mixed') class TestWMTS(SystemTest): config = test_config def setup(self): SystemTest.setup(self) self.common_tile_req = WMTS100TileRequest(url='/service?', param=dict(service='WMTS', version='1.0.0', tilerow='0', tilecol='0', tilematrix='01', tilematrixset='GLOBAL_MERCATOR', layer='mixed_mode', format='image/png', style='', request='GetTile', transparent='True')) self.expected_base_path = '/service?SERVICE=WMS&REQUEST=GetMap&HEIGHT=256' \ '&SRS=EPSG%3A900913&styles=&VERSION=1.1.1&WIDTH=512' \ '&BBOX=-20037508.3428,0.0,20037508.3428,20037508.3428' def test_mixed_mode(self): with create_mixed_mode_img((512, 256)) as img: expected_req = ({'path': self.expected_base_path + '&layers=mixedsource' + '&format=image%2Fpng' + '&transparent=True'}, {'body': img.read(), 'headers': {'content-type': 'image/png'}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): resp = self.app.get(self.common_tile_req) eq_(resp.content_type, 'image/png') assert is_transparent(resp.body) self.created_tiles.append('mixed_cache_EPSG900913/01/000/000/000/000/000/001.mixed') self.common_tile_req.params['tilecol'] = '1' resp = self.app.get(self.common_tile_req) eq_(resp.content_type, 'image/jpeg') self.created_tiles.append('mixed_cache_EPSG900913/01/000/000/001/000/000/001.mixed') @contextmanager def create_mixed_mode_img(size, format='png'): img = Image.new("RGBA", size) # draw a black rectangle into the image, rect_width = 3/4 img_width # thus 1/4 of the image is transparent and with square tiles, one of two # tiles (img size = 512x256) will be fully opaque and the other # has transparency draw = ImageDraw.Draw(img) w, h = size red_color = ImageColor.getrgb("red") draw.rectangle((w/4, 0, w, h), fill=red_color) data = BytesIO() img.save(data, format) data.seek(0) yield data mapproxy-1.11.0/mapproxy/test/system/test_multi_cache_layers.py000066400000000000000000000156251320454472400251160ustar00rootroot00000000000000# -:- encoding: utf8 -:- # This file is part of the MapProxy project. # Copyright (C) 2015 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import functools from io import BytesIO from mapproxy.request.wmts import ( WMTS100TileRequest, WMTS100CapabilitiesRequest ) from mapproxy.request.wms import WMS111CapabilitiesRequest from mapproxy.test.image import is_png, create_tmp_image from mapproxy.test.http import MockServ from mapproxy.test.helper import validate_with_xsd from mapproxy.test.system import module_setup, module_teardown, SystemTest, make_base_config from nose.tools import eq_ test_config = {} base_config = make_base_config(test_config) def setup_module(): module_setup(test_config, 'multi_cache_layers.yaml', with_cache_data=True) def teardown_module(): module_teardown(test_config) ns_wmts = { 'wmts': 'http://www.opengis.net/wmts/1.0', 'ows': 'http://www.opengis.net/ows/1.1', 'xlink': 'http://www.w3.org/1999/xlink' } def eq_xpath(xml, xpath, expected, namespaces=None): eq_(xml.xpath(xpath, namespaces=namespaces)[0], expected) eq_xpath_wmts = functools.partial(eq_xpath, namespaces=ns_wmts) TEST_TILE = create_tmp_image((256, 256)) class TestMultiCacheLayer(SystemTest): config = test_config def setup(self): SystemTest.setup(self) self.common_cap_req = WMTS100CapabilitiesRequest( url='/service?', param=dict(service='WMTS', version='1.0.0', request='GetCapabilities')) self.common_tile_req = WMTS100TileRequest( url='/service?', param=dict(service='WMTS', version='1.0.0', tilerow='0', tilecol='0', tilematrix='01', tilematrixset='GLOBAL_WEBMERCATOR', layer='multi_cache', format='image/png', style='', request='GetTile')) def test_tms_capabilities(self): resp = self.app.get('/tms/1.0.0/') assert 'http://localhost/tms/1.0.0/multi_cache/EPSG25832' in resp assert 'http://localhost/tms/1.0.0/multi_cache/EPSG3857' in resp assert 'http://localhost/tms/1.0.0/multi_cache/CRS84' in resp assert 'http://localhost/tms/1.0.0/multi_cache/EPSG31467' in resp assert 'http://localhost/tms/1.0.0/cache/EPSG25832' in resp xml = resp.lxml assert xml.xpath('count(//TileMap)') == 5 def test_wmts_capabilities(self): req = str(self.common_cap_req) resp = self.app.get(req) eq_(resp.content_type, 'application/xml') xml = resp.lxml assert validate_with_xsd( xml, xsd_name='wmts/1.0/wmtsGetCapabilities_response.xsd') eq_(set(xml.xpath('//wmts:Layer/ows:Identifier/text()', namespaces=ns_wmts)), set(['cache', 'multi_cache'])) eq_(set(xml.xpath('//wmts:Contents/wmts:TileMatrixSet/ows:Identifier/text()', namespaces=ns_wmts)), set(['gk3', 'GLOBAL_WEBMERCATOR', 'utm32', 'InspireCrs84Quad'])) def test_wms_capabilities(self): req = WMS111CapabilitiesRequest(url='/service?') resp = self.app.get(req) eq_(resp.content_type, 'application/vnd.ogc.wms_xml') xml = resp.lxml eq_(xml.xpath('//GetMap//OnlineResource/@xlink:href', namespaces=dict(xlink="http://www.w3.org/1999/xlink"))[0], 'http://localhost/service?') layer_names = set(xml.xpath('//Layer/Layer/Name/text()')) expected_names = set(['wms_only', 'cache']) eq_(layer_names, expected_names) def test_get_tile_webmerc(self): serv = MockServ(42423, bbox_aware_query_comparator=True) serv.expects( '/service?layers=foo,bar&width=256&version=1.1.1&bbox=-20037508.3428,0.0,0.0,20037508.3428&service=WMS&format=image%2Fpng&styles=&srs=EPSG%3A3857&request=GetMap&height=256').returns(TEST_TILE) with serv: resp = self.app.get(str(self.common_tile_req)) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) def test_get_tile_utm(self): serv = MockServ(42423, bbox_aware_query_comparator=True) serv.expects( '/service?layers=foo,bar&width=256&version=1.1.1&bbox=-46133.17,5675047.40429,580038.965712,6301219.54&service=WMS&format=image%2Fpng&styles=&srs=EPSG%3A25832&request=GetMap&height=256').returns(TEST_TILE) self.common_tile_req.params['tilematrixset'] = 'utm32' with serv: resp = self.app.get(str(self.common_tile_req)) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) def test_get_tile_cascaded_cache(self): serv = MockServ( 42423, bbox_aware_query_comparator=True, unordered=True) # gk3 cache requests UTM tiles serv.expects( '/service?layers=foo,bar&width=256&version=1.1.1&bbox=423495.931784,5596775.88732,501767.448748,5675047.40429&service=WMS&format=image%2Fpng&styles=&srs=EPSG%3A25832&request=GetMap&height=256').returns(TEST_TILE) serv.expects( '/service?layers=foo,bar&width=256&version=1.1.1&bbox=345224.41482,5596775.88732,423495.931784,5675047.40429&service=WMS&format=image%2Fpng&styles=&srs=EPSG%3A25832&request=GetMap&height=256').returns(TEST_TILE) serv.expects( '/service?layers=foo,bar&width=256&version=1.1.1&bbox=345224.41482,5518504.37036,423495.931784,5596775.88732&service=WMS&format=image%2Fpng&styles=&srs=EPSG%3A25832&request=GetMap&height=256').returns(TEST_TILE) serv.expects( '/service?layers=foo,bar&width=256&version=1.1.1&bbox=423495.931784,5518504.37036,501767.448748,5596775.88732&service=WMS&format=image%2Fpng&styles=&srs=EPSG%3A25832&request=GetMap&height=256').returns(TEST_TILE) serv.expects( '/service?layers=foo,bar&width=256&version=1.1.1&bbox=345224.41482,5440232.8534,423495.931784,5518504.37036&service=WMS&format=image%2Fpng&styles=&srs=EPSG%3A25832&request=GetMap&height=256').returns(TEST_TILE) serv.expects( '/service?layers=foo,bar&width=256&version=1.1.1&bbox=423495.931784,5440232.8534,501767.448748,5518504.37036&service=WMS&format=image%2Fpng&styles=&srs=EPSG%3A25832&request=GetMap&height=256').returns(TEST_TILE) self.common_tile_req.params['tilematrixset'] = 'gk3' with serv: resp = self.app.get(str(self.common_tile_req)) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) mapproxy-1.11.0/mapproxy/test/system/test_multiapp.py000066400000000000000000000064571320454472400231200ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import io import os import tempfile import shutil from webtest import TestApp from mapproxy.multiapp import app_factory def module_setup(test_config, config_files): fixture_dir = os.path.join(os.path.dirname(__file__), 'fixture') test_config['base_dir'] = tempfile.mkdtemp() test_config['config_files'] = [] for config_file in config_files: config_file_src = os.path.join(fixture_dir, config_file) config_file_dst = os.path.join(test_config['base_dir'], config_file) shutil.copy(config_file_src, config_file_dst) test_config['config_files'].append(config_file_dst) app = app_factory({}, config_dir=test_config['base_dir'], allow_listing=False) test_config['multiapp'] = app test_config['app'] = TestApp(app, use_unicode=False) def module_teardown(test_config): shutil.rmtree(test_config['base_dir']) test_config.clear() test_config = {} def setup_module(): module_setup(test_config, ['multiapp1.yaml', 'multiapp2.yaml']) def teardown_module(): module_teardown(test_config) class TestMultiapp(object): def setup(self): self.multiapp = test_config['multiapp'] self.app = test_config['app'] def test_index_without_list(self): resp = self.app.get('/') assert 'MapProxy' in resp assert 'multiapp1' not in resp def test_index_with_list(self): try: self.multiapp.list_apps = True resp = self.app.get('/') assert 'MapProxy' in resp assert 'multiapp1' in resp finally: self.multiapp.list_apps = False def test_unknown_app(self): self.app.get('/unknownapp', status=404) # assert status == 404 Not Found in app.get def test_known_app(self): resp = self.app.get('/multiapp1') assert 'demo' in resp def test_reloading(self): resp = self.app.get('/multiapp1') assert 'demo' in resp app_config = test_config['config_files'][0] replace_text_in_file(app_config, ' demo:', ' #demo:', ts_delta=5) resp = self.app.get('/multiapp1') assert 'demo' not in resp replace_text_in_file(app_config, ' #demo:', ' demo:', ts_delta=10) resp = self.app.get('/multiapp1') assert 'demo' in resp def replace_text_in_file(filename, old, new, ts_delta=2): text = io.open(filename, encoding='utf-8').read() text = text.replace(old, new) io.open(filename, 'w', encoding='utf-8').write(text) # file timestamps are not precise enough (1sec) # add larger delta to force reload m_time = os.path.getmtime(filename) os.utime(filename, (m_time+ts_delta, m_time+ts_delta)) mapproxy-1.11.0/mapproxy/test/system/test_renderd_client.py000066400000000000000000000270531320454472400242410ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010-2012 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os try: import json; json except ImportError: # test skipped later json = None from mapproxy.test.image import img_from_buf from mapproxy.test.http import mock_single_req_httpd from mapproxy.test.system import module_setup, module_teardown, SystemTest, make_base_config from mapproxy.request.wms import WMS111MapRequest, WMS111FeatureInfoRequest, WMS111CapabilitiesRequest from mapproxy.test.helper import validate_with_dtd from mapproxy.test.http import mock_httpd from mapproxy.test.image import create_tmp_image from mapproxy.test.system.test_wms import is_111_exception from mapproxy.util.fs import ensure_directory from mapproxy.cache.renderd import has_renderd_support from nose.tools import eq_ from nose.plugins.skip import SkipTest test_config = {} base_config = make_base_config(test_config) def setup_module(): if not has_renderd_support(): raise SkipTest("requests required") module_setup(test_config, 'renderd_client.yaml', with_cache_data=True) def teardown_module(): module_teardown(test_config) try: from http.server import BaseHTTPRequestHandler except ImportError: from BaseHTTPServer import BaseHTTPRequestHandler class TestWMS111(SystemTest): config = test_config def setup(self): SystemTest.setup(self) self.common_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1')) self.common_map_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1', bbox='-180,0,0,80', width='200', height='200', layers='wms_cache', srs='EPSG:4326', format='image/png', exceptions='xml', styles='', request='GetMap')) self.common_fi_req = WMS111FeatureInfoRequest(url='/service?', param=dict(x='10', y='20', width='200', height='200', layers='wms_cache', format='image/png', query_layers='wms_cache', styles='', bbox='1000,400,2000,1400', srs='EPSG:900913')) def test_wms_capabilities(self): req = WMS111CapabilitiesRequest(url='/service?').copy_with_request_params(self.common_req) resp = self.app.get(req) eq_(resp.content_type, 'application/vnd.ogc.wms_xml') xml = resp.lxml eq_(xml.xpath('//GetMap//OnlineResource/@xlink:href', namespaces=dict(xlink="http://www.w3.org/1999/xlink"))[0], 'http://localhost/service?') layer_names = set(xml.xpath('//Layer/Layer/Name/text()')) expected_names = set(['direct', 'wms_cache', 'tms_cache']) eq_(layer_names, expected_names) assert validate_with_dtd(xml, dtd_name='wms/1.1.1/WMS_MS_Capabilities.dtd') def test_get_map(self): test_self = self class req_handler(BaseHTTPRequestHandler): def do_POST(self): length = int(self.headers['content-length']) json_data = self.rfile.read(length) task = json.loads(json_data.decode('utf-8')) eq_(task['command'], 'tile') # request main tile of metatile eq_(task['tiles'], [[15, 17, 5]]) eq_(task['cache_identifier'], 'wms_cache_GLOBAL_MERCATOR') eq_(task['priority'], 100) # this id should not change for the same tile/cache_identifier combination eq_(task['id'], 'aeb52b506e4e82d0a1edf649d56e0451cfd5862c') # manually create tile renderd should create tile_filename = os.path.join(test_self.config['cache_dir'], 'wms_cache_EPSG900913/05/000/000/016/000/000/016.jpeg') ensure_directory(tile_filename) with open(tile_filename, 'wb') as f: f.write(create_tmp_image((256, 256), format='jpeg', color=(255, 0, 100))) self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(b'{"status": "ok"}') def log_request(self, code, size=None): pass with mock_single_req_httpd(('localhost', 42423), req_handler): self.common_map_req.params['bbox'] = '0,0,9,9' resp = self.app.get(self.common_map_req) img = img_from_buf(resp.body) main_color = sorted(img.convert('RGBA').getcolors())[-1] # check for red color (jpeg/png conversion requires fuzzy comparision) assert main_color[0] == 40000 assert main_color[1][0] > 250 assert main_color[1][1] < 5 assert 95 < main_color[1][2] < 105 assert main_color[1][3] == 255 eq_(resp.content_type, 'image/png') self.created_tiles.append('wms_cache_EPSG900913/05/000/000/016/000/000/016.jpeg') def test_get_map_error(self): class req_handler(BaseHTTPRequestHandler): def do_POST(self): length = int(self.headers['content-length']) json_data = self.rfile.read(length) task = json.loads(json_data.decode('utf-8')) eq_(task['command'], 'tile') # request main tile of metatile eq_(task['tiles'], [[15, 17, 5]]) eq_(task['cache_identifier'], 'wms_cache_GLOBAL_MERCATOR') eq_(task['priority'], 100) # this id should not change for the same tile/cache_identifier combination eq_(task['id'], 'aeb52b506e4e82d0a1edf649d56e0451cfd5862c') self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(b'{"status": "error", "error_message": "barf"}') def log_request(self, code, size=None): pass with mock_single_req_httpd(('localhost', 42423), req_handler): self.common_map_req.params['bbox'] = '0,0,9,9' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'application/vnd.ogc.se_xml') is_111_exception(resp.lxml, re_msg='Error from renderd: barf') def test_get_map_connection_error(self): self.common_map_req.params['bbox'] = '0,0,9,9' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'application/vnd.ogc.se_xml') is_111_exception(resp.lxml, re_msg='Error while communicating with renderd:') def test_get_map_non_json_response(self): class req_handler(BaseHTTPRequestHandler): def do_POST(self): length = int(self.headers['content-length']) json_data = self.rfile.read(length) json.loads(json_data.decode('utf-8')) self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(b'{"invalid') def log_request(self, code, size=None): pass with mock_single_req_httpd(('localhost', 42423), req_handler): self.common_map_req.params['bbox'] = '0,0,9,9' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'application/vnd.ogc.se_xml') is_111_exception(resp.lxml, re_msg='Error while communicating with renderd: invalid JSON') def test_get_featureinfo(self): expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetFeatureInfo&HEIGHT=200&SRS=EPSG%3A900913' '&VERSION=1.1.1&BBOX=1000.0,400.0,2000.0,1400.0&styles=' '&WIDTH=200&QUERY_LAYERS=foo,bar&X=10&Y=20&feature_count=100'}, {'body': b'info', 'headers': {'content-type': 'text/plain'}}) with mock_httpd(('localhost', 42423), [expected_req]): self.common_fi_req.params['feature_count'] = 100 resp = self.app.get(self.common_fi_req) eq_(resp.content_type, 'text/plain') eq_(resp.body, b'info') class TestTiles(SystemTest): config = test_config def test_get_tile(self): test_self = self class req_handler(BaseHTTPRequestHandler): def do_POST(self): length = int(self.headers['content-length']) json_data = self.rfile.read(length) task = json.loads(json_data.decode('utf-8')) eq_(task['command'], 'tile') eq_(task['tiles'], [[10, 20, 6]]) eq_(task['cache_identifier'], 'tms_cache_GLOBAL_MERCATOR') eq_(task['priority'], 100) # this id should not change for the same tile/cache_identifier combination eq_(task['id'], 'cf35c1c927158e188d8fbe0db380c1772b536da9') # manually create tile renderd should create tile_filename = os.path.join(test_self.config['cache_dir'], 'tms_cache_EPSG900913/06/000/000/010/000/000/020.png') ensure_directory(tile_filename) with open(tile_filename, 'wb') as f: f.write(b"foobaz") self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(b'{"status": "ok"}') def log_request(self, code, size=None): pass with mock_single_req_httpd(('localhost', 42423), req_handler): resp = self.app.get('/tiles/tms_cache/EPSG900913/6/10/20.png') eq_(resp.content_type, 'image/png') eq_(resp.body, b'foobaz') self.created_tiles.append('tms_cache_EPSG900913/06/000/000/010/000/000/020.png') def test_get_tile_error(self): class req_handler(BaseHTTPRequestHandler): def do_POST(self): length = int(self.headers['content-length']) json_data = self.rfile.read(length) task = json.loads(json_data.decode('utf-8')) eq_(task['command'], 'tile') eq_(task['tiles'], [[10, 20, 7]]) eq_(task['cache_identifier'], 'tms_cache_GLOBAL_MERCATOR') eq_(task['priority'], 100) # this id should not change for the same tile/cache_identifier combination eq_(task['id'], 'c24b8c3247afec34fd0a53e5d3706e977877ef47') self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(b'{"status": "error", "error_message": "you told me to fail"}') def log_request(self, code, size=None): pass with mock_single_req_httpd(('localhost', 42423), req_handler): resp = self.app.get('/tiles/tms_cache/EPSG900913/7/10/20.png', status=500) eq_(resp.content_type, 'text/plain') eq_(resp.body, b'Error from renderd: you told me to fail') mapproxy-1.11.0/mapproxy/test/system/test_scalehints.py000066400000000000000000000115231320454472400234100ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import math from mapproxy.request.wms import ( WMS111MapRequest, WMS111CapabilitiesRequest, WMS130CapabilitiesRequest ) from mapproxy.test.system import module_setup, module_teardown, make_base_config, SystemTest from mapproxy.test.image import is_png, is_transparent, tmp_image from mapproxy.test.http import mock_httpd from mapproxy.test.system.test_wms import is_111_capa, is_130_capa, ns130 from nose.tools import assert_almost_equal test_config = {} def setup_module(): module_setup(test_config, 'scalehints.yaml') def teardown_module(): module_teardown(test_config) base_config = make_base_config(test_config) def diagonal_res_to_pixel_res(res): """ >>> '%.2f' % round(diagonal_res_to_pixel_res(14.14214), 4) '10.00' """ return math.sqrt((float(res)**2)/2) class TestWMS(SystemTest): config = test_config def setup(self): SystemTest.setup(self) self.common_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1')) self.common_map_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1', bbox='-180,0,0,80', width='200', height='200', layers='res', srs='EPSG:4326', format='image/png', transparent='true', styles='', request='GetMap')) def test_capabilities_111(self): req = WMS111CapabilitiesRequest(url='/service?').copy_with_request_params(self.common_req) resp = self.app.get(req) xml = resp.lxml assert is_111_capa(xml) hints = xml.xpath('//Layer/Layer/ScaleHint') assert_almost_equal(diagonal_res_to_pixel_res(hints[0].attrib['min']), 10, 2) assert_almost_equal(diagonal_res_to_pixel_res(hints[0].attrib['max']), 10000, 2) assert_almost_equal(diagonal_res_to_pixel_res(hints[1].attrib['min']), 2.8, 2) assert_almost_equal(diagonal_res_to_pixel_res(hints[1].attrib['max']), 280, 2) assert_almost_equal(diagonal_res_to_pixel_res(hints[2].attrib['min']), 0.28, 2) assert_almost_equal(diagonal_res_to_pixel_res(hints[2].attrib['max']), 2.8, 2) def test_capabilities_130(self): req = WMS130CapabilitiesRequest(url='/service?').copy_with_request_params(self.common_req) resp = self.app.get(req) xml = resp.lxml assert is_130_capa(xml) min_scales = xml.xpath('//wms:Layer/wms:Layer/wms:MinScaleDenominator/text()', namespaces=ns130) max_scales = xml.xpath('//wms:Layer/wms:Layer/wms:MaxScaleDenominator/text()', namespaces=ns130) assert_almost_equal(float(min_scales[0]), 35714.28, 1) assert_almost_equal(float(max_scales[0]), 35714285.7, 1) assert_almost_equal(float(min_scales[1]), 10000, 2) assert_almost_equal(float(max_scales[1]), 1000000, 2) assert_almost_equal(float(min_scales[2]), 1000, 2) assert_almost_equal(float(max_scales[2]), 10000, 2) def test_get_map_above_res(self): # no layer rendered resp = self.app.get(self.common_map_req) assert is_png(resp.body) assert is_transparent(resp.body) def test_get_map_mixed(self): # only res layer matches resolution range self.common_map_req.params['layers'] = 'res,scale' self.common_map_req.params['bbox'] = '0,0,100000,100000' self.common_map_req.params['srs'] = 'EPSG:900913' self.common_map_req.params.size = 100, 100 self.created_tiles.append('res_cache_EPSG900913/08/000/000/128/000/000/128.jpeg') with tmp_image((256, 256), format='jpeg') as img: expected_req = ({'path': r'/service?LAYERs=reslayer&SERVICE=WMS&FORMAT=image%2Fjpeg' '&REQUEST=GetMap&HEIGHT=256&SRS=EPSG%3A900913&styles=' '&VERSION=1.1.1&BBOX=0.0,0.0,156543.033928,156543.033928' '&WIDTH=256'}, {'body': img.read(), 'headers': {'content-type': 'image/png'}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): resp = self.app.get(self.common_map_req) assert is_png(resp.body) assert not is_transparent(resp.body) mapproxy-1.11.0/mapproxy/test/system/test_seed.py000066400000000000000000000433611320454472400222000ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import time import shutil import tempfile from mapproxy.config.loader import load_configuration from mapproxy.cache.tile import Tile from mapproxy.image import ImageSource from mapproxy.image.opts import ImageOptions from mapproxy.seed.seeder import seed from mapproxy.seed.cleanup import cleanup from mapproxy.seed.config import load_seed_tasks_conf from mapproxy.config import local_base_config from mapproxy.util.fs import ensure_directory from mapproxy.test.http import mock_httpd from mapproxy.test.image import tmp_image, create_tmp_image_buf, create_tmp_image from nose.tools import eq_ FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'fixture') class SeedTestEnvironment(object): def setup(self): self.dir = tempfile.mkdtemp() shutil.copy(os.path.join(FIXTURE_DIR, self.seed_conf_name), self.dir) shutil.copy(os.path.join(FIXTURE_DIR, self.mapproxy_conf_name), self.dir) shutil.copy(os.path.join(FIXTURE_DIR, self.empty_ogrdata), self.dir) self.seed_conf_file = os.path.join(self.dir, self.seed_conf_name) self.mapproxy_conf_file = os.path.join(self.dir, self.mapproxy_conf_name) self.mapproxy_conf = load_configuration(self.mapproxy_conf_file, seed=True) def teardown(self): shutil.rmtree(self.dir) def make_tile(self, coord=(0, 0, 0), timestamp=None): """ Create file for tile at `coord` with given timestamp. """ tile_dir = os.path.join(self.dir, 'cache/one_EPSG4326/%02d/000/000/%03d/000/000/' % (coord[2], coord[0])) ensure_directory(tile_dir) tile = os.path.join(tile_dir + '%03d.png' % coord[1]) open(tile, 'wb').write(b'') if timestamp: os.utime(tile, (timestamp, timestamp)) return tile def tile_exists(self, coord): tile_dir = os.path.join(self.dir, 'cache/one_EPSG4326/%02d/000/000/%03d/000/000/' % (coord[2], coord[0])) tile = os.path.join(tile_dir + '%03d.png' % coord[1]) return os.path.exists(tile) class SeedTestBase(SeedTestEnvironment): def test_seed_dry_run(self): with local_base_config(self.mapproxy_conf.base_config): seed_conf = load_seed_tasks_conf(self.seed_conf_file, self.mapproxy_conf) tasks, cleanup_tasks = seed_conf.seeds(['one']), seed_conf.cleanups() seed(tasks, dry_run=True) cleanup(cleanup_tasks, verbose=False, dry_run=True) def test_seed(self): with tmp_image((256, 256), format='png') as img: img_data = img.read() expected_req = ({'path': r'/service?LAYERS=foo&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&VERSION=1.1.1&bbox=-180.0,-90.0,180.0,90.0' '&width=256&height=128&srs=EPSG:4326'}, {'body': img_data, 'headers': {'content-type': 'image/png'}}) with mock_httpd(('localhost', 42423), [expected_req]): with local_base_config(self.mapproxy_conf.base_config): seed_conf = load_seed_tasks_conf(self.seed_conf_file, self.mapproxy_conf) tasks, cleanup_tasks = seed_conf.seeds(['one']), seed_conf.cleanups() seed(tasks, dry_run=False) cleanup(cleanup_tasks, verbose=False, dry_run=False) def test_reseed_uptodate(self): # tile already there. self.make_tile((0, 0, 0)) with local_base_config(self.mapproxy_conf.base_config): seed_conf = load_seed_tasks_conf(self.seed_conf_file, self.mapproxy_conf) tasks, cleanup_tasks = seed_conf.seeds(['one']), seed_conf.cleanups() seed(tasks, dry_run=False) cleanup(cleanup_tasks, verbose=False, dry_run=False) class TestSeedOldConfiguration(SeedTestBase): seed_conf_name = 'seed_old.yaml' mapproxy_conf_name = 'seed_mapproxy.yaml' empty_ogrdata = 'empty_ogrdata.geojson' def test_reseed_remove_before(self): # tile already there but too old t000 = self.make_tile((0, 0, 0), timestamp=time.time() - (60*60*25)) # old tile outside the seed view (should be removed) t001 = self.make_tile((0, 0, 1), timestamp=time.time() - (60*60*25)) assert os.path.exists(t000) assert os.path.exists(t001) with tmp_image((256, 256), format='png') as img: img_data = img.read() expected_req = ({'path': r'/service?LAYERS=foo&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&VERSION=1.1.1&bbox=-180.0,-90.0,180.0,90.0' '&width=256&height=128&srs=EPSG:4326'}, {'body': img_data, 'headers': {'content-type': 'image/png'}}) with mock_httpd(('localhost', 42423), [expected_req]): seed_conf = load_seed_tasks_conf(self.seed_conf_file, self.mapproxy_conf) tasks, cleanup_tasks = seed_conf.seeds(), seed_conf.cleanups() seed(tasks, dry_run=False) cleanup(cleanup_tasks, verbose=False, dry_run=False) assert os.path.exists(t000) assert os.path.getmtime(t000) - 5 < time.time() < os.path.getmtime(t000) + 5 assert not os.path.exists(t001) tile_image_buf = create_tmp_image_buf((256, 256), color='blue') tile_image = create_tmp_image((256, 256), color='blue') class TestSeed(SeedTestBase): seed_conf_name = 'seed.yaml' mapproxy_conf_name = 'seed_mapproxy.yaml' empty_ogrdata = 'empty_ogrdata.geojson' def test_cleanup_levels(self): seed_conf = load_seed_tasks_conf(self.seed_conf_file, self.mapproxy_conf) cleanup_tasks = seed_conf.cleanups(['cleanup']) self.make_tile((0, 0, 0)) self.make_tile((0, 0, 1)) self.make_tile((0, 0, 2)) self.make_tile((0, 0, 3)) cleanup(cleanup_tasks, verbose=False, dry_run=False) assert not self.tile_exists((0, 0, 0)) assert not self.tile_exists((0, 0, 1)) assert self.tile_exists((0, 0, 2)) assert not self.tile_exists((0, 0, 3)) eq_(sorted(os.listdir(os.path.join(self.dir, 'cache', 'one_EPSG4326'))), ['02']) def test_cleanup_remove_all(self): seed_conf = load_seed_tasks_conf(self.seed_conf_file, self.mapproxy_conf) cleanup_tasks = seed_conf.cleanups(['remove_all']) self.make_tile((0, 0, 0)) self.make_tile((0, 0, 1)) self.make_tile((1, 0, 1)) self.make_tile((0, 1, 1)) self.make_tile((1, 1, 1)) self.make_tile((0, 0, 2)) self.make_tile((0, 0, 3)) eq_(sorted(os.listdir(os.path.join(self.dir, 'cache', 'one_EPSG4326'))), ['00', '01', '02', '03']) cleanup(cleanup_tasks, verbose=False, dry_run=False) assert self.tile_exists((0, 0, 0)) assert not self.tile_exists((0, 0, 1)) assert not self.tile_exists((1, 0, 1)) assert not self.tile_exists((0, 1, 1)) assert not self.tile_exists((1, 1, 1)) assert not self.tile_exists((0, 0, 1)) assert self.tile_exists((0, 0, 2)) assert self.tile_exists((0, 0, 3)) # remove_all should remove the whole directory eq_(sorted(os.listdir(os.path.join(self.dir, 'cache', 'one_EPSG4326'))), ['00', '02', '03']) def test_cleanup_coverage(self): seed_conf = load_seed_tasks_conf(self.seed_conf_file, self.mapproxy_conf) cleanup_tasks = seed_conf.cleanups(['with_coverage']) self.make_tile((0, 0, 0)) self.make_tile((1, 0, 1)) self.make_tile((2, 0, 2)) self.make_tile((2, 0, 3)) self.make_tile((4, 0, 3)) cleanup(cleanup_tasks, verbose=False, dry_run=False) assert not self.tile_exists((0, 0, 0)) assert not self.tile_exists((1, 0, 1)) assert self.tile_exists((2, 0, 2)) assert not self.tile_exists((2, 0, 3)) assert self.tile_exists((4, 0, 3)) def test_seed_mbtile(self): with tmp_image((256, 256), format='png') as img: img_data = img.read() expected_req = ({'path': r'/service?LAYERS=bar&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&VERSION=1.1.1&bbox=-180.0,-90.0,180.0,90.0' '&width=256&height=128&srs=EPSG:4326'}, {'body': img_data, 'headers': {'content-type': 'image/png'}}) with mock_httpd(('localhost', 42423), [expected_req]): seed_conf = load_seed_tasks_conf(self.seed_conf_file, self.mapproxy_conf) tasks, cleanup_tasks = seed_conf.seeds(['mbtile_cache']), seed_conf.cleanups(['cleanup_mbtile_cache']) seed(tasks, dry_run=False) cleanup(cleanup_tasks, verbose=False, dry_run=False) def create_tile(self, coord=(0, 0, 0)): return Tile(coord, ImageSource(tile_image_buf, image_opts=ImageOptions(format='image/png'))) def test_reseed_mbtiles(self): seed_conf = load_seed_tasks_conf(self.seed_conf_file, self.mapproxy_conf) tasks, cleanup_tasks = seed_conf.seeds(['mbtile_cache']), seed_conf.cleanups(['cleanup_mbtile_cache']) cache = tasks[0].tile_manager.cache cache.store_tile(self.create_tile()) # no refresh before seed(tasks, dry_run=False) def test_reseed_mbtiles_with_refresh(self): seed_conf = load_seed_tasks_conf(self.seed_conf_file, self.mapproxy_conf) tasks, cleanup_tasks = seed_conf.seeds(['mbtile_cache_refresh']), seed_conf.cleanups(['cleanup_mbtile_cache']) cache = tasks[0].tile_manager.cache cache.store_tile(self.create_tile()) expected_req = ({'path': r'/service?LAYERS=bar&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&VERSION=1.1.1&bbox=-180.0,-90.0,180.0,90.0' '&width=256&height=128&srs=EPSG:4326'}, {'body': tile_image, 'headers': {'content-type': 'image/png'}}) with mock_httpd(('localhost', 42423), [expected_req]): # mbtiles does not support timestamps, refresh all tiles seed(tasks, dry_run=False) def test_cleanup_mbtiles(self): seed_conf = load_seed_tasks_conf(self.seed_conf_file, self.mapproxy_conf) tasks, cleanup_tasks = seed_conf.seeds(['mbtile_cache_refresh']), seed_conf.cleanups(['cleanup_mbtile_cache']) cache = tasks[0].tile_manager.cache cache.store_tile(self.create_tile()) cleanup(cleanup_tasks, verbose=False, dry_run=False) def test_cleanup_sqlite(self): seed_conf = load_seed_tasks_conf(self.seed_conf_file, self.mapproxy_conf) cleanup_tasks = seed_conf.cleanups(['sqlite_cache']) cache = cleanup_tasks[0].tile_manager.cache cache.store_tile(self.create_tile((0, 0, 2))) cache.store_tile(self.create_tile((0, 0, 3))) assert cache.is_cached(Tile((0, 0, 2))) assert cache.is_cached(Tile((0, 0, 3))) eq_(sorted(os.listdir(os.path.join(self.dir, 'cache', 'sqlite_cache', 'GLOBAL_GEODETIC'))), ['2.mbtile', '3.mbtile']) cleanup(cleanup_tasks, verbose=False, dry_run=False) # 3.mbtile file is still there eq_(sorted(os.listdir(os.path.join(self.dir, 'cache', 'sqlite_cache', 'GLOBAL_GEODETIC'))), ['2.mbtile', '3.mbtile']) assert cache.is_cached(Tile((0, 0, 2))) assert not cache.is_cached(Tile((0, 0, 3))) def test_cleanup_sqlite_remove_all(self): seed_conf = load_seed_tasks_conf(self.seed_conf_file, self.mapproxy_conf) cleanup_tasks = seed_conf.cleanups(['sqlite_cache_remove_all']) cache = cleanup_tasks[0].tile_manager.cache cache.store_tile(self.create_tile((0, 0, 2))) cache.store_tile(self.create_tile((0, 0, 3))) assert cache.is_cached(Tile((0, 0, 2))) assert cache.is_cached(Tile((0, 0, 3))) eq_(sorted(os.listdir(os.path.join(self.dir, 'cache', 'sqlite_cache', 'GLOBAL_GEODETIC'))), ['2.mbtile', '3.mbtile']) cleanup(cleanup_tasks, verbose=False, dry_run=False) # 3.mbtile file should be removed completely eq_(sorted(os.listdir(os.path.join(self.dir, 'cache', 'sqlite_cache', 'GLOBAL_GEODETIC'))), ['3.mbtile']) assert not cache.is_cached(Tile((0, 0, 2))) assert cache.is_cached(Tile((0, 0, 3))) def test_active_seed_tasks(self): with local_base_config(self.mapproxy_conf.base_config): seed_conf = load_seed_tasks_conf(self.seed_conf_file, self.mapproxy_conf) assert len(seed_conf.seed_tasks_names()) == 5 assert len(seed_conf.seeds()) == 5 def test_seed_refresh_remove_before_from_file(self): # tile already there but old t000 = self.make_tile((0, 0, 0), timestamp=time.time() - (60*60*25)) # mtime is older than tile, no create of the tile timestamp = time.time() - (60*60*30) os.utime(self.seed_conf_file, (timestamp, timestamp)) with local_base_config(self.mapproxy_conf.base_config): seed_conf = load_seed_tasks_conf(self.seed_conf_file, self.mapproxy_conf) tasks = seed_conf.seeds(['refresh_from_file']) seed(tasks, dry_run=False) # touch the seed_conf file and refresh everything os.utime(self.seed_conf_file, None) img_data = create_tmp_image((256, 256), format='png') expected_req = ({'path': r'/service?LAYERS=foo&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&VERSION=1.1.1&bbox=-180.0,-90.0,180.0,90.0' '&width=256&height=128&srs=EPSG:4326'}, {'body': img_data, 'headers': {'content-type': 'image/png'}}) with mock_httpd(('localhost', 42423), [expected_req]): # touch the seed_conf file and refresh everything timestamp = time.time() - 60 os.utime(self.seed_conf_file, (timestamp, timestamp)) with local_base_config(self.mapproxy_conf.base_config): seed_conf = load_seed_tasks_conf(self.seed_conf_file, self.mapproxy_conf) tasks = seed_conf.seeds(['refresh_from_file']) seed(tasks, dry_run=False) assert os.path.exists(t000) assert os.path.getmtime(t000) - 5 < time.time() < os.path.getmtime(t000) + 5 # mtime is older than tile, no cleanup timestamp = time.time() - 5 os.utime(self.seed_conf_file, (timestamp, timestamp)) with local_base_config(self.mapproxy_conf.base_config): seed_conf = load_seed_tasks_conf(self.seed_conf_file, self.mapproxy_conf) cleanup_tasks = seed_conf.cleanups(['remove_from_file']) cleanup(cleanup_tasks, verbose=False, dry_run=False) assert os.path.exists(t000) # now touch the seed_conf again and remove everything timestamp = time.time() + 5 os.utime(self.seed_conf_file, (timestamp, timestamp)) with local_base_config(self.mapproxy_conf.base_config): seed_conf = load_seed_tasks_conf(self.seed_conf_file, self.mapproxy_conf) cleanup_tasks = seed_conf.cleanups(['remove_from_file']) cleanup(cleanup_tasks, verbose=False, dry_run=False) assert not os.path.exists(t000) class TestConcurrentRequestsSeed(SeedTestEnvironment): seed_conf_name = 'seed_timeouts.yaml' mapproxy_conf_name = 'seed_timeouts_mapproxy.yaml' empty_ogrdata = 'empty_ogrdata.geojson' def test_timeout(self): # test concurrent seeding where seed concurrency is higher than the permitted # concurrent_request value of the source and a lock times out seed_conf = load_seed_tasks_conf(self.seed_conf_file, self.mapproxy_conf) tasks = seed_conf.seeds(['test']) expected_req1 = ({'path': r'/service?LAYERS=foo&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&VERSION=1.1.1&bbox=-180.0,-90.0,180.0,90.0' '&width=256&height=128&srs=EPSG:4326'}, {'body': tile_image, 'headers': {'content-type': 'image/png'}, 'duration': 0.1}) expected_req2 = ({'path': r'/service?LAYERS=foo&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&VERSION=1.1.1&bbox=-180.0,-90.0,180.0,90.0' '&width=512&height=256&srs=EPSG:4326'}, {'body': tile_image, 'headers': {'content-type': 'image/png'}, 'duration': 0.1}) expected_req3 = ({'path': r'/service?LAYERS=foo&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&VERSION=1.1.1&bbox=-180.0,-90.0,180.0,90.0' '&width=1024&height=512&srs=EPSG:4326'}, {'body': tile_image, 'headers': {'content-type': 'image/png'}, 'duration': 0.1}) with mock_httpd(('localhost', 42423), [expected_req1, expected_req2, expected_req3], unordered=True): seed(tasks, dry_run=False, concurrency=3) # concurrency=3, concurrent_request=1, client_timeout=0.2, response delay=0.1 # the third request should time out (3x0.1 > 0.2), but exp_backoff() in the seeder ignores this # timeout exception and tries a second time mapproxy-1.11.0/mapproxy/test/system/test_seed_only.py000066400000000000000000000056751320454472400232470ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division from io import BytesIO from mapproxy.request.wms import WMS111MapRequest from mapproxy.compat.image import Image from mapproxy.test.image import is_png, is_jpeg from mapproxy.test.system import module_setup, module_teardown, SystemTest from nose.tools import eq_ test_config = {} def setup_module(): module_setup(test_config, 'seedonly.yaml', with_cache_data=True) def teardown_module(): module_teardown(test_config) class TestSeedOnlyWMS(SystemTest): config = test_config def setup(self): SystemTest.setup(self) self.common_map_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1', bbox='-180,0,0,80', width='200', height='200', layers='wms_cache', srs='EPSG:4326', format='image/png', styles='', request='GetMap', transparent=True)) def test_get_map_cached(self): resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) img = Image.open(data) eq_(img.mode, 'RGB') # cached image has more that 256 colors, getcolors -> None eq_(img.getcolors(), None) def test_get_map_uncached(self): self.common_map_req.params['bbox'] = '10,10,20,20' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) img = Image.open(data) eq_(img.mode, 'RGBA') eq_(img.getcolors(), [(200*200, (255, 255, 255, 0))]) class TestSeedOnlyTMS(SystemTest): config = test_config def test_get_tile_cached(self): resp = self.app.get('/tms/1.0.0/wms_cache/0/0/1.jpeg') eq_(resp.content_type, 'image/jpeg') data = BytesIO(resp.body) assert is_jpeg(data) img = Image.open(data) eq_(img.mode, 'RGB') # cached image has more that 256 colors, getcolors -> None eq_(img.getcolors(), None) def test_get_tile_uncached(self): resp = self.app.get('/tms/1.0.0/wms_cache/0/0/0.jpeg') eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) img = Image.open(data) eq_(img.mode, 'RGBA') eq_(img.getcolors(), [(256*256, (255, 255, 255, 0))])mapproxy-1.11.0/mapproxy/test/system/test_sld.py000066400000000000000000000063331320454472400220400ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import os import tempfile try: from urllib.parse import quote except ImportError: from urllib import quote from mapproxy.request.wms import WMS111MapRequest from mapproxy.test.system import module_setup, module_teardown, make_base_config, SystemTest from mapproxy.test.http import mock_httpd from mapproxy.test.image import create_tmp_image from nose.tools import eq_ test_config = {} def setup_module(): test_config['base_dir'] = tempfile.mkdtemp() with open(os.path.join(test_config['base_dir'], 'mysld.xml'), 'wb') as f: f.write(b'') module_setup(test_config, 'sld.yaml') def teardown_module(): module_teardown(test_config) base_config = make_base_config(test_config) TESTSERVER_ADDRESS = 'localhost', 42423 class TestWMS(SystemTest): config = test_config def setup(self): SystemTest.setup(self) self.common_map_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1', bbox='0,0,10,10', width='200', height='200', srs='EPSG:4326', format='image/png', styles='', request='GetMap', exceptions='xml')) self.common_wms_url = ("/service?styles=&srs=EPSG%3A4326&version=1.1.1&" "bbox=0.0,0.0,10.0,10.0&service=WMS&format=image%2Fpng&request=GetMap" "&width=200&height=200") def test_sld_url(self): self.common_map_req.params['layers'] = 'sld_url' with mock_httpd(TESTSERVER_ADDRESS, [ ({'path': self.common_wms_url + '&sld=' +quote('http://example.org/sld.xml'), 'method': 'GET'}, {'body': create_tmp_image((200, 200), format='png')} )]): resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') def test_sld_file(self): self.common_map_req.params['layers'] = 'sld_file' with mock_httpd(TESTSERVER_ADDRESS, [ ({'path': self.common_wms_url + '&sld_body=' +quote(''), 'method': 'GET'}, {'body': create_tmp_image((200, 200), format='png')} )]): resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') def test_sld_body(self): self.common_map_req.params['layers'] = 'sld_body' with mock_httpd(TESTSERVER_ADDRESS, [ ({'path': self.common_wms_url + '&sld_body=' +quote(''), 'method': 'POST'}, {'body': create_tmp_image((200, 200), format='png')} )]): resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') mapproxy-1.11.0/mapproxy/test/system/test_source_errors.py000066400000000000000000000267421320454472400241600ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010-2014 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import os from mapproxy.request.wms import WMS111MapRequest from mapproxy.test.image import is_transparent, create_tmp_image, bgcolor_ratio, img_from_buf, assert_colors_equal from mapproxy.test.http import mock_httpd from mapproxy.test.system import module_setup, module_teardown, SystemTest from mapproxy.test.system.test_wms import is_111_exception from nose.tools import eq_ test_config = {} test_config_raise = {} def setup_module(): module_setup(test_config, 'source_errors.yaml') module_setup(test_config_raise, 'source_errors_raise.yaml') def teardown_module(): module_teardown(test_config) module_teardown(test_config_raise) transp = create_tmp_image((200, 200), mode='RGBA', color=(0, 0, 0, 0)) class TestWMS(SystemTest): config = test_config def setup(self): SystemTest.setup(self) self.common_map_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1', bbox='9,50,10,51', width='200', height='200', layers='online', srs='EPSG:4326', format='image/png', styles='', request='GetMap', transparent=True)) def test_online(self): common_params = (r'?SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=200&SRS=EPSG%3A4326&styles=' '&VERSION=1.1.1&BBOX=9.0,50.0,10.0,51.0' '&WIDTH=200&transparent=True') expected_req = [({'path': '/service_a' + common_params + '&layers=a_one'}, {'body': transp, 'headers': {'content-type': 'image/png'}}), ] with mock_httpd(('localhost', 42423), expected_req): self.common_map_req.params.layers = 'online' resp = self.app.get(self.common_map_req) assert 'Cache-Control' not in resp.headers eq_(resp.content_type, 'image/png') assert is_transparent(resp.body) def test_mixed_layer_source(self): common_params = (r'?SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=200&SRS=EPSG%3A4326&styles=' '&VERSION=1.1.1&BBOX=9.0,50.0,10.0,51.0' '&WIDTH=200&transparent=True') expected_req = [({'path': '/service_a' + common_params + '&layers=a_one'}, {'body': transp, 'headers': {'content-type': 'image/png'}}), ] with mock_httpd(('localhost', 42423), expected_req): self.common_map_req.params.layers = 'mixed' resp = self.app.get(self.common_map_req) assert_no_cache(resp) eq_(resp.content_type, 'image/png') assert 0.99 > bgcolor_ratio(resp.body) > 0.95 def test_mixed_sources(self): common_params = (r'?SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=200&SRS=EPSG%3A4326&styles=' '&VERSION=1.1.1&BBOX=9.0,50.0,10.0,51.0' '&WIDTH=200&transparent=True') expected_req = [({'path': '/service_a' + common_params + '&layers=a_one'}, {'body': transp, 'headers': {'content-type': 'image/png'}}), ] with mock_httpd(('localhost', 42423), expected_req): self.common_map_req.params.layers = 'online,all_offline' resp = self.app.get(self.common_map_req) assert_no_cache(resp) eq_(resp.content_type, 'image/png') assert 0.99 > bgcolor_ratio(resp.body) > 0.95 # open('/tmp/foo.png', 'wb').write(resp.body) def test_all_offline(self): self.common_map_req.params.layers = 'all_offline' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'application/vnd.ogc.se_xml') is_111_exception(resp.lxml, re_msg='no response from url') class TestWMSRaise(SystemTest): config = test_config_raise def setup(self): SystemTest.setup(self) self.common_map_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1', bbox='9,50,10,51', width='200', height='200', layers='online', srs='EPSG:4326', format='image/png', styles='', request='GetMap', transparent=True)) def test_mixed_layer_source(self): common_params = (r'?SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=200&SRS=EPSG%3A4326&styles=' '&VERSION=1.1.1&BBOX=9.0,50.0,10.0,51.0' '&WIDTH=200&transparent=True') expected_req = [({'path': '/service_a' + common_params + '&layers=a_one'}, {'body': transp, 'headers': {'content-type': 'image/png'}}), ] with mock_httpd(('localhost', 42423), expected_req): self.common_map_req.params.layers = 'mixed' resp = self.app.get(self.common_map_req) is_111_exception(resp.lxml, re_msg='no response from url') def test_all_offline(self): self.common_map_req.params.layers = 'all_offline' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'application/vnd.ogc.se_xml') is_111_exception(resp.lxml, re_msg='no response from url') class TestTileErrors(SystemTest): config = test_config def setup(self): SystemTest.setup(self) self.common_map_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1', bbox='0,-90,180,90', width='250', height='250', layers='tilesource', srs='EPSG:4326', format='image/png', styles='', request='GetMap', transparent=True)) self.common_tile_req = '/tiles/tilesource/EPSG4326/1/1/0.png' def test_wms_uncached_response(self): expected_req = [({'path': '/foo/1/1/0.png'}, {'body': b'not found', 'status': 404, 'headers': {'content-type': 'text/plain'}}), ] with mock_httpd(('localhost', 42423), expected_req): resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') assert_no_cache(resp) img = img_from_buf(resp.body) eq_(img.getcolors(), [(250 * 250, (255, 0, 128))]) assert not os.path.exists(os.path.join(self.base_config().cache.base_dir, 'tilesource_cache_EPSG4326/01/000/000/001/000/000/000.png')) def test_wms_cached_response(self): expected_req = [({'path': '/foo/1/1/0.png'}, {'body': b'no content', 'status': 204, 'headers': {'content-type': 'text/plain'}}), ] with mock_httpd(('localhost', 42423), expected_req): resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') assert 'Cache-Control' not in resp.headers img = img_from_buf(resp.body) assert_colors_equal(img, [(250 * 250, (100, 200, 50, 250))]) self.created_tiles.append('tilesource_cache_EPSG4326/01/000/000/001/000/000/000.png') def test_wms_unhandled_error_code(self): expected_req = [({'path': '/foo/1/1/0.png'}, {'body': b'error', 'status': 500, 'headers': {'content-type': 'text/plain'}}), ] with mock_httpd(('localhost', 42423), expected_req): resp = self.app.get(self.common_map_req) assert 'Cache-Control' not in resp.headers eq_(resp.content_type, 'application/vnd.ogc.se_xml') assert b'500' in resp.body def test_wms_catchall_error_no_image_response(self): expected_req = [({'path': '/foo/1/1/0.png'}, {'body': b'error', 'status': 200, 'headers': {'content-type': 'text/plain'}}), ] with mock_httpd(('localhost', 42423), expected_req): self.common_map_req.params['layers'] = 'tilesource_catchall' resp = self.app.get(self.common_map_req) assert_no_cache(resp) eq_(resp.content_type, 'image/png') img = img_from_buf(resp.body) eq_(img.getcolors(), [(250 * 250, (100, 50, 50))]) def test_tile_uncached_response(self): expected_req = [({'path': '/foo/1/1/0.png'}, {'body': b'not found', 'status': 404, 'headers': {'content-type': 'text/plain'}}), ] with mock_httpd(('localhost', 42423), expected_req): resp = self.app.get(self.common_tile_req) assert_no_cache(resp) eq_(resp.content_type, 'image/png') img = img_from_buf(resp.body) eq_(img.getcolors(), [(256 * 256, (255, 0, 128))]) assert not os.path.exists(os.path.join(self.base_config().cache.base_dir, 'tilesource_cache_EPSG4326/01/000/000/001/000/000/000.png')) def test_tile_cached_response(self): expected_req = [({'path': '/foo/1/1/0.png'}, {'body': b'no content', 'status': 204, 'headers': {'content-type': 'text/plain'}}), ] with mock_httpd(('localhost', 42423), expected_req): resp = self.app.get(self.common_tile_req) assert 'public' in resp.headers['Cache-Control'] eq_(resp.content_type, 'image/png') img = img_from_buf(resp.body) eq_(img.getcolors(), [(256 * 256, (100, 200, 50, 250))]) self.created_tiles.append('tilesource_cache_EPSG4326/01/000/000/001/000/000/000.png') def test_tile_unhandled_error_code(self): expected_req = [({'path': '/foo/1/1/0.png'}, {'body': b'error', 'status': 500, 'headers': {'content-type': 'text/plain'}}), ] with mock_httpd(('localhost', 42423), expected_req): resp = self.app.get(self.common_tile_req, status=500) assert 'Cache-Control' not in resp.headers # no assert_no_cache(resp): returns XML exception that bypasses cache control setting eq_(resp.content_type, 'text/plain') assert b'500' in resp.body def test_tile_catchall_error_no_image_response(self): expected_req = [({'path': '/foo/1/1/0.png'}, {'body': b'error', 'status': 200, 'headers': {'content-type': 'text/plain'}}), ] with mock_httpd(('localhost', 42423), expected_req): resp = self.app.get(self.common_tile_req.replace('tilesource', 'tilesource_catchall')) assert_no_cache(resp) eq_(resp.content_type, 'image/png') img = img_from_buf(resp.body) eq_(img.getcolors(), [(256 * 256, (100, 50, 50))]) def assert_no_cache(resp): eq_(resp.headers['Pragma'], 'no-cache') eq_(resp.headers['Expires'], '-1') eq_(resp.cache_control.no_store, True) mapproxy-1.11.0/mapproxy/test/system/test_tilesource_minmax_res.py000066400000000000000000000042511320454472400256530ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2012 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.test.image import tmp_image from mapproxy.test.http import mock_httpd from mapproxy.test.system import module_setup, module_teardown, SystemTest, make_base_config from nose.tools import eq_ test_config = {} base_config = make_base_config(test_config) def setup_module(): module_setup(test_config, 'tilesource_minmax_res.yaml') def teardown_module(): module_teardown(test_config) class TestTileSourceMinMaxRes(SystemTest): config = test_config def test_get_tile_res_a(self): with tmp_image((256, 256), format='jpeg') as img: expected_req = ({'path': r'/tiles_a/06/000/000/000/000/000/001.png'}, {'body': img.read(), 'headers': {'content-type': 'image/png'}}) with mock_httpd(('localhost', 42423), [expected_req]): resp = self.app.get('/tiles/tms_cache/6/0/1.png') eq_(resp.content_type, 'image/png') self.created_tiles.append('tms_cache_EPSG900913/06/000/000/000/000/000/001.png') def test_get_tile_res_b(self): with tmp_image((256, 256), format='jpeg') as img: expected_req = ({'path': r'/tiles_b/07/000/000/000/000/000/001.png'}, {'body': img.read(), 'headers': {'content-type': 'image/png'}}) with mock_httpd(('localhost', 42423), [expected_req]): resp = self.app.get('/tiles/tms_cache/7/0/1.png') eq_(resp.content_type, 'image/png') self.created_tiles.append('tms_cache_EPSG900913/07/000/000/000/000/000/001.png') mapproxy-1.11.0/mapproxy/test/system/test_tms.py000066400000000000000000000252631320454472400220640ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010-2012 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import hashlib from io import BytesIO from mapproxy.compat.image import Image from mapproxy.test.image import is_jpeg, tmp_image from mapproxy.test.http import mock_httpd from mapproxy.test.system import module_setup, module_teardown, SystemTest, make_base_config from nose.tools import eq_ test_config = {} base_config = make_base_config(test_config) def setup_module(): module_setup(test_config, 'layer.yaml', with_cache_data=True) def teardown_module(): module_teardown(test_config) class TestTMS(SystemTest): config = test_config def test_tms_capabilities(self): resp = self.app.get('/tms/1.0.0/') assert 'WMS Cache Layer' in resp assert 'WMS Cache Multi Layer' in resp assert 'TMS Cache Layer' in resp assert 'TMS Cache Layer + FI' in resp xml = resp.lxml assert xml.xpath('count(//TileMap)') == 11 # without trailing space resp2 = self.app.get('/tms/1.0.0') eq_(resp.body, resp2.body) def test_tms_layer_capabilities(self): resp = self.app.get('/tms/1.0.0/wms_cache') assert 'WMS Cache Layer' in resp xml = resp.lxml eq_(xml.xpath('count(//TileSet)'), 19) def test_tms_root_resource(self): resp = self.app.get('/tms') resp2 = self.app.get('/tms/') assert 'TileMapService' in resp and 'TileMapService' in resp2 xml = resp.lxml eq_(xml.xpath('//TileMapService/@version'),['1.0.0']) def test_tms_get_out_of_bounds_tile(self): for coord in [(0, 0, -1), (-1, 0, 0), (0, -1, 0), (4, 2, 1), (1, 3, 0)]: yield self.check_out_of_bounds, coord def check_out_of_bounds(self, coord): x, y, z = coord url = '/tms/1.0.0/wms_cache/%d/%d/%d.jpeg' % (z, x, y) resp = self.app.get(url , status=404) xml = resp.lxml assert ('outside the bounding box' in xml.xpath('/TileMapServerError/Message/text()')[0]) def test_invalid_layer(self): resp = self.app.get('/tms/1.0.0/inVAlid/0/0/0.png', status=404) xml = resp.lxml assert ('unknown layer: inVAlid' in xml.xpath('/TileMapServerError/Message/text()')[0]) def test_invalid_format(self): resp = self.app.get('/tms/1.0.0/wms_cache/0/0/1.png', status=404) xml = resp.lxml assert ('invalid format' in xml.xpath('/TileMapServerError/Message/text()')[0]) def test_get_tile_tile_source_error(self): resp = self.app.get('/tms/1.0.0/wms_cache/0/0/0.jpeg', status=500) xml = resp.lxml assert ('No response from URL' in xml.xpath('/TileMapServerError/Message/text()')[0]) def test_get_cached_tile(self): resp = self.app.get('/tms/1.0.0/wms_cache/0/0/1.jpeg') eq_(resp.content_type, 'image/jpeg') eq_(resp.content_length, len(resp.body)) data = BytesIO(resp.body) assert is_jpeg(data) def test_get_tile(self): with tmp_image((256, 256), format='jpeg') as img: expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fjpeg' '&REQUEST=GetMap&HEIGHT=256&SRS=EPSG%3A900913&styles=' '&VERSION=1.1.1&BBOX=-20037508.3428,-20037508.3428,0.0,0.0' '&WIDTH=256'}, {'body': img.read(), 'headers': {'content-type': 'image/jpeg'}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): resp = self.app.get('/tms/1.0.0/wms_cache/0/0/0.jpeg') eq_(resp.content_type, 'image/jpeg') self.created_tiles.append('wms_cache_EPSG900913/01/000/000/000/000/000/000.jpeg') def test_get_tile_from_cache_with_tile_source(self): with tmp_image((256, 256), format='jpeg') as img: expected_req = ({'path': r'/tiles/01/000/000/000/000/000/001.png'}, {'body': img.read(), 'headers': {'content-type': 'image/png'}}) with mock_httpd(('localhost', 42423), [expected_req]): resp = self.app.get('/tms/1.0.0/tms_cache/0/0/1.png') eq_(resp.content_type, 'image/png') self.created_tiles.append('tms_cache_EPSG900913/01/000/000/000/000/000/001.png') def test_get_tile_with_watermark_cache(self): with tmp_image((256, 256), format='png', color=(0, 0, 0)) as img: expected_req = ({'path': r'/tiles/01/000/000/000/000/000/000.png'}, {'body': img.read(), 'headers': {'content-type': 'image/png'}}) with mock_httpd(('localhost', 42423), [expected_req]): resp = self.app.get('/tms/1.0.0/watermark_cache/0/0/0.png') eq_(resp.content_type, 'image/png') img = Image.open(BytesIO(resp.body)) colors = img.getcolors() assert len(colors) >= 2 eq_(sorted(colors)[-1][1], (0, 0, 0)) class TestTileService(SystemTest): config = test_config def test_get_out_of_bounds_tile(self): for coord in [(0, 0, -1), (-1, 0, 0), (0, -1, 0), (4, 2, 1), (1, 3, 0)]: yield self.check_out_of_bounds, coord def check_out_of_bounds(self, coord): x, y, z = coord url = '/tiles/wms_cache/%d/%d/%d.jpeg' % (z, x, y) resp = self.app.get(url , status=404) assert 'outside the bounding box' in resp def test_invalid_layer(self): resp = self.app.get('/tiles/inVAlid/0/0/0.png', status=404) eq_(resp.content_type, 'text/plain') assert 'unknown layer: inVAlid' in resp def test_invalid_format(self): resp = self.app.get('/tiles/wms_cache/0/0/1.png', status=404) eq_(resp.content_type, 'text/plain') assert 'invalid format' in resp def test_get_tile_tile_source_error(self): resp = self.app.get('/tiles/wms_cache/0/0/0.jpeg', status=500) eq_(resp.content_type, 'text/plain') assert 'No response from URL' in resp def _check_tile_resp(self, resp): eq_(resp.content_type, 'image/jpeg') eq_(resp.content_length, len(resp.body)) data = BytesIO(resp.body) assert is_jpeg(data) def _update_timestamp(self): timestamp = 1234567890.0 size = 10214 base_dir = base_config().cache.base_dir os.utime(os.path.join(base_dir, 'wms_cache_EPSG900913/01/000/000/000/000/000/001.jpeg'), (timestamp, timestamp)) max_age = base_config().tiles.expires_hours * 60 * 60 etag = hashlib.md5((str(timestamp) + str(size)).encode('ascii')).hexdigest() return etag, max_age def _check_cache_control_headers(self, resp, etag, max_age): eq_(resp.headers['ETag'], etag) eq_(resp.headers['Last-modified'], 'Fri, 13 Feb 2009 23:31:30 GMT') eq_(resp.headers['Cache-control'], 'public, max-age=%d, s-maxage=%d' % (max_age, max_age)) def test_get_cached_tile(self): etag, max_age = self._update_timestamp() resp = self.app.get('/tiles/wms_cache/1/0/1.jpeg') self._check_cache_control_headers(resp, etag, max_age) self._check_tile_resp(resp) def test_get_cached_tile_flipped_y(self): etag, max_age = self._update_timestamp() resp = self.app.get('/tiles/wms_cache/1/0/0.jpeg?origin=nw') self._check_cache_control_headers(resp, etag, max_age) self._check_tile_resp(resp) def test_if_none_match(self): etag, max_age = self._update_timestamp() resp = self.app.get('/tiles/wms_cache/1/0/1.jpeg', headers={'If-None-Match': etag}) eq_(resp.status, '304 Not Modified') self._check_cache_control_headers(resp, etag, max_age) resp = self.app.get('/tiles/wms_cache/1/0/1.jpeg', headers={'If-None-Match': etag + 'foo'}) self._check_cache_control_headers(resp, etag, max_age) eq_(resp.status, '200 OK') self._check_tile_resp(resp) def test_if_modified_since(self): etag, max_age = self._update_timestamp() for date, modified in ( ('Fri, 15 Feb 2009 23:31:30 GMT', False), ('Fri, 13 Feb 2009 23:31:31 GMT', False), ('Fri, 13 Feb 2009 23:31:30 GMT', False), ('Fri, 13 Feb 2009 23:31:29 GMT', True), ('Fri, 11 Feb 2009 23:31:29 GMT', True), ('Friday, 13-Feb-09 23:31:30 GMT', False), ('Friday, 13-Feb-09 23:31:29 GMT', True), ('Fri Feb 13 23:31:30 2009', False), ('Fri Feb 13 23:31:29 2009', True), # and some invalid ones ('Fri Foo 13 23:31:29 2009', True), ('1234567890', True), ): yield self.check_modified_response, date, modified, etag, max_age def check_modified_response(self, date, modified, etag, max_age): resp = self.app.get('/tiles/wms_cache/1/0/1.jpeg', headers={ 'If-Modified-Since': date}) self._check_cache_control_headers(resp, etag, max_age) if modified: eq_(resp.status, '200 OK') self._check_tile_resp(resp) else: eq_(resp.status, '304 Not Modified') def test_get_tile(self): with tmp_image((256, 256), format='jpeg') as img: expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fjpeg' '&REQUEST=GetMap&HEIGHT=256&SRS=EPSG%3A900913&styles=' '&VERSION=1.1.1&BBOX=-20037508.3428,-20037508.3428,0.0,0.0' '&WIDTH=256'}, {'body': img.read(), 'headers': {'content-type': 'image/jpeg'}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): resp = self.app.get('/tiles/wms_cache/1/0/0.jpeg') eq_(resp.content_type, 'image/jpeg') self.created_tiles.append('wms_cache_EPSG900913/01/000/000/000/000/000/000.jpeg') mapproxy-1.11.0/mapproxy/test/system/test_tms_origin.py000066400000000000000000000033371320454472400234310ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010-2012 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.test.image import is_jpeg from mapproxy.test.system import module_setup, module_teardown, SystemTest, make_base_config from nose.tools import eq_ test_config = {} base_config = make_base_config(test_config) def setup_module(): module_setup(test_config, 'tileservice_origin.yaml', with_cache_data=True) def teardown_module(): module_teardown(test_config) class TestTileServicesOrigin(SystemTest): config = test_config ### # tile 0/0/1 is cached, check if we can access it with different URLs def test_get_cached_tile_tms(self): resp = self.app.get('/tms/1.0.0/wms_cache/0/0/1.jpeg') eq_(resp.content_type, 'image/jpeg') assert is_jpeg(resp.body) def test_get_cached_tile_service_origin(self): resp = self.app.get('/tiles/wms_cache/1/0/0.jpeg') eq_(resp.content_type, 'image/jpeg') assert is_jpeg(resp.body) def test_get_cached_tile_request_origin(self): resp = self.app.get('/tiles/wms_cache/1/0/1.jpeg?origin=sw') eq_(resp.content_type, 'image/jpeg') assert is_jpeg(resp.body) mapproxy-1.11.0/mapproxy/test/system/test_util_conf.py000066400000000000000000000200611320454472400232320ustar00rootroot00000000000000# -:- encoding: utf-8 -:- # This file is part of the MapProxy project. # Copyright (C) 2013 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import shutil import tempfile import yaml from mapproxy.script.conf.app import config_command from mapproxy.test.helper import capture from nose.tools import eq_ def filename(name): return os.path.join( os.path.dirname(__file__), 'fixture', name, ) class TestMapProxyConfCmd(object): def setup(self): self.dir = tempfile.mkdtemp() def teardown(self): if os.path.exists(self.dir): shutil.rmtree(self.dir) def tmp_filename(self, name): return os.path.join( self.dir, name, ) def test_cmd_no_args(self): with capture() as (stdout, stderr): assert config_command(['mapproxy-conf']) == 2 assert '--capabilities required' in stderr.getvalue() def test_stdout_output(self): with capture(bytes=True) as (stdout, stderr): assert config_command(['mapproxy-conf', '--capabilities', filename('util-conf-wms-111-cap.xml')]) == 0 assert stdout.getvalue().startswith(b'# MapProxy configuration') def test_test_cap_output_no_base(self): with capture(bytes=True) as (stdout, stderr): assert config_command(['mapproxy-conf', '--capabilities', filename('util-conf-wms-111-cap.xml'), '--output', self.tmp_filename('mapproxy.yaml'), ]) == 0 with open(self.tmp_filename('mapproxy.yaml'), 'rb') as f: conf = yaml.load(f) assert 'grids' not in conf eq_(conf['sources'], { 'osm_roads_wms': { 'supported_srs': ['CRS:84', 'EPSG:25831', 'EPSG:25832', 'EPSG:25833', 'EPSG:31466', 'EPSG:31467', 'EPSG:31468', 'EPSG:3857', 'EPSG:4258', 'EPSG:4326', 'EPSG:900913'], 'req': {'layers': 'osm_roads', 'url': 'http://osm.omniscale.net/proxy/service?', 'transparent': True}, 'type': 'wms', 'coverage': {'srs': 'EPSG:4326', 'bbox': [-180.0, -85.0511287798, 180.0, 85.0511287798]} }, 'osm_wms': { 'supported_srs': ['CRS:84', 'EPSG:25831', 'EPSG:25832', 'EPSG:25833', 'EPSG:31466', 'EPSG:31467', 'EPSG:31468', 'EPSG:3857', 'EPSG:4258', 'EPSG:4326', 'EPSG:900913'], 'req': {'layers': 'osm', 'url': 'http://osm.omniscale.net/proxy/service?', 'transparent': True}, 'type': 'wms', 'coverage': { 'srs': 'EPSG:4326', 'bbox': [-180.0, -85.0511287798, 180.0, 85.0511287798], }, }, }) eq_(conf['layers'], [{ 'title': 'Omniscale OpenStreetMap WMS', 'layers': [ { 'name': 'osm', 'title': 'OpenStreetMap (complete map)', 'sources': ['osm_wms'], }, { 'name': 'osm_roads', 'title': 'OpenStreetMap (streets only)', 'sources': ['osm_roads_wms'], }, ] }]) eq_(len(conf['layers'][0]['layers']), 2) def test_test_cap_output(self): with capture(bytes=True) as (stdout, stderr): assert config_command(['mapproxy-conf', '--capabilities', filename('util-conf-wms-111-cap.xml'), '--output', self.tmp_filename('mapproxy.yaml'), '--base', filename('util-conf-base-grids.yaml'), ]) == 0 with open(self.tmp_filename('mapproxy.yaml'), 'rb') as f: conf = yaml.load(f) assert 'grids' not in conf eq_(len(conf['sources']), 2) eq_(conf['caches'], { 'osm_cache': { 'grids': ['webmercator', 'geodetic'], 'sources': ['osm_wms'] }, 'osm_roads_cache': { 'grids': ['webmercator', 'geodetic'], 'sources': ['osm_roads_wms'] }, }) eq_(conf['layers'], [{ 'title': 'Omniscale OpenStreetMap WMS', 'layers': [ { 'name': 'osm', 'title': 'OpenStreetMap (complete map)', 'sources': ['osm_cache'], }, { 'name': 'osm_roads', 'title': 'OpenStreetMap (streets only)', 'sources': ['osm_roads_cache'], }, ] }]) eq_(len(conf['layers'][0]['layers']), 2) def test_overwrites(self): with capture(bytes=True) as (stdout, stderr): assert config_command(['mapproxy-conf', '--capabilities', filename('util-conf-wms-111-cap.xml'), '--output', self.tmp_filename('mapproxy.yaml'), '--overwrite', filename('util-conf-overwrite.yaml'), '--base', filename('util-conf-base-grids.yaml'), ]) == 0 with open(self.tmp_filename('mapproxy.yaml'), 'rb') as f: conf = yaml.load(f) assert 'grids' not in conf eq_(len(conf['sources']), 2) eq_(conf['sources'], { 'osm_roads_wms': { 'supported_srs': ['EPSG:3857'], 'req': {'layers': 'osm_roads', 'url': 'http://osm.omniscale.net/proxy/service?', 'transparent': True, 'param': 42}, 'type': 'wms', 'coverage': {'srs': 'EPSG:4326', 'bbox': [0, 0, 90, 90]} }, 'osm_wms': { 'supported_srs': ['CRS:84', 'EPSG:25831', 'EPSG:25832', 'EPSG:25833', 'EPSG:31466', 'EPSG:31467', 'EPSG:31468', 'EPSG:3857', 'EPSG:4258', 'EPSG:4326', 'EPSG:900913'], 'req': {'layers': 'osm', 'url': 'http://osm.omniscale.net/proxy/service?', 'transparent': True, 'param': 42}, 'type': 'wms', 'coverage': { 'srs': 'EPSG:4326', 'bbox': [-180.0, -85.0511287798, 180.0, 85.0511287798], }, }, }) eq_(conf['caches'], { 'osm_cache': { 'grids': ['webmercator', 'geodetic'], 'sources': ['osm_wms'], 'cache': { 'type': 'sqlite' }, }, 'osm_roads_cache': { 'grids': ['webmercator'], 'sources': ['osm_roads_wms'], 'cache': { 'type': 'sqlite' }, }, }) eq_(conf['layers'], [{ 'title': 'Omniscale OpenStreetMap WMS', 'layers': [ { 'name': 'osm', 'title': 'OpenStreetMap (complete map)', 'sources': ['osm_cache'], }, { 'name': 'osm_roads', 'title': 'OpenStreetMap (streets only)', 'sources': ['osm_roads_cache'], }, ] }]) eq_(len(conf['layers'][0]['layers']), 2) mapproxy-1.11.0/mapproxy/test/system/test_util_export.py000066400000000000000000000127701320454472400236360ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2012 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import tempfile import shutil import contextlib from nose.tools import eq_, assert_raises from mapproxy.script.export import export_command from mapproxy.test.image import tmp_image from mapproxy.test.http import mock_httpd from mapproxy.test.helper import capture FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'fixture') @contextlib.contextmanager def tile_server(tile_coords): with tmp_image((256, 256), format='jpeg') as img: img = img.read() expected_reqs = [] for tile in tile_coords: expected_reqs.append( ({'path': r'/tiles/%d/%d/%d.png' % (tile[2], tile[0], tile[1])}, {'body': img, 'headers': {'content-type': 'image/png'}})) with mock_httpd(('localhost', 42423), expected_reqs, unordered=True): yield class TestUtilExport(object): def setup(self): self.dir = tempfile.mkdtemp() self.dest = os.path.join(self.dir, 'dest') self.mapproxy_conf_name = 'mapproxy_export.yaml' shutil.copy(os.path.join(FIXTURE_DIR, self.mapproxy_conf_name), self.dir) self.mapproxy_conf_file = os.path.join(self.dir, self.mapproxy_conf_name) self.args = ['command_dummy', '-f', self.mapproxy_conf_file] def teardown(self): shutil.rmtree(self.dir) def test_config_not_found(self): self.args = ['command_dummy', '-f', 'foo.bar'] with capture() as (out, err): try: export_command(self.args) except SystemExit as ex: assert ex.code != 0 else: assert False, 'export command did not exit' assert err.getvalue().startswith("ERROR:") def test_no_fetch_missing_tiles(self): self.args += ['--grid', 'GLOBAL_MERCATOR', '--dest', self.dest, '--levels', '0', '--source', 'tms_cache'] with capture() as (out, err): export_command(self.args) eq_(os.listdir(self.dest), ['tile_locks']) def test_fetch_missing_tiles(self): self.args += ['--grid', 'GLOBAL_MERCATOR', '--dest', self.dest, '--levels', '0,1', '--source', 'tms_cache', '--fetch-missing-tiles'] with tile_server([(0, 0, 0), (0, 0, 1), (0, 1, 1), (1, 0, 1), (1, 1, 1)]): with capture() as (out, err): export_command(self.args) assert os.path.exists(os.path.join(self.dest, 'tile_locks')) assert os.path.exists(os.path.join(self.dest, '0', '0', '0.png')) assert os.path.exists(os.path.join(self.dest, '1', '0', '0.png')) assert os.path.exists(os.path.join(self.dest, '1', '0', '1.png')) assert os.path.exists(os.path.join(self.dest, '1', '1', '0.png')) assert os.path.exists(os.path.join(self.dest, '1', '1', '1.png')) def test_force(self): self.args += ['--grid', 'GLOBAL_MERCATOR', '--dest', self.dest, '--levels', '0', '--source', 'tms_cache'] with capture() as (out, err): export_command(self.args) with capture() as (out, err): assert_raises(SystemExit, export_command, self.args) with capture() as (out, err): export_command(self.args + ['--force']) def test_invalid_grid_definition(self): self.args += ['--grid', 'foo=1', '--dest', self.dest, '--levels', '0', '--source', 'tms_cache'] with capture() as (out, err): assert_raises(SystemExit, export_command, self.args) assert 'foo' in err.getvalue() def test_custom_grid(self): self.args += ['--grid', 'base=GLOBAL_MERCATOR min_res=100000', '--dest', self.dest, '--levels', '1', '--source', 'tms_cache', '--fetch-missing-tiles'] with tile_server([(0, 3, 2), (1, 3, 2), (2, 3, 2), (3, 3, 2), (0, 2, 2), (1, 2, 2), (2, 2, 2), (3, 2, 2), (0, 1, 2), (1, 1, 2), (2, 1, 2), (3, 1, 2), (0, 0, 2), (1, 0, 2), (2, 0, 2), (3, 0, 2)]): with capture() as (out, err): export_command(self.args) assert os.path.exists(os.path.join(self.dest, 'tile_locks')) assert os.path.exists(os.path.join(self.dest, '1', '0', '0.png')) assert os.path.exists(os.path.join(self.dest, '1', '3', '3.png')) def test_coverage(self): self.args += ['--grid', 'GLOBAL_MERCATOR', '--dest', self.dest, '--levels', '0..2', '--source', 'tms_cache', '--fetch-missing-tiles', '--coverage', '10,10,20,20', '--srs', 'EPSG:4326'] with tile_server([(0, 0, 0), (1, 1, 1), (2, 2, 2)]): with capture() as (out, err): export_command(self.args) assert os.path.exists(os.path.join(self.dest, 'tile_locks')) assert os.path.exists(os.path.join(self.dest, '0', '0', '0.png')) assert os.path.exists(os.path.join(self.dest, '1', '1', '1.png')) assert os.path.exists(os.path.join(self.dest, '2', '2', '2.png')) mapproxy-1.11.0/mapproxy/test/system/test_util_grids.py000066400000000000000000000061441320454472400234230ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os from nose.tools import assert_raises from mapproxy.script.grids import grids_command from mapproxy.test.helper import capture FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'fixture') GRID_NAMES = [ 'global_geodetic_sqrt2', 'grid_full_example', 'another_grid_full_example' ] UNUSED_GRID_NAMES = [ 'GLOBAL_GEODETIC', 'GLOBAL_MERCATOR', 'GLOBAL_WEBMERCATOR', ] class TestUtilGrids(object): def setup(self): self.mapproxy_config_file = os.path.join(FIXTURE_DIR, 'util_grids.yaml') self.args = ['command_dummy', '-f', self.mapproxy_config_file] def test_config_not_found(self): self.args = ['command_dummy', '-f', 'foo.bar'] with capture() as (_, err): assert_raises(SystemExit, grids_command, self.args) assert err.getvalue().startswith("ERROR:") def test_list_configured(self): self.args.append('-l') with capture() as (out, err): grids_command(self.args) captured_output = out.getvalue() for grid in GRID_NAMES: assert grid in captured_output number_of_lines = sum(1 for line in captured_output.split('\n') if line) assert number_of_lines == len(GRID_NAMES) def test_list_configured_all(self): self.args.append('-l') self.args.append('--all') with capture() as (out, err): grids_command(self.args) captured_output = out.getvalue() for grid in GRID_NAMES + UNUSED_GRID_NAMES: assert grid in captured_output number_of_lines = sum(1 for line in captured_output.split('\n') if line) assert number_of_lines == len(UNUSED_GRID_NAMES) + len(GRID_NAMES) def test_display_single_grid(self): self.args.append('-g') self.args.append('GLOBAL_MERCATOR') with capture() as (out, err): grids_command(self.args) captured_output = out.getvalue() assert "GLOBAL_MERCATOR" in captured_output def test_ignore_case(self): self.args.append('-g') self.args.append('global_geodetic') with capture() as (out, err): grids_command(self.args) captured_output = out.getvalue() assert "GLOBAL_GEODETIC" in captured_output def test_all_grids(self): with capture() as (out, err): grids_command(self.args) captured_output = out.getvalue() assert "GLOBAL_MERCATOR" in captured_output assert "origin*: 'll'" in captured_output mapproxy-1.11.0/mapproxy/test/system/test_util_wms_capabilities.py000066400000000000000000000124731320454472400256340ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os from nose.tools import assert_raises from mapproxy.client.http import HTTPClient from mapproxy.script.wms_capabilities import wms_capabilities_command from mapproxy.test.http import mock_httpd from mapproxy.test.helper import capture TESTSERVER_ADDRESS = ('127.0.0.1', 56413) TESTSERVER_URL = 'http://%s:%s' % TESTSERVER_ADDRESS CAPABILITIES111_FILE = os.path.join(os.path.dirname(__file__), 'fixture', 'util_wms_capabilities111.xml') CAPABILITIES130_FILE = os.path.join(os.path.dirname(__file__), 'fixture', 'util_wms_capabilities130.xml') SERVICE_EXCEPTION_FILE = os.path.join(os.path.dirname(__file__), 'fixture', 'util_wms_capabilities_service_exception.xml') class TestUtilWMSCapabilities(object): def setup(self): self.client = HTTPClient() self.args = ['command_dummy', '--host', TESTSERVER_URL + '/service'] def test_http_error(self): self.args = ['command_dummy', '--host', 'http://foo.doesnotexist'] with capture() as (out,err): assert_raises(SystemExit, wms_capabilities_command, self.args) assert err.getvalue().startswith("ERROR:") self.args[2] = '/no/valid/url' with capture() as (out,err): assert_raises(SystemExit, wms_capabilities_command, self.args) assert err.getvalue().startswith("ERROR:") def test_request_not_parsable(self): with mock_httpd(TESTSERVER_ADDRESS, [({'path': '/service?request=GetCapabilities&version=1.1.1&service=WMS', 'method': 'GET'}, {'status': '200', 'body': ''})]): with capture() as (out,err): assert_raises(SystemExit, wms_capabilities_command, self.args) error_msg = err.getvalue().rsplit('-'*80, 1)[1].strip() assert error_msg.startswith('Could not parse the document') def test_service_exception(self): self.args = ['command_dummy', '--host', TESTSERVER_URL + '/service?request=GetCapabilities'] with open(SERVICE_EXCEPTION_FILE, 'rb') as fp: capabilities_doc = fp.read() with mock_httpd(TESTSERVER_ADDRESS, [({'path': '/service?request=GetCapabilities&version=1.1.1&service=WMS', 'method': 'GET'}, {'status': '200', 'body': capabilities_doc})]): with capture() as (out,err): assert_raises(SystemExit, wms_capabilities_command, self.args) error_msg = err.getvalue().rsplit('-'*80, 1)[1].strip() assert 'Not a capabilities document' in error_msg def test_parse_capabilities(self): self.args = ['command_dummy', '--host', TESTSERVER_URL + '/service?request=GetCapabilities', '--version', '1.1.1'] with open(CAPABILITIES111_FILE, 'rb') as fp: capabilities_doc = fp.read() with mock_httpd(TESTSERVER_ADDRESS, [({'path': '/service?request=GetCapabilities&version=1.1.1&service=WMS', 'method': 'GET'}, {'status': '200', 'body': capabilities_doc})]): with capture() as (out,err): wms_capabilities_command(self.args) lines = out.getvalue().split('\n') assert lines[1].startswith('Capabilities Document Version 1.1.1') def test_parse_130capabilities(self): self.args = ['command_dummy', '--host', TESTSERVER_URL + '/service?request=GetCapabilities', '--version', '1.3.0'] with open(CAPABILITIES130_FILE, 'rb') as fp: capabilities_doc = fp.read() with mock_httpd(TESTSERVER_ADDRESS, [({'path': '/service?request=GetCapabilities&version=1.3.0&service=WMS', 'method': 'GET'}, {'status': '200', 'body': capabilities_doc})]): with capture() as (out,err): wms_capabilities_command(self.args) lines = out.getvalue().split('\n') assert lines[1].startswith('Capabilities Document Version 1.3.0') def test_key_error(self): self.args = ['command_dummy', '--host', TESTSERVER_URL + '/service?request=GetCapabilities'] with open(CAPABILITIES111_FILE, 'rb') as fp: capabilities_doc = fp.read() capabilities_doc = capabilities_doc.replace(b'minx', b'foo') with mock_httpd(TESTSERVER_ADDRESS, [({'path': '/service?request=GetCapabilities&version=1.1.1&service=WMS', 'method': 'GET'}, {'status': '200', 'body': capabilities_doc})]): with capture() as (out,err): assert_raises(SystemExit, wms_capabilities_command, self.args) assert err.getvalue().startswith('XML-Element has no such attribute') mapproxy-1.11.0/mapproxy/test/system/test_watermark.py000066400000000000000000000066031320454472400232530ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division from io import BytesIO from mapproxy.compat.image import Image from mapproxy.request.wms import WMS111MapRequest from mapproxy.test.http import mock_httpd from mapproxy.test.image import tmp_image from mapproxy.test.system import module_setup, module_teardown, SystemTest, make_base_config from nose.tools import eq_ test_config = {} base_config = make_base_config(test_config) def setup_module(): module_setup(test_config, 'watermark.yaml', with_cache_data=True) def teardown_module(): module_teardown(test_config) class WatermarkTest(SystemTest): config = test_config def setup(self): SystemTest.setup(self) self.common_map_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1', bbox='-180,0,0,80', width='200', height='200', layers='watermark', srs='EPSG:4326', format='image/png', styles='', request='GetMap')) def test_watermark_tile(self): with tmp_image((256, 256), format='png', color=(0, 0, 0)) as img: expected_req = ({'path': r'/service?LAYERs=blank&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=256&SRS=EPSG%3A4326&styles=' '&VERSION=1.1.1&BBOX=-180.0,-90.0,0.0,90.0' '&WIDTH=256'}, {'body': img.read(), 'headers': {'content-type': 'image/jpeg'}}) with mock_httpd(('localhost', 42423), [expected_req]): resp = self.app.get('/tms/1.0.0/watermark/EPSG4326/0/0/0.png') eq_(resp.content_type, 'image/png') img = Image.open(BytesIO(resp.body)) colors = img.getcolors() assert len(colors) >= 2 eq_(sorted(colors)[-1][1], (0, 0, 0)) def test_transparent_watermark_tile(self): with tmp_image((256, 256), format='png', color=(0, 0, 0, 0), mode='RGBA') as img: expected_req = ({'path': r'/service?LAYERs=blank&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=256&SRS=EPSG%3A4326&styles=' '&VERSION=1.1.1&BBOX=-180.0,-90.0,0.0,90.0' '&WIDTH=256'}, {'body': img.read(), 'headers': {'content-type': 'image/jpeg'}}) with mock_httpd(('localhost', 42423), [expected_req]): resp = self.app.get('/tms/1.0.0/watermark_transp/EPSG4326/0/0/0.png') eq_(resp.content_type, 'image/png') img = Image.open(BytesIO(resp.body)) colors = img.getcolors() assert len(colors) >= 2 eq_(sorted(colors)[-1][1], (0, 0, 0, 0)) mapproxy-1.11.0/mapproxy/test/system/test_wms.py000066400000000000000000001576061320454472400220760ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function, division import os import re import sys import shutil import functools from io import BytesIO from mapproxy.srs import SRS from mapproxy.compat.image import Image from mapproxy.request.wms import WMS100MapRequest, WMS111MapRequest, WMS130MapRequest, \ WMS111FeatureInfoRequest, WMS111CapabilitiesRequest, \ WMS130CapabilitiesRequest, WMS100CapabilitiesRequest, \ WMS100FeatureInfoRequest, WMS130FeatureInfoRequest, \ WMS110MapRequest, WMS110FeatureInfoRequest, \ WMS110CapabilitiesRequest, \ wms_request from mapproxy.test.image import is_jpeg, is_png, tmp_image, create_tmp_image from mapproxy.test.http import mock_httpd from mapproxy.test.helper import validate_with_dtd, validate_with_xsd from mapproxy.test.unit.test_grid import assert_almost_equal_bbox from nose.tools import eq_, assert_almost_equal from mapproxy.test.system import module_setup, module_teardown, SystemTest, make_base_config test_config = {} base_config = make_base_config(test_config) def setup_module(): module_setup(test_config, 'layer.yaml', with_cache_data=True) def teardown_module(): module_teardown(test_config) def test_invalid_url(): test_config['app'].get('/invalid?fop', status=404) def is_100_capa(xml): return validate_with_dtd(xml, dtd_name='wms/1.0.0/capabilities_1_0_0.dtd') def is_110_capa(xml): return validate_with_dtd(xml, dtd_name='wms/1.1.0/capabilities_1_1_0.dtd') def is_111_exception(xml, msg=None, code=None, re_msg=None): eq_(xml.xpath('/ServiceExceptionReport/@version')[0], '1.1.1') if msg: eq_(xml.xpath('//ServiceException/text()')[0], msg) if re_msg: exception_msg = xml.xpath('//ServiceException/text()')[0] assert re.findall(re_msg, exception_msg, re.I), "'%r' does not match '%s'" % ( re_msg, exception_msg) if code is not None: eq_(xml.xpath('/ServiceExceptionReport/ServiceException/@code')[0], code) assert validate_with_dtd(xml, 'wms/1.1.1/exception_1_1_1.dtd') def is_111_capa(xml): return validate_with_dtd(xml, dtd_name='wms/1.1.1/WMS_MS_Capabilities.dtd') def is_130_capa(xml): return validate_with_xsd(xml, xsd_name='wms/1.3.0/capabilities_1_3_0.xsd') class WMSTest(SystemTest): config = test_config class TestCoverageWMS(WMSTest): def test_unknown_version_110(self): resp = self.app.get('http://localhost/service?SERVICE=WMS&REQUEST=GetCapabilities' '&VERSION=1.1.0') assert is_110_capa(resp.lxml) def test_unknown_version_113(self): resp = self.app.get('http://localhost/service?SERVICE=WMS&REQUEST=GetCapabilities' '&VERSION=1.1.3') assert is_111_capa(resp.lxml) def test_unknown_version_090(self): resp = self.app.get('http://localhost/service?SERVICE=WMS&REQUEST=GetCapabilities' '&WMTVER=0.9.0') assert is_100_capa(resp.lxml) def test_unknown_version_200(self): resp = self.app.get('http://localhost/service?SERVICE=WMS&REQUEST=GetCapabilities' '&VERSION=2.0.0') assert is_130_capa(resp.lxml) def bbox_srs_from_boundingbox(bbox_elem): return [ float(bbox_elem.attrib['minx']), float(bbox_elem.attrib['miny']), float(bbox_elem.attrib['maxx']), float(bbox_elem.attrib['maxy']), ] class TestWMS111(WMSTest): def setup(self): WMSTest.setup(self) self.common_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1')) self.common_map_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1', bbox='-180,0,0,80', width='200', height='200', layers='wms_cache', srs='EPSG:4326', format='image/png', styles='', request='GetMap')) self.common_fi_req = WMS111FeatureInfoRequest(url='/service?', param=dict(x='10', y='20', width='200', height='200', layers='wms_cache', format='image/png', query_layers='wms_cache', styles='', bbox='1000,400,2000,1400', srs='EPSG:900913')) def test_invalid_request_type(self): req = str(self.common_map_req).replace('GetMap', 'invalid') resp = self.app.get(req) is_111_exception(resp.lxml, "unknown WMS request type 'invalid'") def test_endpoints(self): for endpoint in ('service', 'ows', 'wms'): req = WMS111CapabilitiesRequest(url='/%s?' % endpoint).copy_with_request_params(self.common_req) resp = self.app.get(req) eq_(resp.content_type, 'application/vnd.ogc.wms_xml') xml = resp.lxml assert validate_with_dtd(xml, dtd_name='wms/1.1.1/WMS_MS_Capabilities.dtd') def test_wms_capabilities(self): req = WMS111CapabilitiesRequest(url='/service?').copy_with_request_params(self.common_req) resp = self.app.get(req) eq_(resp.content_type, 'application/vnd.ogc.wms_xml') xml = resp.lxml eq_(xml.xpath('//GetMap//OnlineResource/@xlink:href', namespaces=dict(xlink="http://www.w3.org/1999/xlink"))[0], 'http://localhost/service?') # test for MetadataURL eq_(xml.xpath('//Layer/MetadataURL/OnlineResource/@xlink:href', namespaces=dict(xlink="http://www.w3.org/1999/xlink"))[0], 'http://some.url/') eq_(xml.xpath('//Layer/MetadataURL/@type')[0], 'TC211') layer_names = set(xml.xpath('//Layer/Layer/Name/text()')) expected_names = set(['direct_fwd_params', 'direct', 'wms_cache', 'wms_cache_100', 'wms_cache_130', 'wms_cache_transparent', 'wms_merge', 'tms_cache', 'tms_fi_cache', 'wms_cache_multi', 'wms_cache_link_single', 'wms_cache_110', 'watermark_cache']) eq_(layer_names, expected_names) eq_(set(xml.xpath('//Layer/Layer[3]/Abstract/text()')), set(['Some abstract'])) bboxs = xml.xpath('//Layer/Layer[1]/BoundingBox') bboxs = dict((e.attrib['SRS'], e) for e in bboxs) assert_almost_equal_bbox( bbox_srs_from_boundingbox(bboxs['EPSG:3857']), [-20037508.3428, -15538711.0963, 18924313.4349, 15538711.0963]) assert_almost_equal_bbox( bbox_srs_from_boundingbox(bboxs['EPSG:4326']), [-180.0, -70.0, 170.0, 80.0]) bbox_srs = xml.xpath('//Layer/Layer/BoundingBox') bbox_srs = set(e.attrib['SRS'] for e in bbox_srs) # we have a coverage in EPSG:4258, but it is not in wms.srs (#288) assert 'EPSG:4258' not in bbox_srs assert validate_with_dtd(xml, dtd_name='wms/1.1.1/WMS_MS_Capabilities.dtd') def test_invalid_layer(self): self.common_map_req.params['layers'] = 'invalid' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'application/vnd.ogc.se_xml') is_111_exception(resp.lxml, 'unknown layer: invalid', 'LayerNotDefined') def test_invalid_layer_img_exception(self): self.common_map_req.params['layers'] = 'invalid' self.common_map_req.params['exceptions'] = 'application/vnd.ogc.se_inimage' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') assert is_png(BytesIO(resp.body)) def test_invalid_format(self): self.common_map_req.params['format'] = 'image/ascii' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'application/vnd.ogc.se_xml') is_111_exception(resp.lxml, 'unsupported image format: image/ascii', 'InvalidFormat') def test_invalid_format_img_exception(self): self.common_map_req.params['format'] = 'image/ascii' self.common_map_req.params['exceptions'] = 'application/vnd.ogc.se_inimage' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') assert is_png(BytesIO(resp.body)) def test_invalid_format_options_img_exception(self): self.common_map_req.params['format'] = 'image/png; mode=12bit' self.common_map_req.params['exceptions'] = 'application/vnd.ogc.se_inimage' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') assert is_png(BytesIO(resp.body)) def test_missing_format_img_exception(self): del self.common_map_req.params['format'] self.common_map_req.params['exceptions'] = 'application/vnd.ogc.se_inimage' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') assert is_png(BytesIO(resp.body)) def test_invalid_srs(self): self.common_map_req.params['srs'] = 'EPSG:1234' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'application/vnd.ogc.se_xml') is_111_exception(resp.lxml, 'unsupported srs: EPSG:1234', 'InvalidSRS') def test_get_map_unknown_style(self): self.common_map_req.params['styles'] = 'unknown' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'application/vnd.ogc.se_xml') is_111_exception(resp.lxml, 'unsupported styles: unknown', 'StyleNotDefined') def test_get_map_too_large(self): self.common_map_req.params.size = (5000, 5000) self.common_map_req.params['exceptions'] = 'application/vnd.ogc.se_inimage' resp = self.app.get(self.common_map_req) # is xml, even if inimage was requested eq_(resp.content_type, 'application/vnd.ogc.se_xml') is_111_exception(resp.lxml, 'image size too large') def test_get_map_default_style(self): self.common_map_req.params['styles'] = 'default' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) assert Image.open(data).mode == 'RGB' def test_get_map_png(self): resp = self.app.get(self.common_map_req) assert 'Cache-Control' not in resp.headers eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) assert Image.open(data).mode == 'RGB' def test_get_map_png8_custom_format(self): self.common_map_req.params['layers'] = 'wms_cache' self.common_map_req.params['format'] = 'image/png; mode=8bit' resp = self.app.get(self.common_map_req) eq_(resp.headers['Content-type'], 'image/png; mode=8bit') data = BytesIO(resp.body) assert is_png(data) img = Image.open(data) eq_(img.mode, 'P') def test_get_map_png_transparent_non_transparent_data(self): self.common_map_req.params['transparent'] = 'True' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) img = Image.open(data) eq_(img.mode, 'RGB') def test_get_map_png_transparent(self): self.common_map_req.params['layers'] = 'wms_cache_transparent' self.common_map_req.params['transparent'] = 'True' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) assert Image.open(data).mode == 'RGBA' def test_get_map_png_w_default_bgcolor(self): self.common_map_req.params['layers'] = 'wms_cache_transparent' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) img = Image.open(data) eq_(img.mode, 'RGB') eq_(img.getcolors()[0][1], (255, 255, 255)) def test_get_map_png_w_bgcolor(self): self.common_map_req.params['layers'] = 'wms_cache_transparent' self.common_map_req.params['bgcolor'] = '0xff00a0' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) img = Image.open(data) eq_(img.mode, 'RGB') eq_(sorted(img.getcolors())[-1][1], (255, 0, 160)) def test_get_map_jpeg(self): self.common_map_req.params['format'] = 'image/jpeg' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/jpeg') assert is_jpeg(BytesIO(resp.body)) def test_get_map_xml_exception(self): self.common_map_req.params['bbox'] = '0,0,90,90' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'application/vnd.ogc.se_xml') xml = resp.lxml eq_(xml.xpath('/ServiceExceptionReport/ServiceException/@code'), []) assert 'No response from URL' in xml.xpath('//ServiceException/text()')[0] assert validate_with_dtd(xml, 'wms/1.1.1/exception_1_1_1.dtd') def test_direct_layer_error(self): self.common_map_req.params['layers'] = 'direct' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'application/vnd.ogc.se_xml') xml = resp.lxml eq_(xml.xpath('/ServiceExceptionReport/ServiceException/@code'), []) # TODO hide error # assert 'unable to get map for layers: direct' in \ # xml.xpath('//ServiceException/text()')[0] assert 'No response from URL' in \ xml.xpath('//ServiceException/text()')[0] assert validate_with_dtd(xml, 'wms/1.1.1/exception_1_1_1.dtd') def test_direct_layer_non_image_response(self): self.common_map_req.params['layers'] = 'direct' expected_req = ({'path': r'/service?LAYERs=bar&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=200&SRS=EPSG%3A4326&styles=' '&VERSION=1.1.1&BBOX=-180.0,0.0,0.0,80.0' '&WIDTH=200'}, {'body': b'notanimage', 'headers': {'content-type': 'image/jpeg'}}) with mock_httpd(('localhost', 42423), [expected_req]): resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'application/vnd.ogc.se_xml') xml = resp.lxml eq_(xml.xpath('/ServiceExceptionReport/ServiceException/@code'), []) assert 'error while processing image file' in \ xml.xpath('//ServiceException/text()')[0] assert validate_with_dtd(xml, 'wms/1.1.1/exception_1_1_1.dtd') def test_get_map(self): # check custom tile lock directory tiles_lock_dir = os.path.join(test_config['base_dir'], 'wmscachetilelockdir') # make sure custom tile_lock_dir was not created by other tests shutil.rmtree(tiles_lock_dir, ignore_errors=True) assert not os.path.exists(tiles_lock_dir) self.created_tiles.append('wms_cache_EPSG900913/01/000/000/001/000/000/001.jpeg') with tmp_image((256, 256), format='jpeg') as img: expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fjpeg' '&REQUEST=GetMap&HEIGHT=256&SRS=EPSG%3A900913&styles=' '&VERSION=1.1.1&BBOX=0.0,0.0,20037508.3428,20037508.3428' '&WIDTH=256'}, {'body': img.read(), 'headers': {'content-type': 'image/jpeg'}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): self.common_map_req.params['bbox'] = '0,0,180,90' resp = self.app.get(self.common_map_req) assert 35000 < int(resp.headers['Content-length']) < 75000 eq_(resp.content_type, 'image/png') # check custom tile_lock_dir assert os.path.exists(tiles_lock_dir) def test_get_map_non_image_response(self): self.created_tiles.append('wms_cache_EPSG900913/01/000/000/001/000/000/001.jpeg') expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fjpeg' '&REQUEST=GetMap&HEIGHT=256&SRS=EPSG%3A900913&styles=' '&VERSION=1.1.1&BBOX=0.0,0.0,20037508.3428,20037508.3428' '&WIDTH=256'}, {'body': b'notanimage', 'headers': {'content-type': 'image/jpeg'}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): self.common_map_req.params['bbox'] = '0,0,180,90' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'application/vnd.ogc.se_xml') xml = resp.lxml eq_(xml.xpath('/ServiceExceptionReport/ServiceException/@code'), []) assert 'unable to transform image: cannot identify image file' in \ xml.xpath('//ServiceException/text()')[0] assert validate_with_dtd(xml, 'wms/1.1.1/exception_1_1_1.dtd') def test_get_map_direct_fwd_params_layer(self): img = create_tmp_image((200, 200), format='png') expected_req = ({'path': r'/service?LAYERs=bar&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=200&SRS=EPSG%3A4326&styles=' '&VERSION=1.1.1&BBOX=-180.0,0.0,0.0,80.0' '&WIDTH=200&TIME=20041012'}, {'body': img}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): self.common_map_req.params['layers'] = 'direct_fwd_params' self.common_map_req.params['time'] = '20041012' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') def test_get_map_use_direct_from_level(self): with tmp_image((200, 200), format='png') as img: expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=200&SRS=EPSG%3A4326&styles=' '&VERSION=1.1.1&BBOX=5.0,-10.0,6.0,-9.0' '&WIDTH=200'}, {'body': img.read(), 'headers': {'content-type': 'image/png'}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): self.common_map_req.params['bbox'] = '5,-10,6,-9' resp = self.app.get(self.common_map_req) img.seek(0) assert resp.body == img.read() is_png(img) eq_(resp.content_type, 'image/png') def test_get_map_use_direct_from_level_with_transform(self): with tmp_image((200, 200), format='png') as img: expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=200&SRS=EPSG%3A900913&styles=' '&VERSION=1.1.1&BBOX=908822.944624,7004479.85652,920282.144964,7014491.63726' '&WIDTH=229'}, {'body': img.read(), 'headers': {'content-type': 'image/png'}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): self.common_map_req.params['bbox'] = '444122.311736,5885498.04243,450943.508884,5891425.10484' self.common_map_req.params['srs'] = 'EPSG:25832' resp = self.app.get(self.common_map_req) img.seek(0) assert resp.body != img.read() is_png(img) eq_(resp.content_type, 'image/png') def test_get_map_invalid_bbox(self): # min x larger than max x url = """/service?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&BBOX=7,2,-9,10&SRS=EPSG:4326&WIDTH=164&HEIGHT=388&LAYERS=wms_cache&STYLES=&FORMAT=image/png&TRANSPARENT=TRUE""" resp = self.app.get(url) is_111_exception(resp.lxml, 'invalid bbox 7,2,-9,10') def test_get_map_invalid_bbox2(self): # broken bbox for the requested srs url = """/service?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&BBOX=-72988843.697212,-255661507.634227,142741550.188860,255661507.634227&SRS=EPSG:25833&WIDTH=164&HEIGHT=388&LAYERS=wms_cache_100&STYLES=&FORMAT=image/png&TRANSPARENT=TRUE""" resp = self.app.get(url) # result depends on proj version is_111_exception(resp.lxml, re_msg='Request too large or invalid BBOX.|Could not transform BBOX: Invalid result.') def test_get_map_broken_bbox(self): url = """/service?VERSION=1.1.11&REQUEST=GetMap&SRS=EPSG:31468&BBOX=-10000855.0573254,2847125.18913603,-9329367.42767611,4239924.78564583&WIDTH=130&HEIGHT=62&LAYERS=wms_cache&STYLES=&FORMAT=image/png&TRANSPARENT=TRUE""" resp = self.app.get(url) is_111_exception(resp.lxml, 'Could not transform BBOX: Invalid result.') def test_get_map100(self): # check global tile lock directory tiles_lock_dir = os.path.join(test_config['base_dir'], 'defaulttilelockdir') # make sure global tile_lock_dir was ot created by other tests shutil.rmtree(tiles_lock_dir, ignore_errors=True) assert not os.path.exists(tiles_lock_dir) self.created_tiles.append('wms_cache_100_EPSG900913/01/000/000/001/000/000/001.jpeg') # request_format tiff, cache format jpeg, wms request in png with tmp_image((256, 256), format='tiff') as img: expected_req = ({'path': r'/service?LAYERs=foo,bar&FORMAT=TIFF' '&REQUEST=map&HEIGHT=256&SRS=EPSG%3A900913&styles=' '&WMTVER=1.0.0&BBOX=0.0,0.0,20037508.3428,20037508.3428' '&WIDTH=256'}, {'body': img.read(), 'headers': {'content-type': 'image/tiff'}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): self.common_map_req.params['bbox'] = '0,0,180,90' self.common_map_req.params['layers'] = 'wms_cache_100' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') # check global tile lock directory was created assert os.path.exists(tiles_lock_dir) def test_get_map130(self): self.created_tiles.append('wms_cache_130_EPSG900913/01/000/000/001/000/000/001.jpeg') with tmp_image((256, 256), format='jpeg') as img: expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fjpeg' '&REQUEST=GetMap&HEIGHT=256&CRS=EPSG%3A900913&styles=' '&VERSION=1.3.0&BBOX=0.0,0.0,20037508.3428,20037508.3428' '&WIDTH=256'}, {'body': img.read(), 'headers': {'content-type': 'image/jpeg'}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): self.common_map_req.params['bbox'] = '0,0,180,90' self.common_map_req.params['layers'] = 'wms_cache_130' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') def test_get_map130_axis_order(self): self.created_tiles.append('wms_cache_multi_EPSG4326/02/000/000/003/000/000/001.jpeg') with tmp_image((256, 256), format='jpeg') as img: img = img.read() expected_reqs = [({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fjpeg' '&REQUEST=GetMap&HEIGHT=256&CRS=EPSG%3A4326&styles=' '&VERSION=1.3.0&BBOX=0.0,90.0,90.0,180.0' '&WIDTH=256'}, {'body': img, 'headers': {'content-type': 'image/jpeg'}}),] with mock_httpd(('localhost', 42423), expected_reqs): self.common_map_req.params['bbox'] = '90,0,180,90' self.common_map_req.params['layers'] = 'wms_cache_multi' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') def test_get_featureinfo(self): expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetFeatureInfo&HEIGHT=200&SRS=EPSG%3A900913' '&VERSION=1.1.1&BBOX=1000.0,400.0,2000.0,1400.0&styles=' '&WIDTH=200&QUERY_LAYERS=foo,bar&X=10&Y=20&feature_count=100'}, {'body': b'info', 'headers': {'content-type': 'text/plain'}}) with mock_httpd(('localhost', 42423), [expected_req]): self.common_fi_req.params['feature_count'] = 100 resp = self.app.get(self.common_fi_req) eq_(resp.content_type, 'text/plain') eq_(resp.body, b'info') def test_get_featureinfo_float(self): expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetFeatureInfo&HEIGHT=200&SRS=EPSG%3A900913' '&VERSION=1.1.1&BBOX=1000.0,400.0,2000.0,1400.0&styles=' '&WIDTH=200&QUERY_LAYERS=foo,bar&X=10.123&Y=20.567&feature_count=100'}, {'body': b'info', 'headers': {'content-type': 'text/plain'}}) with mock_httpd(('localhost', 42423), [expected_req]): self.common_fi_req.params['feature_count'] = 100 self.common_fi_req.params['x'] = 10.123 self.common_fi_req.params['y'] = 20.567 resp = self.app.get(self.common_fi_req) eq_(resp.content_type, 'text/plain') eq_(resp.body, b'info') def test_get_featureinfo_transformed(self): expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetFeatureInfo&HEIGHT=200&SRS=EPSG%3A900913' '&BBOX=1172272.30156,7196018.03449,1189711.04571,7213496.99738' '&styles=&VERSION=1.1.1&feature_count=100' '&WIDTH=200&QUERY_LAYERS=foo,bar&X=14&Y=20'}, {'body': b'info', 'headers': {'content-type': 'text/plain'}}) # out fi point at x=10,y=20 p_25832 = (600000+10*(610000 - 600000)/200, 6010000-20*(6010000 - 6000000)/200) # the transformed fi point at x=14,y=20 p_900913 = (1172272.30156+14*(1189711.04571-1172272.30156)/200, 7213496.99738-20*(7213496.99738 - 7196018.03449)/200) # are they the same? # check with tolerance: pixel resolution is ~50 and x/y position is rounded to pizel assert abs(SRS(25832).transform_to(SRS(900913), p_25832)[0] - p_900913[0]) < 50 assert abs(SRS(25832).transform_to(SRS(900913), p_25832)[1] - p_900913[1]) < 50 with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): self.common_fi_req.params['bbox'] = '600000,6000000,610000,6010000' self.common_fi_req.params['srs'] = 'EPSG:25832' self.common_fi_req.params.pos = 10, 20 self.common_fi_req.params['feature_count'] = 100 resp = self.app.get(self.common_fi_req) eq_(resp.content_type, 'text/plain') eq_(resp.body, b'info') def test_get_featureinfo_info_format(self): expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetFeatureInfo&HEIGHT=200&SRS=EPSG%3A900913' '&VERSION=1.1.1&BBOX=1000.0,400.0,2000.0,1400.0&styles=' '&WIDTH=200&QUERY_LAYERS=foo,bar&X=10&Y=20' '&info_format=text%2Fhtml'}, {'body': b'info', 'headers': {'content-type': 'text/html'}}) with mock_httpd(('localhost', 42423), [expected_req]): self.common_fi_req.params['info_format'] = 'text/html' resp = self.app.get(self.common_fi_req) eq_(resp.content_type, 'text/html') eq_(resp.body, b'info') def test_get_featureinfo_130(self): expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetFeatureInfo&HEIGHT=200&CRS=EPSG%3A900913' '&VERSION=1.3.0&BBOX=1000.0,400.0,2000.0,1400.0&styles=' '&WIDTH=200&QUERY_LAYERS=foo,bar&I=10&J=20'}, {'body': b'info', 'headers': {'content-type': 'text/plain'}}) with mock_httpd(('localhost', 42423), [expected_req]): self.common_fi_req.params['layers'] = 'wms_cache_130' self.common_fi_req.params['query_layers'] = 'wms_cache_130' resp = self.app.get(self.common_fi_req) eq_(resp.content_type, 'text/plain') eq_(resp.body, b'info') def test_get_featureinfo_missing_params(self): expected_req = ( {'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetFeatureInfo&HEIGHT=200&SRS=EPSG%3A900913' '&VERSION=1.1.1&BBOX=1000.0,400.0,2000.0,1400.0&styles=' '&WIDTH=200&QUERY_LAYERS=foo,bar&X=10&Y=20'}, {'body': b'info', 'headers': {'content-type': 'text/plain'}}) with mock_httpd(('localhost', 42423), [expected_req]): del self.common_fi_req.params['format'] del self.common_fi_req.params['styles'] resp = self.app.get(self.common_fi_req) eq_(resp.content_type, 'text/plain') eq_(resp.body, b'info') def test_get_featureinfo_missing_params_strict(self): request_parser = self.app.app.handlers['service'].services['wms'].request_parser try: self.app.app.handlers['service'].services['wms'].request_parser = \ functools.partial(wms_request, strict=True) del self.common_fi_req.params['format'] del self.common_fi_req.params['styles'] resp = self.app.get(self.common_fi_req) xml = resp.lxml assert 'missing parameters' in xml.xpath('//ServiceException/text()')[0] assert validate_with_dtd(xml, 'wms/1.1.1/exception_1_1_1.dtd') finally: self.app.app.handlers['service'].services['wms'].request_parser = request_parser self.app.app.handlers['service'].request_parser = request_parser def test_get_featureinfo_not_queryable(self): self.common_fi_req.params['query_layers'] = 'tms_cache' self.common_fi_req.params['exceptions'] = 'application/vnd.ogc.se_xml' resp = self.app.get(self.common_fi_req) eq_(resp.content_type, 'application/vnd.ogc.se_xml') xml = resp.lxml eq_(xml.xpath('/ServiceExceptionReport/ServiceException/@code'), []) assert 'tms_cache is not queryable' in xml.xpath('//ServiceException/text()')[0] assert validate_with_dtd(xml, 'wms/1.1.1/exception_1_1_1.dtd') class TestWMS110(WMSTest): def setup(self): WMSTest.setup(self) self.common_req = WMS110MapRequest(url='/service?', param=dict(service='WMS', version='1.1.0')) self.common_map_req = WMS110MapRequest(url='/service?', param=dict(service='WMS', version='1.1.0', bbox='-180,0,0,80', width='200', height='200', layers='wms_cache', srs='EPSG:4326', format='image/png', styles='', request='GetMap')) self.common_fi_req = WMS110FeatureInfoRequest(url='/service?', param=dict(x='10', y='20', width='200', height='200', layers='wms_cache', format='image/png', query_layers='wms_cache_110', styles='', bbox='1000,400,2000,1400', srs='EPSG:900913')) def test_wms_capabilities(self): req = WMS110CapabilitiesRequest(url='/service?').copy_with_request_params(self.common_req) resp = self.app.get(req) eq_(resp.content_type, 'application/vnd.ogc.wms_xml') xml = resp.lxml eq_(xml.xpath('//GetMap//OnlineResource/@xlink:href', namespaces=dict(xlink="http://www.w3.org/1999/xlink"))[0], 'http://localhost/service?') llbox = xml.xpath('//Capability/Layer/LatLonBoundingBox')[0] # some clients don't like 90deg north/south assert_almost_equal(float(llbox.attrib['miny']), -70.0, 6) assert_almost_equal(float(llbox.attrib['maxy']), 89.999999, 6) assert_almost_equal(float(llbox.attrib['minx']), -180.0, 6) assert_almost_equal(float(llbox.attrib['maxx']), 180.0, 6) layer_names = set(xml.xpath('//Layer/Layer/Name/text()')) expected_names = set(['direct_fwd_params', 'direct', 'wms_cache', 'wms_cache_100', 'wms_cache_130', 'wms_cache_transparent', 'wms_merge', 'tms_cache', 'tms_fi_cache', 'wms_cache_multi', 'wms_cache_link_single', 'wms_cache_110', 'watermark_cache']) eq_(layer_names, expected_names) assert validate_with_dtd(xml, dtd_name='wms/1.1.0/capabilities_1_1_0.dtd') def test_invalid_layer(self): self.common_map_req.params['layers'] = 'invalid' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'application/vnd.ogc.se_xml') xml = resp.lxml eq_(xml.xpath('/ServiceExceptionReport/@version')[0], '1.1.0') eq_(xml.xpath('/ServiceExceptionReport/ServiceException/@code')[0], 'LayerNotDefined') eq_(xml.xpath('//ServiceException/text()')[0], 'unknown layer: invalid') assert validate_with_dtd(xml, dtd_name='wms/1.1.0/exception_1_1_0.dtd') def test_invalid_format(self): self.common_map_req.params['format'] = 'image/ascii' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'application/vnd.ogc.se_xml') xml = resp.lxml eq_(xml.xpath('/ServiceExceptionReport/@version')[0], '1.1.0') eq_(xml.xpath('/ServiceExceptionReport/ServiceException/@code')[0], 'InvalidFormat') eq_(xml.xpath('//ServiceException/text()')[0], 'unsupported image format: image/ascii') assert validate_with_dtd(xml, dtd_name='wms/1.1.0/exception_1_1_0.dtd') def test_invalid_format_img_exception(self): self.common_map_req.params['format'] = 'image/ascii' self.common_map_req.params['exceptions'] = 'application/vnd.ogc.se_inimage' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') assert is_png(BytesIO(resp.body)) def test_missing_format_img_exception(self): del self.common_map_req.params['format'] self.common_map_req.params['exceptions'] = 'application/vnd.ogc.se_inimage' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') assert is_png(BytesIO(resp.body)) def test_invalid_srs(self): self.common_map_req.params['srs'] = 'EPSG:1234' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'application/vnd.ogc.se_xml') xml = resp.lxml eq_(xml.xpath('/ServiceExceptionReport/@version')[0], '1.1.0') eq_(xml.xpath('/ServiceExceptionReport/ServiceException/@code')[0], 'InvalidSRS') eq_(xml.xpath('//ServiceException/text()')[0], 'unsupported srs: EPSG:1234') assert validate_with_dtd(xml, dtd_name='wms/1.1.0/exception_1_1_0.dtd') def test_get_map_png(self): resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) assert Image.open(data).mode == 'RGB' def test_get_map_jpeg(self): self.common_map_req.params['format'] = 'image/jpeg' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/jpeg') assert is_jpeg(BytesIO(resp.body)) def test_get_map_xml_exception(self): self.common_map_req.params['bbox'] = '0,0,90,90' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'application/vnd.ogc.se_xml') xml = resp.lxml eq_(xml.xpath('/ServiceExceptionReport/ServiceException/@code'), []) assert 'No response from URL' in xml.xpath('//ServiceException/text()')[0] assert validate_with_dtd(xml, 'wms/1.1.0/exception_1_1_0.dtd') def test_get_map(self): self.created_tiles.append('wms_cache_EPSG900913/01/000/000/001/000/000/001.jpeg') with tmp_image((256, 256), format='jpeg') as img: expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fjpeg' '&REQUEST=GetMap&HEIGHT=256&SRS=EPSG%3A900913&styles=' '&VERSION=1.1.1&BBOX=0.0,0.0,20037508.3428,20037508.3428' '&WIDTH=256'}, {'body': img.read(), 'headers': {'content-type': 'image/jpeg'}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): self.common_map_req.params['bbox'] = '0,0,180,90' resp = self.app.get(self.common_map_req) assert 35000 < int(resp.headers['Content-length']) < 75000 eq_(resp.content_type, 'image/png') def test_get_map_110(self): self.created_tiles.append('wms_cache_110_EPSG900913/01/000/000/001/000/000/001.jpeg') with tmp_image((256, 256), format='jpeg') as img: expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fjpeg' '&REQUEST=GetMap&HEIGHT=256&SRS=EPSG%3A900913&styles=' '&VERSION=1.1.0&BBOX=0.0,0.0,20037508.3428,20037508.3428' '&WIDTH=256'}, {'body': img.read(), 'headers': {'content-type': 'image/jpeg'}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): self.common_map_req.params['bbox'] = '0,0,180,90' self.common_map_req.params['layers'] = 'wms_cache_110' resp = self.app.get(self.common_map_req) assert 35000 < int(resp.headers['Content-length']) < 75000 eq_(resp.content_type, 'image/png') def test_get_featureinfo(self): expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetFeatureInfo&HEIGHT=200&SRS=EPSG%3A900913' '&VERSION=1.1.0&BBOX=1000.0,400.0,2000.0,1400.0&styles=' '&WIDTH=200&QUERY_LAYERS=foo,bar&X=10&Y=20'}, {'body': b'info', 'headers': {'content-type': 'text/plain'}}) with mock_httpd(('localhost', 42423), [expected_req]): resp = self.app.get(self.common_fi_req) eq_(resp.content_type, 'text/plain') eq_(resp.body, b'info') def test_get_featureinfo_not_queryable(self): self.common_fi_req.params['query_layers'] = 'tms_cache' self.common_fi_req.params['exceptions'] = 'application/vnd.ogc.se_xml' resp = self.app.get(self.common_fi_req) eq_(resp.content_type, 'application/vnd.ogc.se_xml') xml = resp.lxml eq_(xml.xpath('/ServiceExceptionReport/ServiceException/@code'), []) assert 'tms_cache is not queryable' in xml.xpath('//ServiceException/text()')[0] assert validate_with_dtd(xml, 'wms/1.1.0/exception_1_1_0.dtd') class TestWMS100(WMSTest): def setup(self): WMSTest.setup(self) self.common_req = WMS100MapRequest(url='/service?', param=dict(wmtver='1.0.0')) self.common_map_req = WMS100MapRequest(url='/service?', param=dict(wmtver='1.0.0', bbox='-180,0,0,80', width='200', height='200', layers='wms_cache', srs='EPSG:4326', format='PNG', styles='', request='GetMap')) self.common_fi_req = WMS100FeatureInfoRequest(url='/service?', param=dict(x='10', y='20', width='200', height='200', layers='wms_cache_100', format='PNG', query_layers='wms_cache_100', styles='', bbox='1000,400,2000,1400', srs='EPSG:900913')) def test_wms_capabilities(self): req = WMS100CapabilitiesRequest(url='/service?').copy_with_request_params(self.common_req) resp = self.app.get(req) eq_(resp.content_type, 'text/xml') xml = resp.lxml eq_(xml.xpath('/WMT_MS_Capabilities/Service/Title/text()')[0], u'MapProxy test fixture \u2603') layer_names = set(xml.xpath('//Layer/Layer/Name/text()')) expected_names = set(['direct_fwd_params', 'direct', 'wms_cache', 'wms_cache_100', 'wms_cache_130', 'wms_cache_transparent', 'wms_merge', 'tms_cache', 'tms_fi_cache', 'wms_cache_multi', 'wms_cache_link_single', 'wms_cache_110', 'watermark_cache']) eq_(layer_names, expected_names) #TODO srs assert validate_with_dtd(xml, dtd_name='wms/1.0.0/capabilities_1_0_0.dtd') def test_invalid_layer(self): self.common_map_req.params['layers'] = 'invalid' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'text/xml') xml = resp.lxml eq_(xml.xpath('/WMTException/@version')[0], '1.0.0') eq_(xml.xpath('//WMTException/text()')[0].strip(), 'unknown layer: invalid') def test_invalid_format(self): self.common_map_req.params['format'] = 'image/ascii' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'text/xml') xml = resp.lxml eq_(xml.xpath('/WMTException/@version')[0], '1.0.0') eq_(xml.xpath('//WMTException/text()')[0].strip(), 'unsupported image format: ASCII') def test_invalid_format_img_exception(self): self.common_map_req.params['format'] = 'image/ascii' self.common_map_req.params['exceptions'] = 'INIMAGE' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') assert is_png(BytesIO(resp.body)) def test_missing_format_img_exception(self): del self.common_map_req.params['format'] self.common_map_req.params['exceptions'] = 'INIMAGE' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') assert is_png(BytesIO(resp.body)) def test_invalid_srs(self): self.common_map_req.params['srs'] = 'EPSG:1234' print(self.common_map_req.complete_url) resp = self.app.get(self.common_map_req.complete_url) xml = resp.lxml eq_(xml.xpath('//WMTException/text()')[0].strip(), 'unsupported srs: EPSG:1234') def test_get_map_png(self): resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) eq_(Image.open(data).mode, 'RGB') def test_get_map_png_transparent_paletted(self): try: base_config().image.paletted = True self.common_map_req.params['transparent'] = 'True' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) assert Image.open(data).mode == 'P' finally: base_config().image.paletted = False def test_get_map_jpeg(self): self.common_map_req.params['format'] = 'image/jpeg' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/jpeg') assert is_jpeg(BytesIO(resp.body)) def test_get_map_xml_exception(self): self.common_map_req.params['bbox'] = '0,0,90,90' resp = self.app.get(self.common_map_req) xml = resp.lxml assert 'No response from URL' in xml.xpath('//WMTException/text()')[0] def test_get_map(self): self.created_tiles.append('wms_cache_EPSG900913/01/000/000/001/000/000/001.jpeg') with tmp_image((256, 256), format='jpeg') as img: expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fjpeg' '&REQUEST=GetMap&HEIGHT=256&SRS=EPSG%3A900913&styles=' '&VERSION=1.1.1&BBOX=0.0,0.0,20037508.3428,20037508.3428' '&WIDTH=256'}, {'body': img.read(), 'headers': {'content-type': 'image/jpeg'}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): self.common_map_req.params['bbox'] = '0,0,180,90' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') def test_get_featureinfo(self): expected_req = ({'path': r'/service?LAYERs=foo,bar&FORMAT=image%2FPNG' # TODO should be PNG only '&REQUEST=feature_info&HEIGHT=200&SRS=EPSG%3A900913' '&WMTVER=1.0.0&BBOX=1000.0,400.0,2000.0,1400.0&styles=' '&WIDTH=200&QUERY_LAYERS=foo,bar&X=10&Y=20'}, {'body': b'info', 'headers': {'content-type': 'text/plain'}}) with mock_httpd(('localhost', 42423), [expected_req]): resp = self.app.get(self.common_fi_req) eq_(resp.content_type, 'text/plain') eq_(resp.body, b'info') def test_get_featureinfo_not_queryable(self): self.common_fi_req.params['query_layers'] = 'tms_cache' self.common_fi_req.params['exceptions'] = 'application/vnd.ogc.se_xml' resp = self.app.get(self.common_fi_req) eq_(resp.content_type, 'text/xml') xml = resp.lxml assert 'tms_cache is not queryable' in xml.xpath('//WMTException/text()')[0] ns130 = {'wms': 'http://www.opengis.net/wms', 'ogc': 'http://www.opengis.net/ogc', 'sld': 'http://www.opengis.net/sld', 'xlink': 'http://www.w3.org/1999/xlink'} def eq_xpath(xml, xpath, expected, namespaces=None): eq_(xml.xpath(xpath, namespaces=namespaces)[0], expected) eq_xpath_wms130 = functools.partial(eq_xpath, namespaces=ns130) class TestWMS130(WMSTest): def setup(self): WMSTest.setup(self) self.common_req = WMS130MapRequest(url='/service?', param=dict(service='WMS', version='1.3.0')) self.common_map_req = WMS130MapRequest(url='/service?', param=dict(service='WMS', version='1.3.0', bbox='0,-180,80,0', width='200', height='200', layers='wms_cache', crs='EPSG:4326', format='image/png', styles='', request='GetMap')) self.common_fi_req = WMS130FeatureInfoRequest(url='/service?', param=dict(i='10', j='20', width='200', height='200', layers='wms_cache_130', format='image/png', query_layers='wms_cache_130', styles='', bbox='1000,400,2000,1400', crs='EPSG:900913')) def test_wms_capabilities(self): req = WMS130CapabilitiesRequest(url='/service?').copy_with_request_params(self.common_req) resp = self.app.get(req) eq_(resp.content_type, 'text/xml') xml = resp.lxml eq_xpath_wms130(xml, '/wms:WMS_Capabilities/wms:Service/wms:Title/text()', u'MapProxy test fixture \u2603') # test for extended layer metadata eq_xpath_wms130(xml, '/wms:WMS_Capabilities/wms:Capability/wms:Layer/wms:Layer/wms:Attribution/wms:Title/text()', u'My attribution title') layer_names = set(xml.xpath('//wms:Layer/wms:Layer/wms:Name/text()', namespaces=ns130)) expected_names = set(['direct_fwd_params', 'direct', 'wms_cache', 'wms_cache_100', 'wms_cache_130', 'wms_cache_transparent', 'wms_merge', 'tms_cache', 'tms_fi_cache', 'wms_cache_multi', 'wms_cache_link_single', 'wms_cache_110', 'watermark_cache']) eq_(layer_names, expected_names) assert is_130_capa(xml) def test_invalid_layer(self): self.common_map_req.params['layers'] = 'invalid' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'text/xml') xml = resp.lxml eq_xpath_wms130(xml, '/ogc:ServiceExceptionReport/@version', '1.3.0') eq_xpath_wms130(xml, '/ogc:ServiceExceptionReport/ogc:ServiceException/@code', 'LayerNotDefined') eq_xpath_wms130(xml, '//ogc:ServiceException/text()', 'unknown layer: invalid') assert validate_with_xsd(xml, xsd_name='wms/1.3.0/exceptions_1_3_0.xsd') def test_invalid_format(self): self.common_map_req.params['format'] = 'image/ascii' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'text/xml') xml = resp.lxml eq_xpath_wms130(xml, '/ogc:ServiceExceptionReport/@version', '1.3.0') eq_xpath_wms130(xml, '/ogc:ServiceExceptionReport/ogc:ServiceException/@code', 'InvalidFormat') eq_xpath_wms130(xml, '//ogc:ServiceException/text()', 'unsupported image format: image/ascii') assert validate_with_xsd(xml, xsd_name='wms/1.3.0/exceptions_1_3_0.xsd') def test_invalid_format_img_exception(self): self.common_map_req.params['format'] = 'image/ascii' self.common_map_req.params['exceptions'] = 'application/vnd.ogc.se_inimage' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') assert is_png(BytesIO(resp.body)) def test_missing_format_img_exception(self): del self.common_map_req.params['format'] self.common_map_req.params['exceptions'] = 'application/vnd.ogc.se_inimage' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') assert is_png(BytesIO(resp.body)) def test_invalid_srs(self): self.common_map_req.params['srs'] = 'EPSG:1234' self.common_map_req.params['exceptions'] = 'text/xml' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'text/xml') xml = resp.lxml eq_xpath_wms130(xml, '/ogc:ServiceExceptionReport/ogc:ServiceException/@code', 'InvalidCRS') eq_xpath_wms130(xml, '//ogc:ServiceException/text()', 'unsupported crs: EPSG:1234') assert validate_with_xsd(xml, xsd_name='wms/1.3.0/exceptions_1_3_0.xsd') def test_get_map_png(self): resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') data = BytesIO(resp.body) assert is_png(data) assert Image.open(data).mode == 'RGB' def test_get_map_jpeg(self): self.common_map_req.params['format'] = 'image/jpeg' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/jpeg') assert is_jpeg(BytesIO(resp.body)) def test_get_map_xml_exception(self): self.common_map_req.params['bbox'] = '0,0,90,90' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'text/xml') xml = resp.lxml eq_(xml.xpath('/ogc:ServiceExceptionReport/ogc:ServiceException/@code', namespaces=ns130), []) assert ('No response from URL' in xml.xpath('//ogc:ServiceException/text()', namespaces=ns130)[0]) assert validate_with_xsd(xml, xsd_name='wms/1.3.0/exceptions_1_3_0.xsd') def test_get_map(self): self.created_tiles.append('wms_cache_EPSG900913/01/000/000/001/000/000/001.jpeg') with tmp_image((256, 256), format='jpeg') as img: expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fjpeg' '&REQUEST=GetMap&HEIGHT=256&SRS=EPSG%3A900913&styles=' '&VERSION=1.1.1&BBOX=0.0,0.0,20037508.3428,20037508.3428' '&WIDTH=256'}, {'body': img.read(), 'headers': {'content-type': 'image/jpeg'}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): self.common_map_req.params['bbox'] = '0,0,180,90' #internal axis-order resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') def test_get_featureinfo(self): expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetFeatureInfo&HEIGHT=200&CRS=EPSG%3A900913' '&VERSION=1.3.0&BBOX=1000.0,400.0,2000.0,1400.0&styles=' '&WIDTH=200&QUERY_LAYERS=foo,bar&I=10&J=20'}, {'body': b'info', 'headers': {'content-type': 'text/plain'}}) with mock_httpd(('localhost', 42423), [expected_req]): resp = self.app.get(self.common_fi_req) eq_(resp.content_type, 'text/plain') eq_(resp.body, b'info') def test_get_featureinfo_111(self): expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetFeatureInfo&HEIGHT=200&SRS=EPSG%3A900913' '&VERSION=1.1.1&BBOX=1000.0,400.0,2000.0,1400.0&styles=' '&WIDTH=200&QUERY_LAYERS=foo,bar&X=10&Y=20'}, {'body': b'info', 'headers': {'content-type': 'text/plain'}}) with mock_httpd(('localhost', 42423), [expected_req]): self.common_fi_req.params['layers'] = 'wms_cache' self.common_fi_req.params['query_layers'] = 'wms_cache' resp = self.app.get(self.common_fi_req) eq_(resp.content_type, 'text/plain') eq_(resp.body, b'info') if sys.platform != 'win32': class TestWMSLinkSingleColorImages(WMSTest): def setup(self): WMSTest.setup(self) self.common_map_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1', bbox='-180,0,0,80', width='200', height='200', layers='wms_cache_link_single', srs='EPSG:4326', format='image/jpeg', styles='', request='GetMap')) def test_get_map(self): link_name = 'wms_cache_link_single_EPSG900913/01/000/000/001/000/000/001.png' real_name = 'wms_cache_link_single_EPSG900913/single_color_tiles/fe00a0.png' self.created_tiles.append(link_name) self.created_tiles.append(real_name) with tmp_image((256, 256), format='jpeg', color='#fe00a0') as img: expected_req = ({'path': r'/service?LAYERs=foo,bar&SERVICE=WMS&FORMAT=image%2Fjpeg' '&REQUEST=GetMap&HEIGHT=256&SRS=EPSG%3A900913&styles=' '&VERSION=1.1.1&BBOX=0.0,0.0,20037508.3428,20037508.3428' '&WIDTH=256'}, {'body': img.read(), 'headers': {'content-type': 'image/jpeg'}}) with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True): self.common_map_req.params['bbox'] = '0,0,180,90' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/jpeg') base_dir = base_config().cache.base_dir single_loc = os.path.join(base_dir, real_name) tile_loc = os.path.join(base_dir, link_name) assert os.path.exists(single_loc) assert os.path.islink(tile_loc) self.common_map_req.params['format'] = 'image/png' resp = self.app.get(self.common_map_req) eq_(resp.content_type, 'image/png') mapproxy-1.11.0/mapproxy/test/system/test_wms_srs_extent.py000066400000000000000000000150751320454472400243450ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2014 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division from mapproxy.request.wms import WMS111MapRequest, WMS111CapabilitiesRequest from mapproxy.test.system import module_setup, module_teardown, SystemTest, make_base_config from mapproxy.test.image import is_png, is_transparent from mapproxy.test.image import tmp_image, assert_colors_equal, img_from_buf from mapproxy.test.http import mock_httpd from mapproxy.test.system.test_wms import bbox_srs_from_boundingbox from mapproxy.test.unit.test_grid import assert_almost_equal_bbox from nose.tools import eq_ test_config = {} base_config = make_base_config(test_config) def setup_module(): module_setup(test_config, 'wms_srs_extent.yaml') def teardown_module(): module_teardown(test_config) class TestWMSSRSExtentTest(SystemTest): config = test_config def setup(self): SystemTest.setup(self) self.common_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1')) def test_wms_capabilities(self): req = WMS111CapabilitiesRequest(url='/service?').copy_with_request_params(self.common_req) resp = self.app.get(req) eq_(resp.content_type, 'application/vnd.ogc.wms_xml') xml = resp.lxml bboxs = xml.xpath('//Layer/Layer[1]/BoundingBox') bboxs = dict((e.attrib['SRS'], e) for e in bboxs) assert_almost_equal_bbox( bbox_srs_from_boundingbox(bboxs['EPSG:31467']), [2750000.0, 5000000.0, 4250000.0, 6500000.0]) assert_almost_equal_bbox( bbox_srs_from_boundingbox(bboxs['EPSG:25832']), [0.0, 3500000.0, 1000000.0, 8500000.0]) assert_almost_equal_bbox( bbox_srs_from_boundingbox(bboxs['EPSG:3857']), [-20037508.3428, -147730762.670, 20037508.3428, 147730758.195]) assert_almost_equal_bbox( bbox_srs_from_boundingbox(bboxs['EPSG:4326']), [-180.0, -90.0, 180.0, 90.0]) # bboxes clipped to coverage bboxs = xml.xpath('//Layer/Layer[2]/BoundingBox') bboxs = dict((e.attrib['SRS'], e) for e in bboxs) assert_almost_equal_bbox( bbox_srs_from_boundingbox(bboxs['EPSG:31467']), [3213331.57335, 5540436.91132, 3571769.72263, 6104110.432]) assert_almost_equal_bbox( bbox_srs_from_boundingbox(bboxs['EPSG:25832']), [213372.048961, 5538660.64621, 571666.447504, 6102110.74547]) assert_almost_equal_bbox( bbox_srs_from_boundingbox(bboxs['EPSG:3857']), [556597.453966, 6446275.84102, 1113194.90793, 7361866.11305]) assert_almost_equal_bbox( bbox_srs_from_boundingbox(bboxs['EPSG:4326']), [5.0, 50.0, 10.0, 55.0]) def test_out_of_extent(self): resp = self.app.get('http://localhost/service?SERVICE=WMS&REQUEST=GetMap' '&LAYERS=direct&STYLES=' '&WIDTH=100&HEIGHT=100&FORMAT=image/png' '&BBOX=-10000,0,0,1000&SRS=EPSG:25832' '&VERSION=1.1.0&TRANSPARENT=TRUE') # empty/transparent response eq_(resp.content_type, 'image/png') assert is_png(resp.body) assert is_transparent(resp.body) def test_out_of_extent_bgcolor(self): resp = self.app.get('http://localhost/service?SERVICE=WMS&REQUEST=GetMap' '&LAYERS=direct&STYLES=' '&WIDTH=100&HEIGHT=100&FORMAT=image/png' '&BBOX=-10000,0,0,1000&SRS=EPSG:25832' '&VERSION=1.1.0&TRANSPARENT=FALSE&BGCOLOR=0xff0000') # red response eq_(resp.content_type, 'image/png') assert is_png(resp.body) assert_colors_equal(img_from_buf(resp.body).convert('RGBA'), [(100 * 100, [255, 0, 0, 255])]) def test_clipped(self): with tmp_image((256, 256), format='png', color=(255, 0, 0)) as img: expected_req = ({'path': r'/service?LAYERs=bar&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=100&SRS=EPSG%3A25832&styles=' '&VERSION=1.1.1&BBOX=0.0,3500000.0,150.0,3500100.0' '&WIDTH=75'}, {'body': img.read(), 'headers': {'content-type': 'image/png'}}) with mock_httpd(('localhost', 42423), [expected_req]): resp = self.app.get('http://localhost/service?SERVICE=WMS&REQUEST=GetMap' '&LAYERS=direct&STYLES=' '&WIDTH=100&HEIGHT=100&FORMAT=image/png' '&BBOX=-50,3500000,150,3500100&SRS=EPSG:25832' '&VERSION=1.1.0&TRANSPARENT=TRUE') eq_(resp.content_type, 'image/png') assert is_png(resp.body) colors = sorted(img_from_buf(resp.body).convert('RGBA').getcolors()) # quarter is clipped, check if it's transparent eq_(colors[0][0], (25 * 100)) eq_(colors[0][1][3], 0) eq_(colors[1], (75 * 100, (255, 0, 0, 255))) def test_clipped_bgcolor(self): with tmp_image((256, 256), format='png', color=(255, 0, 0)) as img: expected_req = ({'path': r'/service?LAYERs=bar&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=100&SRS=EPSG%3A25832&styles=' '&VERSION=1.1.1&BBOX=0.0,3500000.0,100.0,3500100.0' '&WIDTH=50'}, {'body': img.read(), 'headers': {'content-type': 'image/png'}}) with mock_httpd(('localhost', 42423), [expected_req]): resp = self.app.get('http://localhost/service?SERVICE=WMS&REQUEST=GetMap' '&LAYERS=direct&STYLES=' '&WIDTH=100&HEIGHT=100&FORMAT=image/png' '&BBOX=-100,3500000,100,3500100&SRS=EPSG:25832' '&VERSION=1.1.0&TRANSPARENT=FALSE&BGCOLOR=0x00ff00') eq_(resp.content_type, 'image/png') assert is_png(resp.body) assert_colors_equal(img_from_buf(resp.body).convert('RGBA'), [(50 * 100, [255, 0, 0, 255]), (50 * 100, [0, 255, 0, 255])]) mapproxy-1.11.0/mapproxy/test/system/test_wms_version.py000066400000000000000000000042031320454472400236230ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2014 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division from mapproxy.test.system import module_setup, module_teardown, SystemTest, make_base_config from mapproxy.test.system.test_wms import is_110_capa, is_111_capa test_config = {} base_config = make_base_config(test_config) def setup_module(): module_setup(test_config, 'wms_versions.yaml') def teardown_module(): module_teardown(test_config) class TestWMSVersionsTest(SystemTest): config = test_config def test_supported_version_110(self): resp = self.app.get('http://localhost/service?SERVICE=WMS&REQUEST=GetCapabilities' '&VERSION=1.1.0') assert is_110_capa(resp.lxml) def test_unknown_version_113(self): resp = self.app.get('http://localhost/service?SERVICE=WMS&REQUEST=GetCapabilities' '&VERSION=1.1.3') assert is_111_capa(resp.lxml) def test_unknown_version_090(self): resp = self.app.get('http://localhost/service?SERVICE=WMS&REQUEST=GetCapabilities' '&WMTVER=0.9.0') assert is_110_capa(resp.lxml) def test_unsupported_version_130(self): resp = self.app.get('http://localhost/service?SERVICE=WMS&REQUEST=GetCapabilities' '&VERSION=1.3.0') assert is_111_capa(resp.lxml) def test_unknown_version_200(self): resp = self.app.get('http://localhost/service?SERVICE=WMS&REQUEST=GetCapabilities' '&VERSION=2.0.0') assert is_111_capa(resp.lxml) mapproxy-1.11.0/mapproxy/test/system/test_wmsc.py000066400000000000000000000077111320454472400222300ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division from io import BytesIO from mapproxy.request.wms import ( WMS111MapRequest, WMS111FeatureInfoRequest, WMS111CapabilitiesRequest ) from mapproxy.test.image import is_jpeg from mapproxy.test.helper import validate_with_dtd from mapproxy.test.system.test_wms import is_111_exception from mapproxy.test.system import module_setup, module_teardown, SystemTest, make_base_config from nose.tools import eq_ test_config = {} base_config = make_base_config(test_config) def setup_module(): module_setup(test_config, 'layer.yaml', with_cache_data=True) def teardown_module(): module_teardown(test_config) class TestWMSC(SystemTest): config = test_config def setup(self): SystemTest.setup(self) self.common_cap_req = WMS111CapabilitiesRequest(url='/service?', param=dict(service='WMS', version='1.1.1')) self.common_map_req = WMS111MapRequest(url='/service?', param=dict(service='WMS', version='1.1.1', bbox='-20037508,0.0,0.0,20037508', width='256', height='256', layers='wms_cache', srs='EPSG:900913', format='image/jpeg', styles='', request='GetMap')) self.common_fi_req = WMS111FeatureInfoRequest(url='/service?', param=dict(x='10', y='20', width='200', height='200', layers='wms_cache', format='image/png', query_layers='wms_cache', styles='', bbox='1000,400,2000,1400', srs='EPSG:900913')) def test_capabilities(self): req = str(self.common_cap_req) + '&tiled=true' resp = self.app.get(req) xml = resp.lxml assert validate_with_dtd(xml, dtd_name='wmsc/1.1.1/WMS_MS_Capabilities.dtd') srs = set([e.text for e in xml.xpath('//TileSet/SRS')]) eq_(srs, set(['EPSG:4326', 'EPSG:900913'])) eq_(len(xml.xpath('//TileSet')), 11) def test_get_tile(self): resp = self.app.get(str(self.common_map_req) + '&tiled=true') assert 'public' in resp.headers['Cache-Control'] eq_(resp.content_type, 'image/jpeg') data = BytesIO(resp.body) assert is_jpeg(data) def test_get_tile_w_rounded_bbox(self): self.common_map_req.params.bbox = '-20037400,0.0,0.0,20037400' resp = self.app.get(str(self.common_map_req) + '&tiled=true') assert 'public' in resp.headers['Cache-Control'] eq_(resp.content_type, 'image/jpeg') data = BytesIO(resp.body) assert is_jpeg(data) def test_get_tile_wrong_bbox(self): self.common_map_req.params.bbox = '-20037508,0.0,200000.0,20037508' resp = self.app.get(str(self.common_map_req) + '&tiled=true') assert 'Cache-Control' not in resp.headers is_111_exception(resp.lxml, re_msg='.*invalid bbox') def test_get_tile_wrong_fromat(self): self.common_map_req.params.format = 'image/png' resp = self.app.get(str(self.common_map_req) + '&tiled=true') assert 'Cache-Control' not in resp.headers is_111_exception(resp.lxml, re_msg='Invalid request: invalid.*format.*jpeg') def test_get_tile_wrong_size(self): self.common_map_req.params.size = (256, 255) resp = self.app.get(str(self.common_map_req) + '&tiled=true') assert 'Cache-Control' not in resp.headers is_111_exception(resp.lxml, re_msg='Invalid request: invalid.*size.*256x256') mapproxy-1.11.0/mapproxy/test/system/test_wmts.py000066400000000000000000000160071320454472400222470ustar00rootroot00000000000000# -:- encoding: utf8 -:- # This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import re import os import shutil import functools from io import BytesIO from mapproxy.request.wmts import ( WMTS100TileRequest, WMTS100CapabilitiesRequest ) from mapproxy.test.image import is_jpeg, create_tmp_image from mapproxy.test.http import MockServ from mapproxy.test.helper import validate_with_xsd from mapproxy.test.system import module_setup, module_teardown, SystemTest, make_base_config from nose.tools import eq_ test_config = {} base_config = make_base_config(test_config) def setup_module(): module_setup(test_config, 'wmts.yaml', with_cache_data=True) def teardown_module(): module_teardown(test_config) ns_wmts = { 'wmts': 'http://www.opengis.net/wmts/1.0', 'ows': 'http://www.opengis.net/ows/1.1', 'xlink': 'http://www.w3.org/1999/xlink' } def eq_xpath(xml, xpath, expected, namespaces=None): eq_(xml.xpath(xpath, namespaces=namespaces)[0], expected) eq_xpath_wmts = functools.partial(eq_xpath, namespaces=ns_wmts) class TestWMTS(SystemTest): config = test_config def setup(self): SystemTest.setup(self) self.common_cap_req = WMTS100CapabilitiesRequest(url='/service?', param=dict(service='WMTS', version='1.0.0', request='GetCapabilities')) self.common_tile_req = WMTS100TileRequest(url='/service?', param=dict(service='WMTS', version='1.0.0', tilerow='0', tilecol='0', tilematrix='01', tilematrixset='GLOBAL_MERCATOR', layer='wms_cache', format='image/jpeg', style='', request='GetTile')) def test_endpoints(self): for endpoint in ('service', 'ows'): req = WMTS100CapabilitiesRequest(url='/%s?' % endpoint).copy_with_request_params(self.common_cap_req) resp = self.app.get(req) eq_(resp.content_type, 'application/xml') xml = resp.lxml assert validate_with_xsd(xml, xsd_name='wmts/1.0/wmtsGetCapabilities_response.xsd') def test_capabilities(self): req = str(self.common_cap_req) resp = self.app.get(req) eq_(resp.content_type, 'application/xml') xml = resp.lxml assert validate_with_xsd(xml, xsd_name='wmts/1.0/wmtsGetCapabilities_response.xsd') eq_(xml.xpath('//wmts:Layer/ows:Identifier/text()', namespaces=ns_wmts), ['wms_cache','wms_cache_multi','tms_cache','tms_cache_ul','gk3_cache'], ) eq_(len(xml.xpath('//wmts:Contents/wmts:TileMatrixSet', namespaces=ns_wmts)), 5) goog_matrixset = xml.xpath('//wmts:Contents/wmts:TileMatrixSet[./ows:Identifier/text()="GoogleMapsCompatible"]', namespaces=ns_wmts)[0] eq_(goog_matrixset.findtext('ows:Identifier', namespaces=ns_wmts), 'GoogleMapsCompatible') # top left corner: min X first then max Y assert re.match('-20037508\.\d+ 20037508\.\d+', goog_matrixset.findtext('./wmts:TileMatrix[1]/wmts:TopLeftCorner', namespaces=ns_wmts)) gk_matrixset = xml.xpath('//wmts:Contents/wmts:TileMatrixSet[./ows:Identifier/text()="gk3"]', namespaces=ns_wmts)[0] eq_(gk_matrixset.findtext('ows:Identifier', namespaces=ns_wmts), 'gk3') # Gauß-Krüger uses "reverse" axis order -> top left corner: max Y first then min X assert re.match('6000000.0+ 3000000.0+', gk_matrixset.findtext('./wmts:TileMatrix[1]/wmts:TopLeftCorner', namespaces=ns_wmts)) def test_get_tile(self): resp = self.app.get(str(self.common_tile_req)) eq_(resp.content_type, 'image/jpeg') data = BytesIO(resp.body) assert is_jpeg(data) # test with integer tilematrix url = str(self.common_tile_req).replace('=01', '=1') resp = self.app.get(url) eq_(resp.content_type, 'image/jpeg') data = BytesIO(resp.body) assert is_jpeg(data) def test_get_tile_flipped_axis(self): # test default tile lock directory tiles_lock_dir = os.path.join(test_config['base_dir'], 'cache_data', 'tile_locks') # make sure default tile_lock_dir was not created by other tests shutil.rmtree(tiles_lock_dir, ignore_errors=True) assert not os.path.exists(tiles_lock_dir) self.common_tile_req.params['layer'] = 'tms_cache_ul' self.common_tile_req.params['tilematrixset'] = 'ulgrid' self.common_tile_req.params['format'] = 'image/png' self.common_tile_req.tile = (0, 0, '01') serv = MockServ(port=42423) # source is ll, cache/service ul serv.expects('/tiles/01/000/000/000/000/000/001.png') serv.returns(create_tmp_image((256, 256))) with serv: resp = self.app.get(str(self.common_tile_req), status=200) eq_(resp.content_type, 'image/png') # test default tile lock directory was created assert os.path.exists(tiles_lock_dir) def test_get_tile_source_error(self): self.common_tile_req.params['layer'] = 'tms_cache' self.common_tile_req.params['format'] = 'image/png' resp = self.app.get(str(self.common_tile_req), status=500) xml = resp.lxml assert validate_with_xsd(xml, xsd_name='ows/1.1.0/owsExceptionReport.xsd') eq_xpath_wmts(xml, '/ows:ExceptionReport/ows:Exception/@exceptionCode', 'NoApplicableCode') def test_get_tile_out_of_range(self): self.common_tile_req.params.coord = -1, 1, 1 resp = self.app.get(str(self.common_tile_req), status=400) xml = resp.lxml eq_(resp.content_type, 'text/xml') assert validate_with_xsd(xml, xsd_name='ows/1.1.0/owsExceptionReport.xsd') eq_xpath_wmts(xml, '/ows:ExceptionReport/ows:Exception/@exceptionCode', 'TileOutOfRange') def test_get_tile_invalid_format(self): self.common_tile_req.params['format'] = 'image/png' self.check_invalid_parameter() def test_get_tile_invalid_layer(self): self.common_tile_req.params['layer'] = 'unknown' self.check_invalid_parameter() def test_get_tile_invalid_matrixset(self): self.common_tile_req.params['tilematrixset'] = 'unknown' self.check_invalid_parameter() def check_invalid_parameter(self): resp = self.app.get(str(self.common_tile_req), status=400) xml = resp.lxml eq_(resp.content_type, 'text/xml') assert validate_with_xsd(xml, xsd_name='ows/1.1.0/owsExceptionReport.xsd') eq_xpath_wmts(xml, '/ows:ExceptionReport/ows:Exception/@exceptionCode', 'InvalidParameterValue') mapproxy-1.11.0/mapproxy/test/system/test_wmts_dimensions.py000066400000000000000000000157401320454472400245020ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import functools from mapproxy.test.image import create_tmp_image from mapproxy.test.http import MockServ from mapproxy.test.helper import validate_with_xsd from mapproxy.test.system import module_setup, module_teardown, SystemTest, make_base_config from nose.tools import eq_ test_config = {} base_config = make_base_config(test_config) def setup_module(): module_setup(test_config, 'wmts_dimensions.yaml', with_cache_data=True) def teardown_module(): module_teardown(test_config) ns_wmts = { 'wmts': 'http://www.opengis.net/wmts/1.0', 'ows': 'http://www.opengis.net/ows/1.1', 'xlink': 'http://www.w3.org/1999/xlink' } def eq_xpath(xml, xpath, expected, namespaces=None): eq_(xml.xpath(xpath, namespaces=namespaces)[0], expected) eq_xpath_wmts = functools.partial(eq_xpath, namespaces=ns_wmts) DIMENSION_LAYER_BASE_REQ = ( '/service1?styles=&format=image%2Fpng&height=256' '&bbox=-20037508.3428,0.0,0.0,20037508.3428' '&layers=foo,bar&service=WMS&srs=EPSG%3A900913' '&request=GetMap&width=256&version=1.1.1' ) NO_DIMENSION_LAYER_BASE_REQ = DIMENSION_LAYER_BASE_REQ.replace('/service1?', '/service2?') WMTS_KVP_URL = ( '/service?service=wmts&request=GetTile&version=1.0.0' '&tilematrixset=GLOBAL_MERCATOR&tilematrix=01&tilecol=0&tilerow=0&format=png&style=' ) TEST_TILE = create_tmp_image((256, 256)) class TestWMTS(SystemTest): config = test_config def setup(self): SystemTest.setup(self) def test_capabilities(self): resp = self.app.get('/wmts/myrest/1.0.0/WMTSCapabilities.xml') xml = resp.lxml assert validate_with_xsd(xml, xsd_name='wmts/1.0/wmtsGetCapabilities_response.xsd') eq_(len(xml.xpath('//wmts:Layer', namespaces=ns_wmts)), 2) eq_(len(xml.xpath('//wmts:Contents/wmts:TileMatrixSet', namespaces=ns_wmts)), 1) eq_(set(xml.xpath('//wmts:Contents/wmts:Layer/wmts:ResourceURL/@template', namespaces=ns_wmts)), set(['http://localhost/wmts/myrest/dimension_layer/{TileMatrixSet}/{Time}/{Elevation}/{TileMatrix}/{TileCol}/{TileRow}.png', 'http://localhost/wmts/myrest/no_dimension_layer/{TileMatrixSet}/{Time}/{Elevation}/{TileMatrix}/{TileCol}/{TileRow}.png'])) # check dimension values for dimension_layer dimension_elems = xml.xpath( '//wmts:Layer/ows:Identifier[text()="dimension_layer"]/following-sibling::wmts:Dimension', namespaces=ns_wmts, ) dimensions = {} for elem in dimension_elems: dim = elem.find('{http://www.opengis.net/ows/1.1}Identifier').text default = elem.find('{http://www.opengis.net/wmts/1.0}Default').text values = [e.text for e in elem.findall('{http://www.opengis.net/wmts/1.0}Value')] dimensions[dim] = (values, default) eq_(dimensions['Time'][0], ["2012-11-12T00:00:00", "2012-11-13T00:00:00", "2012-11-14T00:00:00", "2012-11-15T00:00:00"] ) eq_(dimensions['Time'][1], '2012-11-15T00:00:00') eq_(dimensions['Elevation'][1], '0') eq_(dimensions['Elevation'][0], ["0", "1000", "3000"] ) def test_get_tile_valid_dimension(self): serv = MockServ(42423, bbox_aware_query_comparator=True) serv.expects(DIMENSION_LAYER_BASE_REQ + '&Time=2012-11-15T00:00:00&elevation=1000').returns(TEST_TILE) with serv: resp = self.app.get('/wmts/dimension_layer/GLOBAL_MERCATOR/2012-11-15T00:00:00/1000/01/0/0.png') eq_(resp.content_type, 'image/png') def test_get_tile_invalid_dimension(self): self.check_invalid_parameter('/wmts/dimension_layer/GLOBAL_MERCATOR/2042-11-15T00:00:00/default/01/0/0.png') def test_get_tile_default_dimension(self): serv = MockServ(42423, bbox_aware_query_comparator=True) serv.expects(DIMENSION_LAYER_BASE_REQ + '&Time=2012-11-15T00:00:00&elevation=0').returns(TEST_TILE) with serv: resp = self.app.get('/wmts/dimension_layer/GLOBAL_MERCATOR/default/default/01/0/0.png') eq_(resp.content_type, 'image/png') def test_get_tile_invalid_no_dimension_source(self): # unsupported dimension need to be 'default' in RESTful request self.check_invalid_parameter('/wmts/no_dimension_layer/GLOBAL_MERCATOR/2042-11-15T00:00:00/default/01/0/0.png') def test_get_tile_default_no_dimension_source(self): # check if dimensions are ignored serv = MockServ(42423, bbox_aware_query_comparator=True) serv.expects(NO_DIMENSION_LAYER_BASE_REQ).returns(TEST_TILE) with serv: resp = self.app.get('/wmts/no_dimension_layer/GLOBAL_MERCATOR/default/default/01/0/0.png') eq_(resp.content_type, 'image/png') def test_get_tile_kvp_valid_dimension(self): serv = MockServ(42423, bbox_aware_query_comparator=True) serv.expects(DIMENSION_LAYER_BASE_REQ + '&Time=2012-11-14T00:00:00&elevation=3000').returns(TEST_TILE) with serv: resp = self.app.get(WMTS_KVP_URL + '&layer=dimension_layer&timE=2012-11-14T00:00:00&ELEvatioN=3000') eq_(resp.content_type, 'image/png') def test_get_tile_kvp_valid_dimension_defaults(self): serv = MockServ(42423, bbox_aware_query_comparator=True) serv.expects(DIMENSION_LAYER_BASE_REQ + '&Time=2012-11-15T00:00:00&elevation=0').returns(TEST_TILE) with serv: resp = self.app.get(WMTS_KVP_URL + '&layer=dimension_layer') eq_(resp.content_type, 'image/png') def test_get_tile_kvp_invalid_dimension(self): self.check_invalid_parameter(WMTS_KVP_URL + '&layer=dimension_layer&elevation=500') def test_get_tile_kvp_default_no_dimension_source(self): # check if dimensions are ignored serv = MockServ(42423, bbox_aware_query_comparator=True) serv.expects(NO_DIMENSION_LAYER_BASE_REQ).returns(TEST_TILE) with serv: resp = self.app.get(WMTS_KVP_URL + '&layer=no_dimension_layer&Time=2012-11-14T00:00:00&Elevation=3000') eq_(resp.content_type, 'image/png') def check_invalid_parameter(self, url): resp = self.app.get(url, status=400) xml = resp.lxml eq_(resp.content_type, 'text/xml') assert validate_with_xsd(xml, xsd_name='ows/1.1.0/owsExceptionReport.xsd') eq_xpath_wmts(xml, '/ows:ExceptionReport/ows:Exception/@exceptionCode', 'InvalidParameterValue') mapproxy-1.11.0/mapproxy/test/system/test_wmts_restful.py000066400000000000000000000107661320454472400240210ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import functools from io import BytesIO from mapproxy.test.image import is_jpeg, create_tmp_image from mapproxy.test.http import MockServ from mapproxy.test.helper import validate_with_xsd from mapproxy.test.system import module_setup, module_teardown, SystemTest, make_base_config from nose.tools import eq_ test_config = {} base_config = make_base_config(test_config) def setup_module(): module_setup(test_config, 'wmts.yaml', with_cache_data=True) def teardown_module(): module_teardown(test_config) ns_wmts = { 'wmts': 'http://www.opengis.net/wmts/1.0', 'ows': 'http://www.opengis.net/ows/1.1', 'xlink': 'http://www.w3.org/1999/xlink' } def eq_xpath(xml, xpath, expected, namespaces=None): eq_(xml.xpath(xpath, namespaces=namespaces)[0], expected) eq_xpath_wmts = functools.partial(eq_xpath, namespaces=ns_wmts) class TestWMTS(SystemTest): config = test_config def setup(self): SystemTest.setup(self) def test_capabilities(self): resp = self.app.get('/wmts/myrest/1.0.0/WMTSCapabilities.xml') xml = resp.lxml assert validate_with_xsd(xml, xsd_name='wmts/1.0/wmtsGetCapabilities_response.xsd') eq_(len(xml.xpath('//wmts:Layer', namespaces=ns_wmts)), 5) eq_(len(xml.xpath('//wmts:Contents/wmts:TileMatrixSet', namespaces=ns_wmts)), 5) def test_get_tile(self): resp = self.app.get('/wmts/myrest/wms_cache/GLOBAL_MERCATOR/01/0/0.jpeg') eq_(resp.content_type, 'image/jpeg') data = BytesIO(resp.body) assert is_jpeg(data) # test without leading 0 in level resp = self.app.get('/wmts/myrest/wms_cache/GLOBAL_MERCATOR/1/0/0.jpeg') eq_(resp.content_type, 'image/jpeg') data = BytesIO(resp.body) assert is_jpeg(data) def test_get_tile_flipped_axis(self): serv = MockServ(port=42423) # source is ll, cache/service ul serv.expects('/tiles/01/000/000/000/000/000/001.png') serv.returns(create_tmp_image((256, 256))) with serv: resp = self.app.get('/wmts/myrest/tms_cache_ul/ulgrid/01/0/0.png', status=200) eq_(resp.content_type, 'image/png') # test without leading 0 in level resp = self.app.get('/wmts/myrest/tms_cache_ul/ulgrid/1/0/0.png', status=200) eq_(resp.content_type, 'image/png') def test_get_tile_source_error(self): resp = self.app.get('/wmts/myrest/tms_cache/GLOBAL_MERCATOR/01/0/0.png', status=500) xml = resp.lxml assert validate_with_xsd(xml, xsd_name='ows/1.1.0/owsExceptionReport.xsd') eq_xpath_wmts(xml, '/ows:ExceptionReport/ows:Exception/@exceptionCode', 'NoApplicableCode') def test_get_tile_out_of_range(self): resp = self.app.get('/wmts/myrest/wms_cache/GLOBAL_MERCATOR/01/-1/0.jpeg', status=400) xml = resp.lxml eq_(resp.content_type, 'text/xml') assert validate_with_xsd(xml, xsd_name='ows/1.1.0/owsExceptionReport.xsd') eq_xpath_wmts(xml, '/ows:ExceptionReport/ows:Exception/@exceptionCode', 'TileOutOfRange') def test_get_tile_invalid_format(self): url = '/wmts/myrest/wms_cache/GLOBAL_MERCATOR/01/0/0.png' self.check_invalid_parameter(url) def test_get_tile_invalid_layer(self): url = '/wmts/myrest/unknown/GLOBAL_MERCATOR/01/0/0.jpeg' self.check_invalid_parameter(url) def test_get_tile_invalid_matrixset(self): url = '/wmts/myrest/wms_cache/unknown/01/0/0.jpeg' self.check_invalid_parameter(url) def check_invalid_parameter(self, url): resp = self.app.get(url, status=400) xml = resp.lxml eq_(resp.content_type, 'text/xml') assert validate_with_xsd(xml, xsd_name='ows/1.1.0/owsExceptionReport.xsd') eq_xpath_wmts(xml, '/ows:ExceptionReport/ows:Exception/@exceptionCode', 'InvalidParameterValue') mapproxy-1.11.0/mapproxy/test/system/test_xslt_featureinfo.py000066400000000000000000000243111320454472400246330ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import os from mapproxy.request.wms import WMS111FeatureInfoRequest, WMS130FeatureInfoRequest from mapproxy.test.system import module_setup, module_teardown, SystemTest from mapproxy.test.http import mock_httpd from mapproxy.test.helper import strip_whitespace from nose.tools import eq_ test_config = {} xslt_input = b""" """.strip() xslt_input_html = b""" """.strip() xslt_output = b""" """.strip() xslt_output_html = b"""

Bars

""".strip() def setup_module(): module_setup(test_config, 'xslt_featureinfo.yaml') with open(os.path.join(test_config['base_dir'], 'fi_in.xsl'), 'wb') as f: f.write(xslt_input) with open(os.path.join(test_config['base_dir'], 'fi_in_html.xsl'), 'wb') as f: f.write(xslt_input_html) with open(os.path.join(test_config['base_dir'], 'fi_out.xsl'), 'wb') as f: f.write(xslt_output) with open(os.path.join(test_config['base_dir'], 'fi_out_html.xsl'), 'wb') as f: f.write(xslt_output_html) def teardown_module(): module_teardown(test_config) TESTSERVER_ADDRESS = 'localhost', 42423 class TestWMSXSLTFeatureInfo(SystemTest): config = test_config def setup(self): SystemTest.setup(self) self.common_fi_req = WMS111FeatureInfoRequest(url='/service?', param=dict(x='10', y='20', width='200', height='200', layers='fi_layer', format='image/png', query_layers='fi_layer', styles='', bbox='1000,400,2000,1400', srs='EPSG:900913')) def test_get_featureinfo(self): fi_body = b"Bar" expected_req = ({'path': r'/service_a?LAYERs=a_one&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetFeatureInfo&HEIGHT=200&CRS=EPSG%3A900913' '&VERSION=1.3.0&BBOX=1000.0,400.0,2000.0,1400.0&styles=' '&WIDTH=200&QUERY_LAYERS=a_one&i=10&J=20&info_format=text/xml'}, {'body': fi_body, 'headers': {'content-type': 'text/xml; charset=UTF-8'}}) with mock_httpd(('localhost', 42423), [expected_req]): resp = self.app.get(self.common_fi_req) eq_(resp.content_type, 'application/vnd.ogc.gml') eq_(strip_whitespace(resp.body), b'Bar') def test_get_featureinfo_130(self): fi_body = b"Bar" expected_req = ({'path': r'/service_a?LAYERs=a_one&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetFeatureInfo&HEIGHT=200&CRS=EPSG%3A900913' '&VERSION=1.3.0&BBOX=1000.0,400.0,2000.0,1400.0&styles=' '&WIDTH=200&QUERY_LAYERS=a_one&i=10&J=20&info_format=text/xml'}, {'body': fi_body, 'headers': {'content-type': 'text/xml'}}) with mock_httpd(('localhost', 42423), [expected_req]): req = WMS130FeatureInfoRequest(url='/service?').copy_with_request_params(self.common_fi_req) resp = self.app.get(req) eq_(resp.content_type, 'text/xml') eq_(strip_whitespace(resp.body), b'Bar') def test_get_multiple_featureinfo(self): fi_body1 = b"Bar1" fi_body2 = b"Bar2" fi_body3 = b"

Hello

Bar3" expected_req1 = ({'path': r'/service_a?LAYERs=a_one&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetFeatureInfo&HEIGHT=200&CRS=EPSG%3A900913' '&VERSION=1.3.0&BBOX=1000.0,400.0,2000.0,1400.0&styles=' '&WIDTH=200&QUERY_LAYERS=a_one&i=10&J=20&info_format=text/xml'}, {'body': fi_body1, 'headers': {'content-type': 'text/xml'}}) expected_req2 = ({'path': r'/service_b?LAYERs=b_one&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetFeatureInfo&HEIGHT=200&SRS=EPSG%3A900913' '&VERSION=1.1.1&BBOX=1000.0,400.0,2000.0,1400.0&styles=' '&WIDTH=200&QUERY_LAYERS=b_one&X=10&Y=20&info_format=text/xml'}, {'body': fi_body2, 'headers': {'content-type': 'text/xml'}}) expected_req3 = ({'path': r'/service_d?LAYERs=d_one&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetFeatureInfo&HEIGHT=200&SRS=EPSG%3A900913' '&VERSION=1.1.1&BBOX=1000.0,400.0,2000.0,1400.0&styles=' '&WIDTH=200&QUERY_LAYERS=d_one&X=10&Y=20&info_format=text/html'}, {'body': fi_body3, 'headers': {'content-type': 'text/html'}}) with mock_httpd(('localhost', 42423), [expected_req1, expected_req2, expected_req3]): self.common_fi_req.params['layers'] = 'fi_multi_layer' self.common_fi_req.params['query_layers'] = 'fi_multi_layer' resp = self.app.get(self.common_fi_req) eq_(resp.content_type, 'application/vnd.ogc.gml') eq_(strip_whitespace(resp.body), b'Bar1Bar2Bar3') def test_get_multiple_featureinfo_html_out(self): fi_body1 = b"Bar1" fi_body2 = b"Bar2" fi_body3 = b"

Hello

Bar3" expected_req1 = ({'path': r'/service_a?LAYERs=a_one&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetFeatureInfo&HEIGHT=200&CRS=EPSG%3A900913' '&VERSION=1.3.0&BBOX=1000.0,400.0,2000.0,1400.0&styles=' '&WIDTH=200&QUERY_LAYERS=a_one&i=10&J=20&info_format=text/xml'}, {'body': fi_body1, 'headers': {'content-type': 'text/xml'}}) expected_req2 = ({'path': r'/service_b?LAYERs=b_one&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetFeatureInfo&HEIGHT=200&SRS=EPSG%3A900913' '&VERSION=1.1.1&BBOX=1000.0,400.0,2000.0,1400.0&styles=' '&WIDTH=200&QUERY_LAYERS=b_one&X=10&Y=20&info_format=text/xml'}, {'body': fi_body2, 'headers': {'content-type': 'text/xml'}}) expected_req3 = ({'path': r'/service_d?LAYERs=d_one&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetFeatureInfo&HEIGHT=200&SRS=EPSG%3A900913' '&VERSION=1.1.1&BBOX=1000.0,400.0,2000.0,1400.0&styles=' '&WIDTH=200&QUERY_LAYERS=d_one&X=10&Y=20&info_format=text/html'}, {'body': fi_body3, 'headers': {'content-type': 'text/html'}}) with mock_httpd(('localhost', 42423), [expected_req1, expected_req2, expected_req3]): self.common_fi_req.params['layers'] = 'fi_multi_layer' self.common_fi_req.params['query_layers'] = 'fi_multi_layer' self.common_fi_req.params['info_format'] = 'text/html' resp = self.app.get(self.common_fi_req) eq_(resp.content_type, 'text/html') eq_(strip_whitespace(resp.body), b'

Bars

Bar1

Bar2

Bar3

') def test_mixed_featureinfo(self): fi_body1 = b"Hello" fi_body2 = b"Bar2" expected_req1 = ({'path': r'/service_c?LAYERs=c_one&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetFeatureInfo&HEIGHT=200&SRS=EPSG%3A900913' '&VERSION=1.1.1&BBOX=1000.0,400.0,2000.0,1400.0&styles=' '&WIDTH=200&QUERY_LAYERS=c_one&X=10&Y=20'}, {'body': fi_body1, 'headers': {'content-type': 'text/plain'}}) expected_req2 = ({'path': r'/service_a?LAYERs=a_one&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetFeatureInfo&HEIGHT=200&CRS=EPSG%3A900913' '&VERSION=1.3.0&BBOX=1000.0,400.0,2000.0,1400.0&styles=' '&WIDTH=200&QUERY_LAYERS=a_one&i=10&J=20&info_format=text/xml'}, {'body': fi_body2, 'headers': {'content-type': 'text/xml'}}) with mock_httpd(('localhost', 42423), [expected_req1, expected_req2]): self.common_fi_req.params['layers'] = 'fi_without_xslt_layer,fi_layer' self.common_fi_req.params['query_layers'] = 'fi_without_xslt_layer,fi_layer' resp = self.app.get(self.common_fi_req) eq_(resp.content_type, 'text/plain') eq_(strip_whitespace(resp.body), b'HelloBar2')mapproxy-1.11.0/mapproxy/test/test_http_helper.py000066400000000000000000000176741320454472400222620ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2013 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import requests from mapproxy.test.http import ( MockServ, RequestsMismatchError, mock_httpd, basic_auth_value, query_eq, ) from nose.tools import eq_ class TestMockServ(object): def test_no_requests(self): serv = MockServ() with serv: pass def test_expects_get_no_body(self): serv = MockServ() serv.expects('/test') with serv: resp = requests.get('http://localhost:%d/test' % serv.port) eq_(resp.status_code, 200) eq_(resp.content, b'') def test_expects_w_header(self): serv = MockServ() serv.expects('/test', headers={'Accept': 'Coffee'}) with serv: resp = requests.get('http://localhost:%d/test' % serv.port, headers={'Accept': 'Coffee'}) assert resp.ok def test_expects_w_header_but_missing(self): serv = MockServ() serv.expects('/test', headers={'Accept': 'Coffee'}) try: with serv: requests.get('http://localhost:%d/test' % serv.port) except RequestsMismatchError as ex: assert ex.assertions[0].expected == 'Accept: Coffee' def test_expects_post(self): # TODO POST handling in MockServ is hacky. # data just gets appended to URL serv = MockServ() serv.expects('/test?foo', method='POST') with serv: requests.post('http://localhost:%d/test' % serv.port, data=b'foo') def test_expects_post_but_get(self): serv = MockServ() serv.expects('/test', method='POST') try: with serv: requests.get('http://localhost:%d/test' % serv.port) except RequestsMismatchError as ex: assert ex.assertions[0].expected == 'POST' assert ex.assertions[0].actual == 'GET' else: raise AssertionError('AssertionError expected') def test_returns(self): serv = MockServ() serv.expects('/test') serv.returns(body=b'hello') with serv: resp = requests.get('http://localhost:%d/test' % serv.port) assert 'Content-type' not in resp.headers eq_(resp.content, b'hello') def test_returns_headers(self): serv = MockServ() serv.expects('/test') serv.returns(body=b'hello', headers={'content-type': 'text/plain'}) with serv: resp = requests.get('http://localhost:%d/test' % serv.port) eq_(resp.headers['Content-type'], 'text/plain') eq_(resp.content, b'hello') def test_returns_status(self): serv = MockServ() serv.expects('/test') serv.returns(body=b'hello', status_code=418) with serv: resp = requests.get('http://localhost:%d/test' % serv.port) eq_(resp.status_code, 418) eq_(resp.content, b'hello') def test_multiple_requests(self): serv = MockServ() serv.expects('/test1').returns(body=b'hello1') serv.expects('/test2').returns(body=b'hello2') with serv: resp = requests.get('http://localhost:%d/test1' % serv.port) eq_(resp.content, b'hello1') resp = requests.get('http://localhost:%d/test2' % serv.port) eq_(resp.content, b'hello2') def test_too_many_requests(self): serv = MockServ() serv.expects('/test1').returns(body=b'hello1') with serv: resp = requests.get('http://localhost:%d/test1' % serv.port) eq_(resp.content, b'hello1') try: requests.get('http://localhost:%d/test2' % serv.port) except requests.exceptions.RequestException: pass else: raise AssertionError('RequestException expected') def test_missing_requests(self): serv = MockServ() serv.expects('/test1').returns(body=b'hello1') serv.expects('/test2').returns(body=b'hello2') try: with serv: resp = requests.get('http://localhost:%d/test1' % serv.port) eq_(resp.content, b'hello1') except RequestsMismatchError as ex: assert 'requests mismatch:\n - missing requests' in str(ex) else: raise AssertionError('AssertionError expected') def test_reset_unordered(self): serv = MockServ(unordered=True) serv.expects('/test1').returns(body=b'hello1') serv.expects('/test2').returns(body=b'hello2') with serv: resp = requests.get('http://localhost:%d/test1' % serv.port) eq_(resp.content, b'hello1') resp = requests.get('http://localhost:%d/test2' % serv.port) eq_(resp.content, b'hello2') serv.reset() with serv: resp = requests.get('http://localhost:%d/test2' % serv.port) eq_(resp.content, b'hello2') resp = requests.get('http://localhost:%d/test1' % serv.port) eq_(resp.content, b'hello1') def test_unexpected(self): serv = MockServ(unordered=True) serv.expects('/test1').returns(body=b'hello1') serv.expects('/test2').returns(body=b'hello2') try: with serv: resp = requests.get('http://localhost:%d/test1' % serv.port) eq_(resp.content, b'hello1') try: requests.get('http://localhost:%d/test3' % serv.port) except requests.exceptions.RequestException: pass else: raise AssertionError('RequestException expected') resp = requests.get('http://localhost:%d/test2' % serv.port) eq_(resp.content, b'hello2') except RequestsMismatchError as ex: assert 'unexpected request' in ex.assertions[0] else: raise AssertionError('AssertionError expected') class TestMockHttpd(object): def test_no_requests(self): with mock_httpd(('localhost', 42423), []): pass def test_headers_status_body(self): with mock_httpd(('localhost', 42423), [ ({'path':'/test', 'headers': {'Accept': 'Coffee'}}, {'body': b'ok', 'status': 418})]): resp = requests.get('http://localhost:42423/test', headers={'Accept': 'Coffee'}) assert resp.status_code == 418 def test_auth(self): with mock_httpd(('localhost', 42423), [ ({'path':'/test', 'headers': {'Accept': 'Coffee'}, 'require_basic_auth': True}, {'body': b'ok', 'status': 418})]): resp = requests.get('http://localhost:42423/test') eq_(resp.status_code, 401) eq_(resp.content, b'no access') resp = requests.get('http://localhost:42423/test', headers={ 'Authorization': basic_auth_value('foo', 'bar'), 'Accept': 'Coffee'} ) eq_(resp.content, b'ok') def test_query_eq(): assert query_eq('?baz=42&foo=bar', '?foo=bar&baz=42') assert query_eq('?baz=42.00&foo=bar', '?foo=bar&baz=42.0') assert query_eq('?baz=42.000000001&foo=bar', '?foo=bar&baz=42.0') assert not query_eq('?baz=42.00000001&foo=bar', '?foo=bar&baz=42.0') assert query_eq('?baz=42.000000001,23.99999999999&foo=bar', '?foo=bar&baz=42.0,24.0') assert not query_eq('?baz=42.00000001&foo=bar', '?foo=bar&baz=42.0')mapproxy-1.11.0/mapproxy/test/unit/000077500000000000000000000000001320454472400172735ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/unit/__init__.py000066400000000000000000000000001320454472400213720ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/unit/epsg000066400000000000000000000000721320454472400201530ustar00rootroot00000000000000# test srs <1234> +proj=longlat +ellps=bessel +no_defs <> mapproxy-1.11.0/mapproxy/test/unit/fixture/000077500000000000000000000000001320454472400207615ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/unit/fixture/cache.gpkg000066400000000000000000001300001320454472400226700ustar00rootroot00000000000000SQLite format 3@ , GP10-"  2k 1 =no_spatial_ref_systilestest_case2016-06-13T16:24:03.423ZsE|sE|AsE|AsE|AsE|_  =buildingsfeaturesbuildings2016-06-13T13:19:59.348Z0k~(@%<64+rm\@)(˒:*k 9=cachetilescacheCreated with Mapproxy.2016-06-10T15:03:39.390ZsE|sE|AsE|AsE| 1 1no_spatial_ref_sys buildings cache  test_case buildings cache data_type TEXT NOT NULL, -- Type of data stored in the table: "features" per clause Features (http://www.geopackage.org/spec/#features), "tiles" per clause Tiles (http://www.geopackage.org/spec/#tiles), or an implementer-defined value for other data tables per clause in an Extended GeoPackage identifier TEXT UNIQUE, -- A human-readable identifier (e.g. short name) for the table_name content description TEXT DEFAULT '', -- A human-readable description for the table_name content last_change DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), -- Timestamp value in ISO 8601 format as defined by the strftime function %Y-%m-%dT%H:%M:%fZ format string applied to the current time min_x DOUBLE, -- Bounding box minimum easting or longitude for all content in table_name min_y DOUBLE, -- Bounding box minimum northing or latitude for all content in table_name max_x DOUBLE, -- Bounding box maximum easting or longitude for all content in table_name max_y DOUBLE, -- Bounding box maximum northing or latitude for all content in table_name srs_id INTEGER, -- Spatial Reference System ID: gpkg_spatial_ref_sys.srs_id; when data_type is features, SHALL also match gpkg_geometry_columns.srs_id; When data_type is tiles, SHALL also match gpkg_tile_matrix_set.srs.id CONSTRAINT fk_gc_r_srs_id FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys(srs_id)) L''Wtablegpkg_contentsgpkg_contentsCREATE TABLE gpkg_contents (table_name TEXT NOT NULL PRIMARY KEY, -- The name of the tiles, or feature table 9M'indexsqlite_autoindex_gpkg_contents_1gpkg_contents9M'indexsqlite_autoindex_gpkg_contents_2gpkg_contents %%j55wtablegpkg_spatial_ref_sysgpkg_spatial_ref_sysCREATE TABLE gpkg_spatial_ref_sys (srs_name TEXT NOT NULL, -- Human readable name of this SRS (Spatial Reference System) srs_id INTEGER NOT NULL PRIMARY KEY, -- Unique identifier for each Spatial Reference System within a GeoPackage organization TEXT NOT NULL, -- Case-insensitive name of the defining organization e.g. EPSG or epsg organization_coordsys_id INTEGER NOT NULL, -- Numeric ID of the Spatial Reference System assigned by the organization definition TEXT NOT NULL, -- Well-known Text representation of the Spatial Reference System description TEXT)X--ctablegpkg_tile_matrixgpkg_tile_matrix CREATE TABLE gpkg_tile_matrix (table_ hAxO&U*) $#     ) cache?E|?E|) cache?E|?E|) cache@E|@E|) cache@E|@E|' cache@@@#E|@#E|' cache @3E|@3E|' cache @CE|@CE|' cache @SE|@SE|' cache @cE|@cE|' cache @sE|@sE|' cache@E|@E|' cache@E|@E|% cache@@@E|@E|% cache @E|@E|% cache@E|@E|% cache@E|@E|% cache@E|@E|$  cache@E|@E|"  cacheAE|AE| sg[OC7+   cache cache cache cache cache cache  cache cache cache cache cache cache cache cache cache cache cache  cache  cachename TEXT NOT NULL, -- Tile Pyramid User Data Table Name zoom_level INTEGER NOT NULL, -- 0 <= zoom_level <= max_level for table_name matrix_width INTEGER NOT NULL, -- Number of columns (>= 1) in tile matrix at this zoom level matrix_height INTEGER NOT NULL, -- Number of rows (>= 1) in tile matrix at this zoom level tile_width INTEGER NOT NULL, -- Tile width in pixels (>= 1) for this zoom level tile_height INTEGER NOT NULL, -- Tile height in pixels (>= 1) for this zoom level pixel_x_size DOUBLE NOT NULL, -- In t_table_name srid units or default meters for srid 0 (>0) pixel_y_size DOUBLE NOT NULL, -- In t_table_name srid units or default meters for srid 0 (>0) CONSTRAINT pk_ttm PRIMARY KEY (table_name, zoom_level), CONSTRAINT fk_tmm_table_name FOREIGN KEY (table_name) REFERENCES gpkg_contents(table_name)) 22s*?S-indexsqlite_autoindex_gpkg_tile_matrix_1gpkg_tile_matrix 557tablegpkg_tile_matrix_setgpkg_tile_matrix_setCREATE TABLE gpkg_tile_matrix_set G[5indexsqlite_autoindex_gpkg_tile_matrix_set_1gpkg_tile_matrix_setS tablecachecacheCREATE TABLE cache (id INTEGER PRIMARY KEY AUTOINCREMENT, -- Autoincrement primary key zoom_level INTEGER NOT NULL, -- min(zoom_level) <= zoom_level <= max(zoom_level) for t_table_name tile_column INTEGER NOT NULL, -- 0 to tile_matrix matrix_width - 1 tile_row INTEGER NOT NULL, -- 0 to tile_matrix matrix_height - 1 tile_data BLOB NOT NULL, -- Of an image MIME type specified in clauses Tile Encoding PNG, Tile Encoding JPEG, Tile Encoding WEBP UNIQUE (zoom_level, tile_column, tile_row)) @;no_gpkg_spatial_ref_syssE|sE|AsE|AsE|/cache 1sE|sE|AsE|AsE| ;no_gpkg_spatial_ref_sys cache (table_name TEXT NOT NULL PRIMARY KEY, -- Tile Pyramid User Data Table Name srs_id INTEGER NOT NULL, -- Spatial Reference System ID: gpkg_spatial_ref_sys.srs_id min_x DOUBLE NOT NULL, -- Bounding box minimum easting or longitude for all content in table_name min_y DOUBLE NOT NULL, -- Bounding box minimum northing or latitude for all content in table_name max_x DOUBLE NOT NULL, -- Bounding box maximum easting or longitude for all content in table_name max_y DOUBLE NOT NULL, -- Bounding box maximum northing or latitude for all content in table_name 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))  8PNG  IHDRkXTPLTE݋(('777GGGvutggfVVU@@?à`_^   =Q+=Q+indexsqlite_autoindex_no_gpkg_content_1no_gpkg_contentz CtablebuildingsbuildingsCREATE TABLE "buildings" ( "fid" INTEGER PRIMARY KEY AUTOINCREMENT, 'geom' POLYGON, 'OGC_FID' INTEGER, 'OSM_ID' TEXT(32), 'OSM_WAY_ID' TEXT(9), 'NAME' TEXT(50), 'TYPE' TEXT(32), 'AEROWAY' TEXT(32), 'AMENITY' TEXT(16), 'ADMIN_LEVE' TEXT(32), 'BARRIER' TEXT(32), 'BOUNDARY' TEXT(32), 'BUILDING' TEXT(12), 'CRAFT' TEXT(32), 'GEOLOGICAL' TEXT(32), 'HISTORIC' TEXT(32), 'LAND_AREA' TEXT(32), 'LANDUSE' TEXT(32), 'LEISURE' TEXT(32), 'MAN_MADE' TEXT(32), 'MILITARY' TEXT(32), 'NATURAL' TEXT(32), 'OFFICE' TEXT(32), 'PLACE' TEXT(32), 'SHOP' TEXT(11), 'SPORT' TEXT(32), 'TOURISM' TEXT(6), 'OTHER_TAGS' TEXT(116))P ++Ytablesqlite_sequencesqlite_sequenceCREATE TABLE sqlite_sequence(name,seq)) =indexsqlite_autoindex_cache_1cache - no_gpkg_contents  buildings cache ?s NONE undefined @s NONE undefined f= WGS 84 / Pseudo-Mercatorepsg PROJCS["WGS 84 / Pseudo-Mercator",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"]] ,AUTHORITY["EPSG","9122"]]AUTHORITY["EPSG","4326"]],PROJECTION[ "Mercator_1SP"],PARAMETER["central_meridian",0],PARAMETER[ "scale_factor",1],PARAMETER["false_easting",0],PARAMETER[ "false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS[ "X",EAST],AXIS["Y",NORTH] oZ1% Not providedepsg 1 Added via Mapproxy. fWGS 84epsg 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"]], AUTHORITY["EPSG","4326"]] >!_IDATxv*K@p\N& n{3 H xqa$%<`7ȰIee*w =qm3&J2*n(Z6\Vm^c*8|-9725^m93 ɶIFv{Ml# >EQ9LdmNaK‰Q8`%JP”Vb#ί-ۭ;Am YJ%D-NQ7 \Xm֘x6"%,3̲" mdfE% g(#bDJZn $' g!a&M*l8a gOI$VYcC ݉0LF:q:aRwPda܂&R8](P ݊f;5noNEaK_+ ao,8l+ڽ!'*(p %pib6Iv3lb1$"R(H(Ou&ۓ#N^P RD+2۾f&b> @#*LdG"e‰HbBbR}! ݪ^3ITVV$K'TIٶ2%V{QG#*Jn[6c"+@xSXϵ=CaKP,,5K dp챜jD $\ ereu6l_$)J@.,1,=cmw~ϣW3ja;5df)m'w ጢfv2WUɂE)8?j{ D)wÜlOb7d aR D'sgڌPq q 5 V[f$$|KfoƟby7*LĊv* 9VV{,1l+5 g"ᯤɨ6 R JjceDvJ;=-76#oiu$iY-ʕ=a=ALHDf%`n\#dk$@a=;5"0=fBdO ͞ć%jlTClKvokK2ٽJT鿴¢(rf8 f;kFĹ_ N²pl'Spm'pRI\V)'a; #w$v:qi".ZV ,lRᤱYaX NrMN>xGX$Iah:YlI|\ʞg$ <(l!H@04 `>K4J&b8 @ؑ,ADT9(| Y7ZoE0'B0@زtG(GBdk=5ZWAȟ$I%I|{p>b%DHj(H*Hj{*YE8l P[#YjUzGrb=s8'E qӪ"#,aInlO$! =)H95B59rND(ʞ"R%?"40ST{>H!  EJYpOd)0"FEHE2 FkZ,xvNTHj/G[lf\S;``cU©D2LA6{_$ NюIC ֵ I9Nّ2dJAvZz&j#FC^c Fo)gEW8^]N'(d+pѪ]Pev * l1DE`0ۭ\Qp"E^k!HvJv)Jd.IKZ Z6 TEjDqvQ=F"N%dxEeEN$&;IvY=QwDJĝf)|${j!Al`jl.I*8QaxMj"jSf5Zp•oSdUvIvR$vM՞)' )HxXrOQ)sD=Ɏw(Z?Y %-wu=O!@@x [6]T&!LD HvOהZ+Ol..`V l*Tav NAvA5Y<vω#E d N jvI.)72{Dwjw/PH.)T[A)X+SKRK[)[ D5g$FXvqMx [k;g"ZCXLvq)GYl'Q#Evq9`fl۬'$Fc8l w)(jؓ+*UXs(+w# !LgAALvGmm9 EOS Pv 'ZYuTE!J*I{6H&\Sxس+ѲrOW)GT PS)-Vv:@-J^R j"ٗ*bz## Z#Eն>(* jgC(}qlpj;b3A }uDDQp˝0Y@EULv^8P/W Ea jgèkRع0z0ٹ پ^n=۹ %AJw0707о+پ)] %AҤwI K34M}i˿)/mmS}ц?5;m`;Ҭ\O 0q~c w7+W~ im=Ҥ\ZW~`_w#,~Wu>>|Oj#w/7~{5IHp3굻_eUͦͦ،f 17]6>}cn\ͦ[>b\W?^Ttt1ތ>Uf\W͸nWo0yc\%n)R9%i2w>YҕfiG7,]nڷ&p݇ wƇwڵO_en,fGᣛ?||MtfmMt>|7~cgGC;Qϥܯ}p݇7aޮ>||V6>}cn\p݇s<_s>}ÇM0woWc >f>6|ܮ|2&>|1CQx&MJJ9~ow>|uM3, ~ͷUra݇>U݇>}tkx~ݭ݇>|{~}$KÇtnmM7w|:, Tq]fCs>}p؄lb{137cv3n'w1cq]}?{Շw>}161<6><cp#c o lbt{23?1|UonTXzI5g8=V+G˪w /}kPIENDB` kko 47 50 00 03 e6 10 00 00 e3 d1 7c 84 50 08 30 c0 a7 6e d3 44 44 08 30 c0 c9 a3 65 ee 6b 22 28 40 86 ce c6 a5 85 22 28 40 01 03 00 00 00 01 00 00 00 09 00 00 00 b1 dc d2 6a 48 08 30 c0 86 ce c6 a5 85 22 28 40 a7 6e d3 44 44 08 30 c0 2c d6 70 91 7b 22 28 40 34 5f ca 65 48 08 30 c0 5f 16 c9 0d 75 22 28 40 46 b6 4e b7 47 08 30 c0 06 eb a4 63 73 22 28 40 cd 59 9f 72 4c 08 30 c0 c9 a3 65 ee 6b 22 28 40 e3 d1 7c 84 50 08 30 c0 41 b5 66 d0 75 22 28 40 1a c3 9c a0 4d 08 30 c0 8c 6e 18 60 7a 22 28 40 25 e1 e7 64 4e 08 30 c0 c6 80 91 3c 7c 22 28 40 b1 dc d2 6a 48 08 30 c0 86 ce c6 a5 85 22 28 40 yes Y.PNG  IHDRkXTPLTE雵޹̔ΩΘ̱ɩdžɗР#  VV?S-indexsqlite_autoindex_no_gpkg_contents_1no_gpkg_contentsMa;indexsqlite_autoindex_no_gpkg_spatial_ref_sys_1no_gpkg_spatial_ref_sys!;;5tableno_gpkg_spatial_ref_sysno_gpkg_spatial_ref_sys CREATE TABLE no_gpkg_spatial_ref_sys (id INTEGER PRIMARY KEY AUTOINCREMENT, -- Autoincrement primary key zoom_level INTEGER NOT NULL, -- min(zoom_level) <= zoom_level <= max(zoom_level) for t_table_name tile_column INTEGER NOT NULL, -- 0 to tile_matrix matrix_width - 1 tile_row INTEGER NOT NULL, -- 0 to tile_matrix matrix_height - 1 tile_data BLOB NOT NULL, -- Of an image MIME type specified in clauses Tile Encoding PNG, Tile Encoding JPEG, Tile Encoding WEBP UNIQUE (zoom_level, tile_column, tile_row))   Ma;indexsqlite_autoindex_no_gp?S-indexsqlite_autoindex_no_gpkg_contents_1no_gpkg_contentsz--'tableno_gpkg_contentsno_gpkg_contentsCREATE TABLE no_gpkg_contents (id INTEGER PRIMARY KEY AUTOINCREMENT, -- Autoincrement primary key zoom_level INTEGER NOT NULL, -- min(zoom_level) <= zoom_level <= max(zoom_level) for t_table_name tile_column INTEGER NOT NULL, -- 0 to tile_matrix matrix_width - 1 tile_row INTEGER NOT NULL, -- 0 to tile_matrix matrix_height - 1 tile_data BLOB NOT NULL, -- Of an image MIME type specified in clauses Tile Encoding PNG, Tile Encoding JPEG, Tile Encoding WEBP UNIQUE (zoom_level, tile_column, tile_row))$=(%IDATxvTۖ-a\vp2&{׿|?"$$dA%p?\p?\.JJw圵Vo&k9%UTU4~/ҥtȪ%1T)VjY)G>R4FvTާ4~#k-mH@7~#nd:)Qd0~#(QMIa6?g4VU'IQ -j]Gt~GW:"%ޒHJ\zZ~OIT5XED&xoki,3rYxJ-&UV9WQi~FY%YO9;rQU2zis:"*%>HiY_ƙs\Ȏ RUUIx*_PEVx{}LU4gn3H~Iޖ%ok$m3'zu_]R[uRly+RA5rd2i<#*Tg[)Na|Ǎ?**7\T*FWe$NV 0$Rig@.*dH|/FCҲl)Vm6mkH|~NK[/=΅KȉʙH5rQҒƉRRG4 E|(qHV>ɪP[ݸYx'FX0qJo6`MRkk&z“J񍓕QK㝜bt t; 9ei^U;x(G蹬eim^yx3'ykwéG7[-lxی ) @.+QiOqbgWC- # ^`)HugP%֥=I^Y8&-&SxNmOXjZg UҒ;5zۑ >RJF$HsQ9IUU.M\mJO^7ܣmճ5jQ 6'F$%Rd$FTIuwz{U^n4N 1FK[W v#xJ`!) IgU{(%VR;S;9ŚƉSdoW1R$E O 1l|g,To˥WRYSHcoEqH_bHv0;#1|챬c ٓSUÝ.U>RƍL1]-jp')Uom{;#%Vjd810IUԞku*ђƙS|V-Ɖv#d(qGWtp"HD,- w;jRKC Q)DV* ٍG٤Ij;v|hT*5.KwS"ksZJJ[SJ6O;8|̦-$XEJ?jm)-9ø 3QYIJ*e RG.mOԍ\$>iFWh:UJpIݙ@vE,D5?* U*ʞzKVGFP3܂Ӝ%ȈLUK"Mdoij8k|D3XRUZɪEdD+ ۨHK%Q<=5`IUI&J Q|@3X'GZHj8ciI'k=Ksؗ< X.?HG=  >FU"4|f7+8;E//gd.K8+CuxaKuݮށZҰq/{vVUgE9!8X;; սJY'wFTwl,W_eԩssSwbkVd[\\GZ; nd0r2Fu73TyxHx($Rmw`""YW_n=3{e_PH$UyG}A8Ej59? Vøƣ%1U+#ŦKø^_$!eJ\`(j#3T97%b',xKjirQ7+1(5=U {s6F/r]#JY".E?]̫Mٔ BE3e J3GK[m *l JW(=Tk0!q (H|`cS {Pf)g%Jـ r9z[/'ڂ*i |ED|e/:#wiWeRj((G_E)e2g߈[ˑ=m|/FBrQRmiAa#]rucډHEu*Q< Y$#b3IZO _eE֑뜊QS˅߮y([\C)]2Kn7AV$"e+ v0x+4/p4x#yR"H ޖ4QKM)}Mufn ͢M)enx+v}9cFh^ٮixђ0x#:Di\`zZϫ$Fe~* 5K߿H*Nvs!j^6&xF 'g WN]mP h )xG˔rU}͗/_6)s*Ej-J$N8eUygy lwl۞F(RY̫ "T6?*.M:,PW(MlȑR((((%HFa~ZœͦF0T.Pz; dx m--?[Kolk?vsPoD)ARQdOB E0flf)e?_ uFYw0rᲐ`gh&۵m#oġ7Tyj])S< oW27 v8sdzbxV/qI*oyռST,J _)[*TIġl}3z@:gcYTOb1n۞Tx >lO0wdC-MKs69Y#uYy֖+Ϛ(m*m{\\+/ 9K);F"i')oaseY;˗ChWI*\6z&^ēv AŒaM F)2 8硔]r7Bec**JlĸW_fJp_aM֍KH횋v6e F)8CվԈZYNH[z/ J_7]k[b(Th3[k*vMϰGs QJD-C,z.qR~G8_kYNƜrR G I<#/K$E]_l-lu<'S] Q-`IUJٗم♶6pinfnW3|xţE۞62mKhxZ4墨2,(Jd ž-e(kO“qXf)QLG^g~kbnT`0`;$mm*YR'* sϳМ}iq瑶0v%v 247Ҝ,q NR8r_\[<Ԫ0p+8 P/Ƴ<2̄=z/8\,s΋9\2/. +%r b禬+;GQ{ظ#xT6|6s Yo̫klJD\vQUMs*/T[a`(1.'N](Ws3Kh+_97Ws*xD)Q#C&VqRf Tr4VsR#jW\/@WT)Sr63&Qt("ٗʭU>lx~N4z?Y[y̢Q>1v[K
Ai=|*yqd=0QȾZW*["[P/x'fs(׍y[16i7|ܮ[5L2} #ݥm W3i7XH,s>`)i7Qf)D#xMQ}&W&2* x_#?/$#'{V gmL!24SK© e*BMv*x\ER ^lqd3;0ĝtĻ"vdD7J^"BsSJDw"xJF%)h}pbl2[bux#{f Fir+srQJSGihYaec&YGv5'SM4PNfTHjF*>mܱ,%+zrQ&>Xeo%^c,qtEeֈY6>w2V `u5SNKrK|8jww<2ԌЅ.6JirK2J>8w dLSU|Bȸᦖ;p4;q202^fT,3%4g NRpx C|Ίݸ82f9fR<\8d0r F)1aBi`WYx 4RJ3HJ,*(% |i{峖[6n>4dM7A6om(EQ7cxgw7 J Q6W3~rJQUsqb!>owȹzVۭ(bƏ0>ONũ< D?W>kwȣG9Bxq(r"wQ]({N$Q^x&\>w3x.N#Ərd2a*qca<;ug&ٳm[q8NMFx9|3"F⁡#:xxAoc<6qr-0Nf$Bid '8VvW\(>v3x'^f Cfj#\J !ؽ𐳫>SJRmk^ HjF0>S3Hj5~; >S $>&ƍ m t|isf|,$ |ghPKC8lr!3g&>Enq(8 4>5PF8CA*_ǣq#QJJhƍ(NM_5L4' |' p.-_Oˎe ۈf:O_>ST)i=6>Rg5'l70>3 |c(vI>тa3>l%ux%U\z^v3#ܓ7!Ts6 6KUk"ި"\[[MONc$1lqbtiUl|Nc#xE#-a'8I=?'|0 4-Jl_5{oZk.'V+B_>;o55F-nU鈿ZOho-+<IYxUxSҚ8 gkwR/;$~LWU]p#$Ƴ|._,# .> MmFs<*Vx7*:<7j7w gY.uUd4d;[n^emY:iXxOLvfv騵H|A3%iW g[to2DϫOqwQ o6'- <%~J|AUVNix)a <Rmxh@xhy :?@m)* <͞?DqH7 RR!zFm4}Mo,~ oDT%̀N% ,G]RT1|\*?XUe"$ N<g -?[lg -?[lg -?[lg -?[l?u2IENDB`mapproxy-1.11.0/mapproxy/test/unit/polygons/000077500000000000000000000000001320454472400211455ustar00rootroot00000000000000mapproxy-1.11.0/mapproxy/test/unit/polygons/polygons.dbf000066400000000000000000000015011320454472400234710ustar00rootroot00000000000000_AWnameC germany germany netherland mapproxy-1.11.0/mapproxy/test/unit/polygons/polygons.shp000066400000000000000000000030541320454472400235350ustar00rootroot00000000000000' 8 @6ʌ˩G@۰.@3qK@􀀨@6ʌ˩G@۰.@3qK@1H!@2iK@R"@3qK@2R #@mŞfK@2R #@mŞfK@EZ$@+K k%@'2.-K@tA.ֵD&@*z<+K@V?&@6pLJ@SFl$'@r n K@RM)@ȋ7K@KA*@. K@Vn0 ,@(J@~ë,@irĔJ@mQz,@ҤnJ@Q6-@!wMJ@M~]<-@J@(˜Z-@ l%I@۰.@o &I@ ZV-@vI@,6Ki-@ibgI@0w(@K<2-"I@w_((@vI@2_>)@%ڶH@ҥv+@JoH@z+@VaQH@(b)@"N/H@ 5*@SݲG@gŶJ&@,G@8/\$$@6ʌ˩G@<Y"@E0G@o@ 9 G@\&p~C@RGN!2H@vFo @+@tJj}H@Ȯ9 @ eH@{h)@%ڶH@@?H@􀀨@[>lI@[@n737I@D@OƛPI@FFr@{J@4畵 @tIJ@@AQhJ@2U@vJ@l' @1J@>@o @oCJ@3i;!@J@e"@Va.J@$ !@R mK@H!@2iK@`vJH*@FiK@0 ԑL+@|%WK@ N7*@FiK@~*@96$ @!XRJ@&t@CZ5J@>6$ @T۾qJ@i^@&lHJ@t&P@ J@e@M8 @6ʌ˩G@۰.@3qK@2`:mapproxy-1.11.0/mapproxy/test/unit/test_async.py000066400000000000000000000250271320454472400220270ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function import time import threading from mapproxy.util.async import imap_async_threaded, ThreadPool from nose.tools import eq_ from nose.plugins.skip import SkipTest class TestThreaded(object): def test_map(self): def func(x): time.sleep(0.05) return x start = time.time() result = list(imap_async_threaded(func, list(range(40)))) stop = time.time() duration = stop - start assert duration < 0.5, "took %s" % duration eq_(len(result), 40) def test_map_with_exception(self): def func(x): raise Exception() try: list(imap_async_threaded(func, list(range(40)))) except Exception: pass else: assert False, 'exception expected' try: import eventlet from mapproxy.util.async import imap_async_eventlet, EventletPool _has_eventlet = True except ImportError: _has_eventlet = False class TestEventlet(object): def setup(self): if not _has_eventlet: raise SkipTest('eventlet required') def test_map(self): def func(x): eventlet.sleep(0.05) return x start = time.time() result = list(imap_async_eventlet(func, list(range(40)))) stop = time.time() duration = stop - start assert duration < 0.2, "took %s" % duration eq_(len(result), 40) def test_map_with_exception(self): def func(x): raise Exception() try: list(imap_async_eventlet(func, list(range(40)))) except Exception: pass else: assert False, 'exception expected' class CommonPoolTests(object): def _check_single_arg(self, func): result = list(func()) eq_(result, [3]) def test_single_argument(self): f1 = lambda x, y: x+y pool = self.mk_pool() check = self._check_single_arg yield check, lambda: pool.map(f1, [1], [2]) yield check, lambda: pool.imap(f1, [1], [2]) yield check, lambda: pool.starmap(f1, [(1, 2)]) yield check, lambda: pool.starcall([(f1, 1, 2)]) def _check_single_arg_raise(self, func): try: list(func()) except ValueError: pass else: assert False, 'expected ValueError' def test_single_argument_raise(self): def f1(x, y): raise ValueError pool = self.mk_pool() check = self._check_single_arg_raise yield check, lambda: pool.map(f1, [1], [2]) yield check, lambda: pool.imap(f1, [1], [2]) yield check, lambda: pool.starmap(f1, [(1, 2)]) yield check, lambda: pool.starcall([(f1, 1, 2)]) def _check_single_arg_result_object(self, func): result = list(func()) assert result[0].result == None assert isinstance(result[0].exception[1], ValueError) def test_single_argument_result_object(self): def f1(x, y): raise ValueError pool = self.mk_pool() check = self._check_single_arg_result_object yield check, lambda: pool.map(f1, [1], [2], use_result_objects=True) yield check, lambda: pool.imap(f1, [1], [2], use_result_objects=True) yield check, lambda: pool.starmap(f1, [(1, 2)], use_result_objects=True) yield check, lambda: pool.starcall([(f1, 1, 2)], use_result_objects=True) def _check_multiple_args(self, func): result = list(func()) eq_(result, [3, 5]) def test_multiple_arguments(self): f1 = lambda x, y: x+y pool = self.mk_pool() check = self._check_multiple_args yield check, lambda: pool.map(f1, [1, 2], [2, 3]) yield check, lambda: pool.imap(f1, [1, 2], [2, 3]) yield check, lambda: pool.starmap(f1, [(1, 2), (2, 3)]) yield check, lambda: pool.starcall([(f1, 1, 2), (f1, 2, 3)]) def _check_multiple_args_with_exceptions_result_object(self, func): result = list(func()) eq_(result[0].result, 3) eq_(type(result[1].exception[1]), ValueError) eq_(result[2].result, 7) def test_multiple_arguments_exceptions_result_object(self): def f1(x, y): if x+y == 5: raise ValueError() return x+y pool = self.mk_pool() check = self._check_multiple_args_with_exceptions_result_object yield check, lambda: pool.map(f1, [1, 2, 3], [2, 3, 4], use_result_objects=True) yield check, lambda: pool.imap(f1, [1, 2, 3], [2, 3, 4], use_result_objects=True) yield check, lambda: pool.starmap(f1, [(1, 2), (2, 3), (3, 4)], use_result_objects=True) yield check, lambda: pool.starcall([(f1, 1, 2), (f1, 2, 3), (f1, 3, 4)], use_result_objects=True) def _check_multiple_args_with_exceptions(self, func): result = func() try: # first result might aleady raise the exception when # when second result is returned faster by the ThreadPoolWorker eq_(next(result), 3) next(result) except ValueError: pass else: assert False, 'expected ValueError' def test_multiple_arguments_exceptions(self): def f1(x, y): if x+y == 5: raise ValueError() return x+y pool = self.mk_pool() check = self._check_multiple_args_with_exceptions def check_pool_map(): try: pool.map(f1, [1, 2, 3], [2, 3, 4]) except ValueError: pass else: assert False, 'expected ValueError' yield check_pool_map yield check, lambda: pool.imap(f1, [1, 2, 3], [2, 3, 4]) yield check, lambda: pool.starmap(f1, [(1, 2), (2, 3), (3, 4)]) yield check, lambda: pool.starcall([(f1, 1, 2), (f1, 2, 3), (f1, 3, 4)]) class TestThreadPool(CommonPoolTests): def mk_pool(self): return ThreadPool() def test_base_config(self): # test that all concurrent have access to their # local base_config from mapproxy.config import base_config from mapproxy.config import local_base_config from copy import deepcopy # make two separate base_configs conf1 = deepcopy(base_config()) conf1.conf = 1 conf2 = deepcopy(base_config()) conf2.conf = 2 base_config().bar = 'baz' # run test in parallel, check1 and check2 should interleave # each with their local conf error_occured = False def check1(x): global error_occured if base_config().conf != 1 or 'bar' in base_config(): error_occured = True def check2(x): global error_occured if base_config().conf != 2 or 'bar' in base_config(): error_occured = True assert 'bar' in base_config() def test1(): with local_base_config(conf1): pool1 = ThreadPool(5) list(pool1.imap(check1, list(range(200)))) def test2(): with local_base_config(conf2): pool2 = ThreadPool(5) list(pool2.imap(check2, list(range(200)))) t1 = threading.Thread(target=test1) t2 = threading.Thread(target=test2) t1.start() t2.start() t1.join() t2.join() assert not error_occured assert 'bar' in base_config() class TestEventletPool(CommonPoolTests): def setup(self): if not _has_eventlet: raise SkipTest('eventlet required') def mk_pool(self): if not _has_eventlet: raise SkipTest('eventlet required') return EventletPool() def test_base_config(self): # test that all concurrent have access to their # local base_config from mapproxy.config import base_config from mapproxy.config import local_base_config from copy import deepcopy # make two separate base_configs conf1 = deepcopy(base_config()) conf1.conf = 1 conf2 = deepcopy(base_config()) conf2.conf = 2 base_config().bar = 'baz' # run test in parallel, check1 and check2 should interleave # each with their local conf error_occured = False def check1(x): global error_occured if base_config().conf != 1 or 'bar' in base_config(): error_occured = True def check2(x): global error_occured if base_config().conf != 2 or 'bar' in base_config(): error_occured = True assert 'bar' in base_config() def test1(): with local_base_config(conf1): pool1 = EventletPool(5) list(pool1.imap(check1, list(range(200)))) def test2(): with local_base_config(conf2): pool2 = EventletPool(5) list(pool2.imap(check2, list(range(200)))) t1 = eventlet.spawn(test1) t2 = eventlet.spawn(test2) t1.wait() t2.wait() assert not error_occured assert 'bar' in base_config() class DummyException(Exception): pass class TestThreadedExecutorException(object): def setup(self): self.lock = threading.Lock() self.exec_count = 0 self.te = ThreadPool(size=2) def execute(self, x): time.sleep(0.005) with self.lock: self.exec_count += 1 if self.exec_count == 7: raise DummyException() return x def test_execute_w_exception(self): try: self.te.map(self.execute, list(range(100))) except DummyException: print(self.exec_count) assert 7 <= self.exec_count <= 10, 'execution should be interrupted really '\ 'soon (exec_count should be 7+(max(3)))' else: assert False, 'expected DummyException' mapproxy-1.11.0/mapproxy/test/unit/test_auth.py000066400000000000000000000355071320454472400216570ustar00rootroot00000000000000from mapproxy.grid import tile_grid from mapproxy.layer import MapLayer, DefaultMapExtent from mapproxy.image import BlankImageSource from mapproxy.image.opts import ImageOptions from mapproxy.request.base import Request from mapproxy.exception import RequestError from mapproxy.request.wms import wms_request from mapproxy.request.tile import tile_request from mapproxy.service.wms import WMSLayer, WMSGroupLayer, WMSServer from mapproxy.service.tile import TileServer from mapproxy.service.kml import KMLServer, kml_request from mapproxy.test.http import make_wsgi_env from nose.tools import raises, eq_ class DummyLayer(MapLayer): transparent = True extent = DefaultMapExtent() has_legend = False queryable = False def __init__(self, name): MapLayer.__init__(self) self.name = name self.requested = False self.queried = False def get_map(self, query): self.requested = True def get_info(self, query): self.queried = True def map_layers_for_query(self, query): return [(self.name, self)] def info_layers_for_query(self, query): return [(self.name, self)] MAP_REQ = "FORMAT=image%2Fpng&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&STYLES=&SRS=EPSG%3A4326&BBOX=5,46,8,48&WIDTH=60&HEIGHT=40" FI_REQ = "FORMAT=image%2Fpng&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetFeatureInfo&STYLES=&SRS=EPSG%3A4326&BBOX=5,46,8,48&WIDTH=60&HEIGHT=40&X=30&Y=20" class TestWMSAuth(object): def setup(self): layers = {} wms_layers = {} # create test layer tree # - unnamed root # - layer1 # - layer1a # - layer1b # - layer2 # - layer2a # - layer2b # - layer2b1 layers['layer1a'] = DummyLayer('layer1a') wms_layers['layer1a'] = WMSLayer('layer1a', None, [layers['layer1a']], info_layers=[layers['layer1a']]) layers['layer1b'] = DummyLayer('layer1b') wms_layers['layer1b'] = WMSLayer('layer1b', None, [layers['layer1b']], info_layers=[layers['layer1b']]) wms_layers['layer1'] = WMSGroupLayer('layer1', None, None, [wms_layers['layer1a'], wms_layers['layer1b']]) layers['layer2a'] = DummyLayer('layer2a') wms_layers['layer2a'] = WMSLayer('layer2a', None, [layers['layer2a']], info_layers=[layers['layer2a']]) layers['layer2b1'] = DummyLayer('layer2b1') wms_layers['layer2b1'] = WMSLayer('layer2b1', None, [layers['layer2b1']], info_layers=[layers['layer2b1']]) layers['layer2b'] = DummyLayer('layer2b') wms_layers['layer2b'] = WMSGroupLayer('layer2b', None, layers['layer2b'], [wms_layers['layer2b1']]) wms_layers['layer2'] = WMSGroupLayer('layer2', None, None, [wms_layers['layer2a'], wms_layers['layer2b']]) root_layer = WMSGroupLayer(None, 'root layer', None, [wms_layers['layer1'], wms_layers['layer2']]) self.wms_layers = wms_layers self.layers = layers self.server = WMSServer(md={}, root_layer=root_layer, srs=['EPSG:4326'], image_formats={'image/png': ImageOptions(format='image/png')}) # ### # see mapproxy.test.system.test_auth for WMS GetCapabilities request tests # ### class TestWMSGetMapAuth(TestWMSAuth): def map_request(self, layers, auth): env = make_wsgi_env(MAP_REQ+'&layers=' + layers, extra_environ={'mapproxy.authorize': auth}) req = Request(env) return wms_request(req) def test_allow_all(self): def auth(service, layers, **kw): eq_(layers, 'layer1a layer1b'.split()) return { 'authorized': 'full' } self.server.map(self.map_request('layer1', auth)) assert self.layers['layer1a'].requested assert self.layers['layer1b'].requested def test_root_with_partial_sublayers(self): # filter out sublayer layer1b def auth(service, layers, **kw): eq_(layers, 'layer1a layer1b'.split()) return { 'authorized': 'partial', 'layers': { 'layer1': {'map': True}, 'layer1a': {'map': True}, 'layer1b': {'map': False}, } } self.server.map(self.map_request('layer1', auth)) assert self.layers['layer1a'].requested assert not self.layers['layer1b'].requested def test_accept_sublayer(self): def auth(service, layers, **kw): eq_(layers, 'layer1a'.split()) return { 'authorized': 'partial', 'layers': { 'layer1': {'map': True}, 'layer1a': {'map': True}, 'layer1b': {'map': False}, } } self.server.map(self.map_request('layer1a', auth)) assert self.layers['layer1a'].requested assert not self.layers['layer1b'].requested def test_accept_sublayer_w_root_denied(self): def auth(service, layers, **kw): eq_(layers, 'layer1a'.split()) return { 'authorized': 'partial', 'layers': { 'layer1': {'map': False}, 'layer1a': {'map': True}, 'layer1b': {'map': False}, } } self.server.map(self.map_request('layer1a', auth)) assert self.layers['layer1a'].requested assert not self.layers['layer1b'].requested @raises(RequestError) def test_deny_sublayer(self): def auth(service, layers, **kw): eq_(layers, 'layer1b'.split()) return { 'authorized': 'partial', 'layers': { 'layer1': {'map': True}, 'layer1a': {'map': True}, 'layer1b': {'map': False}, } } self.server.map(self.map_request('layer1b', auth)) @raises(RequestError) def test_deny_group_layer_w_source(self): def auth(service, layers, **kw): eq_(layers, 'layer2b'.split()) return { 'authorized': 'partial', 'layers': { 'layer2b': {'map': False}, } } self.server.map(self.map_request('layer2b', auth)) def test_nested_layers_with_partial_sublayers(self): def auth(service, layers, **kw): eq_(layers, 'layer1a layer1b layer2a layer2b'.split()) return { 'authorized': 'partial', 'layers': { 'layer1a': {'map': False}, # deny is the default #'layer1b': {'map': False}, 'layer2a': {'map': True}, 'layer2b': {'map': False}, } } self.server.map(self.map_request('layer1,layer2', auth)) assert self.layers['layer2a'].requested assert not self.layers['layer2b'].requested assert not self.layers['layer1a'].requested assert not self.layers['layer1b'].requested def test_unauthenticated(self): def auth(service, layers, **kw): eq_(layers, 'layer1b'.split()) return { 'authorized': 'unauthenticated', } try: self.server.map(self.map_request('layer1b', auth)) except RequestError as ex: assert ex.status == 401, '%s != 401' % (ex.status, ) else: assert False, 'expected RequestError' class TestWMSGetFeatureInfoAuth(TestWMSAuth): def fi_request(self, layers, auth): env = make_wsgi_env(FI_REQ+'&layers=%s&query_layers=%s' % (layers, layers), extra_environ={'mapproxy.authorize': auth}) req = Request(env) return wms_request(req) def test_root_with_partial_sublayers(self): # filter out sublayer layer1b def auth(service, layers, **kw): eq_(layers, 'layer1a layer1b'.split()) return { 'authorized': 'partial', 'layers': { 'layer1': {'featureinfo': True}, 'layer1a': {'featureinfo': True}, 'layer1b': {'featureinfo': False}, } } self.server.featureinfo(self.fi_request('layer1', auth)) assert self.layers['layer1a'].queried assert not self.layers['layer1b'].queried def test_accept_sublayer(self): def auth(service, layers, **kw): eq_(layers, 'layer1a'.split()) return { 'authorized': 'partial', 'layers': { 'layer1': {'featureinfo': True}, 'layer1a': {'featureinfo': True}, 'layer1b': {'featureinfo': False}, } } self.server.featureinfo(self.fi_request('layer1a', auth)) assert self.layers['layer1a'].queried assert not self.layers['layer1b'].queried def test_accept_sublayer_w_root_denied(self): def auth(service, layers, **kw): eq_(layers, 'layer1a'.split()) return { 'authorized': 'partial', 'layers': { 'layer1': {'featureinfo': False}, 'layer1a': {'featureinfo': True}, 'layer1b': {'featureinfo': False}, } } self.server.featureinfo(self.fi_request('layer1a', auth)) assert self.layers['layer1a'].queried assert not self.layers['layer1b'].queried @raises(RequestError) def test_deny_sublayer(self): def auth(service, layers, **kw): eq_(layers, 'layer1b'.split()) return { 'authorized': 'partial', 'layers': { 'layer1': {'featureinfo': True}, 'layer1a': {'featureinfo': True}, 'layer1b': {'featureinfo': False}, } } self.server.featureinfo(self.fi_request('layer1b', auth)) @raises(RequestError) def test_deny_group_layer_w_source(self): def auth(service, layers, **kw): eq_(layers, 'layer2b'.split()) return { 'authorized': 'partial', 'layers': { 'layer2b': {'featureinfo': False}, } } self.server.featureinfo(self.fi_request('layer2b', auth)) def test_nested_layers_with_partial_sublayers(self): def auth(service, layers, **kw): eq_(layers, 'layer1a layer1b layer2a layer2b'.split()) return { 'authorized': 'partial', 'layers': { 'layer1a': {'featureinfo': False}, # deny is the default #'layer1b': {'featureinfo': False}, 'layer2a': {'featureinfo': True}, 'layer2b': {'featureinfo': False}, } } self.server.featureinfo(self.fi_request('layer1,layer2', auth)) assert self.layers['layer2a'].queried assert not self.layers['layer2b'].queried assert not self.layers['layer1a'].queried assert not self.layers['layer1b'].queried class DummyTileLayer(object): def __init__(self, name): self.requested = False self.name = name self.grid = tile_grid(900913) def tile_bbox(self, request, use_profiles=False): # this dummy code does not handle profiles and different tile origins! return self.grid.tile_bbox(request.tile) def render(self, tile_request, use_profiles=None, coverage=None, decorate_img=None): self.requested = True resp = BlankImageSource((256, 256), image_opts=ImageOptions(format='image/png')) resp.timestamp = 0 return resp class TestTMSAuth(object): service = 'tms' def setup(self): self.layers = {} self.layers['layer1'] = DummyTileLayer('layer1') self.layers['layer2'] = DummyTileLayer('layer2') self.layers['layer3'] = DummyTileLayer('layer3') self.server = TileServer(self.layers, {}) def tile_request(self, tile, auth): env = make_wsgi_env('', extra_environ={'mapproxy.authorize': auth, 'PATH_INFO': '/tms/1.0.0/'+tile}) req = Request(env) return tile_request(req) @raises(RequestError) def test_deny_all(self): def auth(service, layers, **kw): eq_(service, self.service) eq_(layers, 'layer1'.split()) return { 'authorized': 'none', } self.server.map(self.tile_request('layer1/0/0/0.png', auth)) @raises(RequestError) def test_deny_layer(self): def auth(service, layers, **kw): eq_(service, self.service) eq_(layers, 'layer1'.split()) return { 'authorized': 'partial', 'layers': { 'layer1': {'tile': False}, 'layer2': {'tile': True}, } } self.server.map(self.tile_request('layer1/0/0/0.png', auth)) def test_allow_all(self): def auth(service, layers, **kw): eq_(service, self.service) eq_(layers, 'layer1'.split()) return { 'authorized': 'full', } self.server.map(self.tile_request('layer1/0/0/0.png', auth)) assert self.layers['layer1'].requested def test_allow_layer(self): def auth(service, layers, **kw): eq_(service, self.service) eq_(layers, 'layer1'.split()) return { 'authorized': 'partial', 'layers': { 'layer1': {'tile': True}, 'layer2': {'tile': False}, } } self.server.map(self.tile_request('layer1/0/0/0.png', auth)) assert self.layers['layer1'].requested class TestTileAuth(TestTMSAuth): def tile_request(self, tile, auth): env = make_wsgi_env('', extra_environ={'mapproxy.authorize': auth, 'PATH_INFO': '/tiles/'+tile}) req = Request(env) return tile_request(req) class TestKMLAuth(TestTMSAuth): service = 'kml' def setup(self): TestTMSAuth.setup(self) self.server = KMLServer(self.layers, {}) def tile_request(self, tile, auth): env = make_wsgi_env('', extra_environ={'mapproxy.authorize': auth, 'PATH_INFO': '/kml/'+tile}) req = Request(env) return kml_request(req) mapproxy-1.11.0/mapproxy/test/unit/test_cache.py000066400000000000000000001203411320454472400217500ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import re import time import threading import shutil import tempfile import base64 from io import BytesIO from mapproxy.compat.image import Image from mapproxy.layer import ( CacheMapLayer, SRSConditional, ResolutionConditional, DirectMapLayer, MapExtent, MapQuery, ) from mapproxy.source import InvalidSourceQuery, SourceError from mapproxy.client.wms import WMSClient from mapproxy.source.wms import WMSSource from mapproxy.source.tile import TiledSource from mapproxy.cache.base import TileLocker from mapproxy.cache.file import FileCache from mapproxy.cache.tile import Tile, TileManager from mapproxy.grid import TileGrid, resolution_range from mapproxy.srs import SRS from mapproxy.client.http import HTTPClient from mapproxy.image import ImageSource from mapproxy.image.opts import ImageOptions from mapproxy.layer import BlankImage, MapLayer, MapBBOXError from mapproxy.request.wms import WMS111MapRequest from mapproxy.util.coverage import BBOXCoverage from mapproxy.test.image import create_debug_img, is_png, tmp_image from mapproxy.test.http import assert_query_eq, wms_query_eq, query_eq, mock_httpd from collections import defaultdict from nose.tools import eq_, raises, assert_not_equal, assert_raises TEST_SERVER_ADDRESS = ('127.0.0.1', 56413) GLOBAL_GEOGRAPHIC_EXTENT = MapExtent((-180, -90, 180, 90), SRS(4326)) tmp_lock_dir = None def setup(): global tmp_lock_dir tmp_lock_dir = tempfile.mkdtemp() def teardown(): shutil.rmtree(tmp_lock_dir) class counting_set(object): def __init__(self, items): self.data = defaultdict(int) for item in items: self.data[item] += 1 def add(self, item): self.data[item] += 1 def __repr__(self): return 'counting_set(%r)' % dict(self.data) def __eq__(self, other): return self.data == other.data class MockTileClient(object): def __init__(self): self.requested_tiles = [] def get_tile(self, tile_coord, format=None): self.requested_tiles.append(tile_coord) return ImageSource(create_debug_img((256, 256))) class TestTiledSourceGlobalGeodetic(object): def setup(self): self.grid = TileGrid(SRS(4326), bbox=[-180, -90, 180, 90]) self.client = MockTileClient() self.source = TiledSource(self.grid, self.client) def test_match(self): self.source.get_map(MapQuery([-180, -90, 0, 90], (256, 256), SRS(4326))) self.source.get_map(MapQuery([0, -90, 180, 90], (256, 256), SRS(4326))) eq_(self.client.requested_tiles, [(0, 0, 1), (1, 0, 1)]) @raises(InvalidSourceQuery) def test_wrong_size(self): self.source.get_map(MapQuery([-180, -90, 0, 90], (512, 256), SRS(4326))) @raises(InvalidSourceQuery) def test_wrong_srs(self): self.source.get_map(MapQuery([-180, -90, 0, 90], (512, 256), SRS(4326))) class MockFileCache(FileCache): def __init__(self, *args, **kw): super(MockFileCache, self).__init__(*args, **kw) self.stored_tiles = set() self.loaded_tiles = counting_set([]) def store_tile(self, tile): assert tile.coord not in self.stored_tiles self.stored_tiles.add(tile.coord) if self.cache_dir != '/dev/null': FileCache.store_tile(self, tile) def load_tile(self, tile, with_metadata=False): self.loaded_tiles.add(tile.coord) return FileCache.load_tile(self, tile, with_metadata) def is_cached(self, tile): return tile.coord in self.stored_tiles def create_cached_tile(tile, cache, timestamp=None): loc = cache.tile_location(tile, create_dir=True) with open(loc, 'wb') as f: f.write(b'foo') if timestamp: os.utime(loc, (timestamp, timestamp)) class TestTileManagerStaleTiles(object): def setup(self): self.cache_dir = tempfile.mkdtemp() self.file_cache = FileCache(cache_dir=self.cache_dir, file_ext='png') self.grid = TileGrid(SRS(4326), bbox=[-180, -90, 180, 90]) self.client = MockTileClient() self.source = TiledSource(self.grid, self.client) self.locker = TileLocker(tmp_lock_dir, 10, "id") self.tile_mgr = TileManager(self.grid, self.file_cache, [self.source], 'png', locker=self.locker) def teardown(self): shutil.rmtree(self.cache_dir) def test_is_stale_missing(self): assert not self.tile_mgr.is_stale(Tile((0, 0, 1))) def test_is_stale_not_expired(self): create_cached_tile(Tile((0, 0, 1)), self.file_cache) assert not self.tile_mgr.is_stale(Tile((0, 0, 1))) def test_is_stale_expired(self): create_cached_tile(Tile((0, 0, 1)), self.file_cache, timestamp=time.time()-3600) self.tile_mgr._expire_timestamp = time.time() assert self.tile_mgr.is_stale(Tile((0, 0, 1))) class TestTileManagerRemoveTiles(object): def setup(self): self.cache_dir = tempfile.mkdtemp() self.file_cache = FileCache(cache_dir=self.cache_dir, file_ext='png') self.grid = TileGrid(SRS(4326), bbox=[-180, -90, 180, 90]) self.client = MockTileClient() self.source = TiledSource(self.grid, self.client) self.image_opts = ImageOptions(format='image/png') self.locker = TileLocker(tmp_lock_dir, 10, "id") self.tile_mgr = TileManager(self.grid, self.file_cache, [self.source], 'png', image_opts=self.image_opts, locker=self.locker) def teardown(self): shutil.rmtree(self.cache_dir) def test_remove_missing(self): self.tile_mgr.remove_tile_coords([(0, 0, 0), (0, 0, 1)]) def test_remove_existing(self): create_cached_tile(Tile((0, 0, 1)), self.file_cache) assert self.tile_mgr.is_cached(Tile((0, 0, 1))) self.tile_mgr.remove_tile_coords([(0, 0, 0), (0, 0, 1)]) assert not self.tile_mgr.is_cached(Tile((0, 0, 1))) class TestTileManagerTiledSource(object): def setup(self): self.file_cache = MockFileCache('/dev/null', 'png') self.grid = TileGrid(SRS(4326), bbox=[-180, -90, 180, 90]) self.client = MockTileClient() self.source = TiledSource(self.grid, self.client) self.image_opts = ImageOptions(format='image/png') self.locker = TileLocker(tmp_lock_dir, 10, "id") self.tile_mgr = TileManager(self.grid, self.file_cache, [self.source], 'png', image_opts=self.image_opts, locker=self.locker, ) def test_create_tiles(self): self.tile_mgr.creator().create_tiles([Tile((0, 0, 1)), Tile((1, 0, 1))]) eq_(self.file_cache.stored_tiles, set([(0, 0, 1), (1, 0, 1)])) eq_(sorted(self.client.requested_tiles), [(0, 0, 1), (1, 0, 1)]) class TestTileManagerDifferentSourceGrid(object): def setup(self): self.file_cache = MockFileCache('/dev/null', 'png') self.grid = TileGrid(SRS(4326), bbox=[-180, -90, 180, 90]) self.source_grid = TileGrid(SRS(4326), bbox=[0, -90, 180, 90]) self.client = MockTileClient() self.source = TiledSource(self.source_grid, self.client) self.image_opts = ImageOptions(format='image/png') self.locker = TileLocker(tmp_lock_dir, 10, "id") self.tile_mgr = TileManager(self.grid, self.file_cache, [self.source], 'png', image_opts=self.image_opts, locker=self.locker, ) def test_create_tiles(self): self.tile_mgr.creator().create_tiles([Tile((1, 0, 1))]) eq_(self.file_cache.stored_tiles, set([(1, 0, 1)])) eq_(self.client.requested_tiles, [(0, 0, 0)]) @raises(InvalidSourceQuery) def test_create_tiles_out_of_bounds(self): self.tile_mgr.creator().create_tiles([Tile((0, 0, 0))]) class MockSource(MapLayer): def __init__(self, *args): MapLayer.__init__(self, *args) self.requested = [] def _image(self, size): return create_debug_img(size) def get_map(self, query): self.requested.append((query.bbox, query.size, query.srs)) return ImageSource(self._image(query.size)) class TestTileManagerSource(object): def setup(self): self.file_cache = MockFileCache('/dev/null', 'png') self.grid = TileGrid(SRS(4326), bbox=[-180, -90, 180, 90]) self.source = MockSource() self.image_opts = ImageOptions(format='image/png') self.locker = TileLocker(tmp_lock_dir, 10, "id") self.tile_mgr = TileManager(self.grid, self.file_cache, [self.source], 'png', image_opts=self.image_opts, locker=self.locker, ) def test_create_tile(self): self.tile_mgr.creator().create_tiles([Tile((0, 0, 1)), Tile((1, 0, 1))]) eq_(self.file_cache.stored_tiles, set([(0, 0, 1), (1, 0, 1)])) eq_(sorted(self.source.requested), [((-180.0, -90.0, 0.0, 90.0), (256, 256), SRS(4326)), ((0.0, -90.0, 180.0, 90.0), (256, 256), SRS(4326))]) class MockWMSClient(object): def __init__(self): self.requested = [] def retrieve(self, query, format): self.requested.append((query.bbox, query.size, query.srs)) return create_debug_img(query.size) class TestTileManagerWMSSource(object): def setup(self): self.file_cache = MockFileCache('/dev/null', 'png') self.grid = TileGrid(SRS(4326), bbox=[-180, -90, 180, 90]) self.client = MockWMSClient() self.source = WMSSource(self.client) self.image_opts = ImageOptions(format='image/png') self.locker = TileLocker(tmp_lock_dir, 10, "id") self.tile_mgr = TileManager(self.grid, self.file_cache, [self.source], 'png', meta_size=[2, 2], meta_buffer=0, image_opts=self.image_opts, locker=self.locker, ) def test_same_lock_for_meta_tile(self): eq_(self.tile_mgr.lock(Tile((0, 0, 1))).lock_file, self.tile_mgr.lock(Tile((1, 0, 1))).lock_file ) def test_locks_for_meta_tiles(self): assert_not_equal(self.tile_mgr.lock(Tile((0, 0, 2))).lock_file, self.tile_mgr.lock(Tile((2, 0, 2))).lock_file ) def test_create_tile_first_level(self): self.tile_mgr.creator().create_tiles([Tile((0, 0, 1)), Tile((1, 0, 1))]) eq_(self.file_cache.stored_tiles, set([(0, 0, 1), (1, 0, 1)])) eq_(self.client.requested, [((-180.0, -90.0, 180.0, 90.0), (512, 256), SRS(4326))]) def test_create_tile(self): self.tile_mgr.creator().create_tiles([Tile((0, 0, 2))]) eq_(self.file_cache.stored_tiles, set([(0, 0, 2), (1, 0, 2), (0, 1, 2), (1, 1, 2)])) eq_(sorted(self.client.requested), [((-180.0, -90.0, 0.0, 90.0), (512, 512), SRS(4326))]) def test_create_tiles(self): self.tile_mgr.creator().create_tiles([Tile((0, 0, 2)), Tile((2, 0, 2))]) eq_(self.file_cache.stored_tiles, set([(0, 0, 2), (1, 0, 2), (0, 1, 2), (1, 1, 2), (2, 0, 2), (3, 0, 2), (2, 1, 2), (3, 1, 2)])) eq_(sorted(self.client.requested), [((-180.0, -90.0, 0.0, 90.0), (512, 512), SRS(4326)), ((0.0, -90.0, 180.0, 90.0), (512, 512), SRS(4326))]) def test_load_tile_coords(self): tiles = self.tile_mgr.load_tile_coords(((0, 0, 2), (2, 0, 2))) eq_(tiles[0].coord, (0, 0, 2)) assert isinstance(tiles[0].source, ImageSource) eq_(tiles[1].coord, (2, 0, 2)) assert isinstance(tiles[1].source, ImageSource) eq_(self.file_cache.stored_tiles, set([(0, 0, 2), (1, 0, 2), (0, 1, 2), (1, 1, 2), (2, 0, 2), (3, 0, 2), (2, 1, 2), (3, 1, 2)])) eq_(sorted(self.client.requested), [((-180.0, -90.0, 0.0, 90.0), (512, 512), SRS(4326)), ((0.0, -90.0, 180.0, 90.0), (512, 512), SRS(4326))]) class TestTileManagerWMSSourceConcurrent(TestTileManagerWMSSource): def setup(self): TestTileManagerWMSSource.setup(self) self.tile_mgr.concurrent_tile_creators = 2 class TestTileManagerWMSSourceMinimalMetaRequests(object): def setup(self): self.file_cache = MockFileCache('/dev/null', 'png') self.grid = TileGrid(SRS(4326), bbox=[-180, -90, 180, 90]) self.client = MockWMSClient() self.source = WMSSource(self.client) self.locker = TileLocker(tmp_lock_dir, 10, "id") self.tile_mgr = TileManager(self.grid, self.file_cache, [self.source], 'png', meta_size=[2, 2], meta_buffer=10, minimize_meta_requests=True, locker=self.locker, ) def test_create_tile_single(self): # not enabled for single tile requests self.tile_mgr.creator().create_tiles([Tile((0, 0, 2))]) eq_(self.file_cache.stored_tiles, set([(0, 0, 2), (0, 1, 2), (1, 0, 2), (1, 1, 2)])) eq_(sorted(self.client.requested), [((-180.0, -90.0, 3.515625, 90.0), (522, 512), SRS(4326))]) def test_create_tile_multiple(self): self.tile_mgr.creator().create_tiles([Tile((4, 0, 3)), Tile((4, 1, 3)), Tile((4, 2, 3))]) eq_(self.file_cache.stored_tiles, set([(4, 0, 3), (4, 1, 3), (4, 2, 3)])) eq_(sorted(self.client.requested), [((-1.7578125, -90, 46.7578125, 46.7578125), (276, 778), SRS(4326))]) def test_create_tile_multiple_fragmented(self): self.tile_mgr.creator().create_tiles([Tile((4, 0, 3)), Tile((5, 2, 3))]) eq_(self.file_cache.stored_tiles, set([(4, 0, 3), (4, 1, 3), (4, 2, 3), (5, 0, 3), (5, 1, 3), (5, 2, 3)])) eq_(sorted(self.client.requested), [((-1.7578125, -90, 91.7578125, 46.7578125), (532, 778), SRS(4326))]) class SlowMockSource(MockSource): supports_meta_tiles = True def get_map(self, query): time.sleep(0.1) return MockSource.get_map(self, query) class TestTileManagerLocking(object): def setup(self): self.tile_dir = tempfile.mkdtemp() self.file_cache = MockFileCache(self.tile_dir, 'png') self.grid = TileGrid(SRS(4326), bbox=[-180, -90, 180, 90]) self.source = SlowMockSource() self.image_opts = ImageOptions(format='image/png') self.locker = TileLocker(tmp_lock_dir, 10, "id") self.tile_mgr = TileManager(self.grid, self.file_cache, [self.source], 'png', meta_size=[2, 2], meta_buffer=0, image_opts=self.image_opts, locker=self.locker, ) def test_get_single(self): self.tile_mgr.creator().create_tiles([Tile((0, 0, 1)), Tile((1, 0, 1))]) eq_(self.file_cache.stored_tiles, set([(0, 0, 1), (1, 0, 1)])) eq_(self.source.requested, [((-180.0, -90.0, 180.0, 90.0), (512, 256), SRS(4326))]) def test_concurrent(self): def do_it(): self.tile_mgr.creator().create_tiles([Tile((0, 0, 1)), Tile((1, 0, 1))]) threads = [threading.Thread(target=do_it) for _ in range(3)] [t.start() for t in threads] [t.join() for t in threads] eq_(self.file_cache.stored_tiles, set([(0, 0, 1), (1, 0, 1)])) eq_(self.file_cache.loaded_tiles, counting_set([(0, 0, 1), (1, 0, 1), (0, 0, 1), (1, 0, 1)])) eq_(self.source.requested, [((-180.0, -90.0, 180.0, 90.0), (512, 256), SRS(4326))]) assert os.path.exists(self.file_cache.tile_location(Tile((0, 0, 1)))) def teardown(self): shutil.rmtree(self.tile_dir) class TestTileManagerMultipleSources(object): def setup(self): self.file_cache = MockFileCache('/dev/null', 'png') self.grid = TileGrid(SRS(4326), bbox=[-180, -90, 180, 90]) self.source_base = MockSource() self.source_overlay = MockSource() self.image_opts = ImageOptions(format='image/png') self.locker = TileLocker(tmp_lock_dir, 10, "id") self.tile_mgr = TileManager(self.grid, self.file_cache, [self.source_base, self.source_overlay], 'png', image_opts=self.image_opts, locker=self.locker, ) self.layer = CacheMapLayer(self.tile_mgr) def test_get_single(self): self.tile_mgr.creator().create_tiles([Tile((0, 0, 1))]) eq_(self.file_cache.stored_tiles, set([(0, 0, 1)])) eq_(self.source_base.requested, [((-180.0, -90.0, 0.0, 90.0), (256, 256), SRS(4326))]) eq_(self.source_overlay.requested, [((-180.0, -90.0, 0.0, 90.0), (256, 256), SRS(4326))]) class SolidColorMockSource(MockSource): def __init__(self, color='#ff0000'): MockSource.__init__(self) self.color = color def _image(self, size): return Image.new('RGB', size, self.color) class TestTileManagerMultipleSourcesWithMetaTiles(object): def setup(self): self.file_cache = MockFileCache('/dev/null', 'png') self.grid = TileGrid(SRS(4326), bbox=[-180, -90, 180, 90]) self.source_base = SolidColorMockSource(color='#ff0000') self.source_base.supports_meta_tiles = True self.source_overlay = MockSource() self.source_overlay.supports_meta_tiles = True self.locker = TileLocker(tmp_lock_dir, 10, "id") self.tile_mgr = TileManager(self.grid, self.file_cache, [self.source_base, self.source_overlay], 'png', meta_size=[2, 2], meta_buffer=0, locker=self.locker, ) def test_merged_tiles(self): tiles = self.tile_mgr.creator().create_tiles([Tile((0, 0, 1)), Tile((1, 0, 1))]) eq_(self.file_cache.stored_tiles, set([(0, 0, 1), (1, 0, 1)])) eq_(self.source_base.requested, [((-180.0, -90.0, 180.0, 90.0), (512, 256), SRS(4326))]) eq_(self.source_overlay.requested, [((-180.0, -90.0, 180.0, 90.0), (512, 256), SRS(4326))]) hist = tiles[0].source.as_image().histogram() # lots of red (base), but not everything (overlay) assert 55000 < hist[255] < 60000 # red = 0xff assert 55000 < hist[256] # green = 0x00 assert 55000 < hist[512] # blue = 0x00 @raises(ValueError) def test_sources_with_mixed_support_for_meta_tiles(self): self.source_base.supports_meta_tiles = False self.locker = TileLocker(tmp_lock_dir, 10, "id") self.tile_mgr = TileManager(self.grid, self.file_cache, [self.source_base, self.source_overlay], 'png', meta_size=[2, 2], meta_buffer=0, locker=self.locker) def test_sources_with_no_support_for_meta_tiles(self): self.source_base.supports_meta_tiles = False self.source_overlay.supports_meta_tiles = False self.locker = TileLocker(tmp_lock_dir, 10, "id") self.tile_mgr = TileManager(self.grid, self.file_cache, [self.source_base, self.source_overlay], 'png', meta_size=[2, 2], meta_buffer=0, locker=self.locker) assert self.tile_mgr.meta_grid is None class TestTileManagerBulkMetaTiles(object): def setup(self): self.file_cache = MockFileCache('/dev/null', 'png') self.grid = TileGrid(SRS(4326), bbox=[-180, -90, 180, 90], origin='ul') self.source_base = SolidColorMockSource(color='#ff0000') self.source_base.supports_meta_tiles = False self.source_overlay = MockSource() self.source_overlay.supports_meta_tiles = False self.locker = TileLocker(tmp_lock_dir, 10, "id") self.tile_mgr = TileManager(self.grid, self.file_cache, [self.source_base, self.source_overlay], 'png', meta_size=[2, 2], meta_buffer=0, locker=self.locker, bulk_meta_tiles=True, ) def test_bulk_get(self): tiles = self.tile_mgr.creator().create_tiles([Tile((0, 0, 2))]) eq_(len(tiles), 2*2) eq_(self.file_cache.stored_tiles, set([(0, 0, 2), (1, 0, 2), (0, 1, 2), (1, 1, 2)])) for requested in [self.source_base.requested, self.source_overlay.requested]: eq_(set(requested), set([ ((-180.0, 0.0, -90.0, 90.0), (256, 256), SRS(4326)), ((-90.0, 0.0, 0.0, 90.0), (256, 256), SRS(4326)), ((-180.0, -90.0, -90.0, 0.0), (256, 256), SRS(4326)), ((-90.0, -90.0, 0.0, 0.0), (256, 256), SRS(4326)), ])) def test_bulk_get_error(self): self.tile_mgr.sources = [self.source_base, ErrorSource()] try: self.tile_mgr.creator().create_tiles([Tile((0, 0, 2))]) except Exception as ex: eq_(ex.args[0], "source error") def test_bulk_get_multiple_meta_tiles(self): tiles = self.tile_mgr.creator().create_tiles([Tile((1, 0, 2)), Tile((2, 0, 2))]) eq_(len(tiles), 2*2*2) eq_(self.file_cache.stored_tiles, set([ (0, 0, 2), (1, 0, 2), (0, 1, 2), (1, 1, 2), (2, 0, 2), (3, 0, 2), (2, 1, 2), (3, 1, 2), ])) class ErrorSource(MapLayer): def __init__(self, *args): MapLayer.__init__(self, *args) self.requested = [] def get_map(self, query): self.requested.append((query.bbox, query.size, query.srs)) raise Exception("source error") class TestTileManagerBulkMetaTilesConcurrent(TestTileManagerBulkMetaTiles): def setup(self): TestTileManagerBulkMetaTiles.setup(self) self.tile_mgr.concurrent_tile_creators = 2 default_image_opts = ImageOptions(resampling='bicubic') class TestCacheMapLayer(object): def setup(self): self.file_cache = MockFileCache('/dev/null', 'png') self.grid = TileGrid(SRS(4326), bbox=[-180, -90, 180, 90]) self.client = MockWMSClient() self.source = WMSSource(self.client) self.image_opts = ImageOptions(resampling='nearest') self.locker = TileLocker(tmp_lock_dir, 10, "id") self.tile_mgr = TileManager(self.grid, self.file_cache, [self.source], 'png', meta_size=[2, 2], meta_buffer=0, image_opts=self.image_opts, locker=self.locker) self.layer = CacheMapLayer(self.tile_mgr, image_opts=default_image_opts) def test_get_map_small(self): result = self.layer.get_map(MapQuery((-180, -90, 180, 90), (300, 150), SRS(4326), 'png')) eq_(self.file_cache.stored_tiles, set([(0, 0, 1), (1, 0, 1)])) eq_(result.size, (300, 150)) def test_get_map_large(self): # gets next resolution layer result = self.layer.get_map(MapQuery((-180, -90, 180, 90), (600, 300), SRS(4326), 'png')) eq_(self.file_cache.stored_tiles, set([(0, 0, 2), (1, 0, 2), (0, 1, 2), (1, 1, 2), (2, 0, 2), (3, 0, 2), (2, 1, 2), (3, 1, 2)])) eq_(result.size, (600, 300)) def test_transformed(self): result = self.layer.get_map(MapQuery( (-20037508.34, -20037508.34, 20037508.34, 20037508.34), (500, 500), SRS(900913), 'png')) eq_(self.file_cache.stored_tiles, set([(0, 0, 2), (1, 0, 2), (0, 1, 2), (1, 1, 2), (2, 0, 2), (3, 0, 2), (2, 1, 2), (3, 1, 2)])) eq_(result.size, (500, 500)) def test_single_tile_match(self): result = self.layer.get_map(MapQuery( (0.001, 0, 90, 90), (256, 256), SRS(4326), 'png', tiled_only=True)) eq_(self.file_cache.stored_tiles, set([(3, 0, 2), (2, 0, 2), (3, 1, 2), (2, 1, 2)])) eq_(result.size, (256, 256)) @raises(MapBBOXError) def test_single_tile_no_match(self): self.layer.get_map(MapQuery( (0.1, 0, 90, 90), (256, 256), SRS(4326), 'png', tiled_only=True)) def test_get_map_with_res_range(self): res_range = resolution_range(1000, 10) self.source = WMSSource(self.client, res_range=res_range) self.locker = TileLocker(tmp_lock_dir, 10, "id") self.tile_mgr = TileManager(self.grid, self.file_cache, [self.source], 'png', meta_size=[2, 2], meta_buffer=0, image_opts=self.image_opts, locker=self.locker) self.layer = CacheMapLayer(self.tile_mgr, image_opts=default_image_opts) try: result = self.layer.get_map(MapQuery( (-20037508.34, -20037508.34, 20037508.34, 20037508.34), (500, 500), SRS(900913), 'png')) except BlankImage: pass else: assert False, 'expected BlankImage exception' eq_(self.file_cache.stored_tiles, set()) result = self.layer.get_map(MapQuery( (0, 0, 10000, 10000), (50, 50), SRS(900913), 'png')) eq_(self.file_cache.stored_tiles, set([(512, 257, 10), (513, 256, 10), (512, 256, 10), (513, 257, 10)])) eq_(result.size, (50, 50)) class TestCacheMapLayerWithExtent(object): def setup(self): self.file_cache = MockFileCache('/dev/null', 'png') self.grid = TileGrid(SRS(4326), bbox=[-180, -90, 180, 90]) self.client = MockWMSClient() self.source = WMSSource(self.client) self.image_opts = ImageOptions(resampling='nearest', format='png') self.locker = TileLocker(tmp_lock_dir, 10, "id") self.tile_mgr = TileManager(self.grid, self.file_cache, [self.source], 'png', meta_size=[1, 1], meta_buffer=0, image_opts=self.image_opts, locker=self.locker) self.layer = CacheMapLayer(self.tile_mgr, image_opts=default_image_opts) self.layer.extent = BBOXCoverage([0, 0, 90, 45], SRS(4326)).extent def test_get_outside_extent(self): assert_raises(BlankImage, self.layer.get_map, MapQuery((-180, -90, 0, 0), (300, 150), SRS(4326), 'png')) def test_get_map_small(self): result = self.layer.get_map(MapQuery((-180, -90, 180, 90), (300, 150), SRS(4326), 'png')) eq_(self.file_cache.stored_tiles, set([(1, 0, 1)])) # source requests one tile (no meta-tiling configured) eq_(self.client.requested, [((0.0, -90.0, 180.0, 90.0), (256, 256), SRS('EPSG:4326'))]) eq_(result.size, (300, 150)) def test_get_map_small_with_source_extent(self): self.source.extent = BBOXCoverage([0, 0, 90, 45], SRS(4326)).extent result = self.layer.get_map(MapQuery((-180, -90, 180, 90), (300, 150), SRS(4326), 'png')) eq_(self.file_cache.stored_tiles, set([(1, 0, 1)])) # source requests one tile (no meta-tiling configured) limited to source.extent eq_(self.client.requested, [((0, 0, 90, 45), (128, 64), (SRS(4326)))]) eq_(result.size, (300, 150)) class TestDirectMapLayer(object): def setup(self): self.client = MockWMSClient() self.source = WMSSource(self.client) self.layer = DirectMapLayer(self.source, GLOBAL_GEOGRAPHIC_EXTENT) def test_get_map(self): result = self.layer.get_map(MapQuery((-180, -90, 180, 90), (300, 150), SRS(4326), 'png')) eq_(self.client.requested, [((-180, -90, 180, 90), (300, 150), SRS(4326))]) eq_(result.size, (300, 150)) def test_get_map_mercator(self): result = self.layer.get_map(MapQuery( (-20037508.34, -20037508.34, 20037508.34, 20037508.34), (500, 500), SRS(900913), 'png')) eq_(self.client.requested, [((-20037508.34, -20037508.34, 20037508.34, 20037508.34), (500, 500), SRS(900913))]) eq_(result.size, (500, 500)) class TestDirectMapLayerWithSupportedSRS(object): def setup(self): self.client = MockWMSClient() self.source = WMSSource(self.client) self.layer = DirectMapLayer(self.source, GLOBAL_GEOGRAPHIC_EXTENT) def test_get_map(self): result = self.layer.get_map(MapQuery((-180, -90, 180, 90), (300, 150), SRS(4326), 'png')) eq_(self.client.requested, [((-180, -90, 180, 90), (300, 150), SRS(4326))]) eq_(result.size, (300, 150)) def test_get_map_mercator(self): result = self.layer.get_map(MapQuery( (-20037508.34, -20037508.34, 20037508.34, 20037508.34), (500, 500), SRS(900913), 'png')) eq_(self.client.requested, [((-20037508.34, -20037508.34, 20037508.34, 20037508.34), (500, 500), SRS(900913))]) eq_(result.size, (500, 500)) class MockHTTPClient(object): def __init__(self): self.requested = [] def open(self, url, data=None): self.requested.append(url) w = int(re.search(r'width=(\d+)', url, re.IGNORECASE).group(1)) h = int(re.search(r'height=(\d+)', url, re.IGNORECASE).group(1)) format = re.search(r'format=image(/|%2F)(\w+)', url, re.IGNORECASE).group(2) transparent = re.search(r'transparent=(\w+)', url, re.IGNORECASE) transparent = True if transparent and transparent.group(1).lower() == 'true' else False result = BytesIO() create_debug_img((int(w), int(h)), transparent).save(result, format=format) result.seek(0) result.headers = {'Content-type': 'image/'+format} return result class TestWMSSourceTransform(object): def setup(self): self.http_client = MockHTTPClient() self.req_template = WMS111MapRequest(url='http://localhost/service?', param={ 'format': 'image/png', 'layers': 'foo' }) self.client = WMSClient(self.req_template, http_client=self.http_client) self.source = WMSSource(self.client, supported_srs=[SRS(4326)], image_opts=ImageOptions(resampling='bilinear')) def test_get_map(self): self.source.get_map(MapQuery((-180, -90, 180, 90), (300, 150), SRS(4326))) assert query_eq(self.http_client.requested[0], "http://localhost/service?" "layers=foo&width=300&version=1.1.1&bbox=-180,-90,180,90&service=WMS" "&format=image%2Fpng&styles=&srs=EPSG%3A4326&request=GetMap&height=150") def test_get_map_transformed(self): self.source.get_map(MapQuery( (556597, 4865942, 1669792, 7361866), (300, 150), SRS(900913))) assert wms_query_eq(self.http_client.requested[0], "http://localhost/service?" "layers=foo&width=300&version=1.1.1" "&bbox=4.99999592195,39.9999980766,14.999996749,54.9999994175&service=WMS" "&format=image%2Fpng&styles=&srs=EPSG%3A4326&request=GetMap&height=450") class TestWMSSourceWithClient(object): def setup(self): self.req_template = WMS111MapRequest( url='http://%s:%d/service?' % TEST_SERVER_ADDRESS, param={'format': 'image/png', 'layers': 'foo'}) self.client = WMSClient(self.req_template) self.source = WMSSource(self.client) def test_get_map(self): with tmp_image((512, 512)) as img: expected_req = ({'path': r'/service?LAYERS=foo&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=512&SRS=EPSG%3A4326&styles=' '&VERSION=1.1.1&BBOX=0.0,10.0,10.0,20.0&WIDTH=512'}, {'body': img.read(), 'headers': {'content-type': 'image/png'}}) with mock_httpd(TEST_SERVER_ADDRESS, [expected_req]): q = MapQuery((0.0, 10.0, 10.0, 20.0), (512, 512), SRS(4326)) result = self.source.get_map(q) assert isinstance(result, ImageSource) eq_(result.size, (512, 512)) assert is_png(result.as_buffer(seekable=True)) eq_(result.as_image().size, (512, 512)) def test_get_map_non_image_content_type(self): with tmp_image((512, 512)) as img: expected_req = ({'path': r'/service?LAYERS=foo&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=512&SRS=EPSG%3A4326&styles=' '&VERSION=1.1.1&BBOX=0.0,10.0,10.0,20.0&WIDTH=512'}, {'body': img.read(), 'headers': {'content-type': 'text/plain'}}) with mock_httpd(TEST_SERVER_ADDRESS, [expected_req]): q = MapQuery((0.0, 10.0, 10.0, 20.0), (512, 512), SRS(4326)) try: self.source.get_map(q) except SourceError as e: assert 'no image returned' in e.args[0] else: assert False, 'no SourceError raised' def test_basic_auth(self): http_client = HTTPClient(self.req_template.url, username='foo', password='bar@') self.client.http_client = http_client def assert_auth(req_handler): assert 'Authorization' in req_handler.headers auth_data = req_handler.headers['Authorization'].split()[1] auth_data = base64.b64decode(auth_data.encode('utf-8')).decode('utf-8') eq_(auth_data, 'foo:bar@') return True expected_req = ({'path': r'/service?LAYERS=foo&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=512&SRS=EPSG%3A4326' '&VERSION=1.1.1&BBOX=0.0,10.0,10.0,20.0&WIDTH=512&STYLES=', 'require_basic_auth': True, 'req_assert_function': assert_auth}, {'body': b'no image', 'headers': {'content-type': 'image/png'}}) with mock_httpd(TEST_SERVER_ADDRESS, [expected_req]): q = MapQuery((0.0, 10.0, 10.0, 20.0), (512, 512), SRS(4326)) self.source.get_map(q) TESTSERVER_URL = 'http://%s:%d' % TEST_SERVER_ADDRESS class TestWMSSource(object): def setup(self): self.req = WMS111MapRequest(url=TESTSERVER_URL + '/service?map=foo', param={'layers':'foo'}) self.http = MockHTTPClient() self.wms = WMSClient(self.req, http_client=self.http) self.source = WMSSource(self.wms, supported_srs=[SRS(4326)], image_opts=ImageOptions(resampling='bilinear')) def test_request(self): req = MapQuery((-180.0, -90.0, 180.0, 90.0), (512, 256), SRS(4326), 'png') self.source.get_map(req) eq_(len(self.http.requested), 1) assert_query_eq(self.http.requested[0], TESTSERVER_URL+'/service?map=foo&LAYERS=foo&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=256&SRS=EPSG%3A4326' '&VERSION=1.1.1&BBOX=-180.0,-90.0,180.0,90.0&WIDTH=512&STYLES=') def test_transformed_request(self): req = MapQuery((-200000, -200000, 200000, 200000), (512, 512), SRS(900913), 'png') resp = self.source.get_map(req) eq_(len(self.http.requested), 1) assert wms_query_eq(self.http.requested[0], TESTSERVER_URL+'/service?map=foo&LAYERS=foo&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=512&SRS=EPSG%3A4326' '&VERSION=1.1.1&WIDTH=512&STYLES=' '&BBOX=-1.79663056824,-1.7963362121,1.79663056824,1.7963362121') img = resp.as_image() assert img.mode in ('P', 'RGB') def test_similar_srs(self): # request in 3857 and source supports only 900913 # 3857 and 900913 are equal but the client requests must use 900913 self.req = WMS111MapRequest(url=TESTSERVER_URL + '/service?map=foo', param={'layers':'foo', 'transparent': 'true'}) self.wms = WMSClient(self.req, http_client=self.http) self.source = WMSSource(self.wms, supported_srs=[SRS(900913)], image_opts=ImageOptions(resampling='bilinear')) req = MapQuery((-200000, -200000, 200000, 200000), (512, 512), SRS(3857), 'png') self.source.get_map(req) eq_(len(self.http.requested), 1) assert_query_eq(self.http.requested[0], TESTSERVER_URL+'/service?map=foo&LAYERS=foo&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=512&SRS=EPSG%3A900913' '&VERSION=1.1.1&WIDTH=512&STYLES=&transparent=true' '&BBOX=-200000,-200000,200000,200000') def test_transformed_request_transparent(self): self.req = WMS111MapRequest(url=TESTSERVER_URL + '/service?map=foo', param={'layers':'foo', 'transparent': 'true'}) self.wms = WMSClient(self.req, http_client=self.http) self.source = WMSSource(self.wms, supported_srs=[SRS(4326)], image_opts=ImageOptions(resampling='bilinear')) req = MapQuery((-200000, -200000, 200000, 200000), (512, 512), SRS(900913), 'png') resp = self.source.get_map(req) eq_(len(self.http.requested), 1) assert wms_query_eq(self.http.requested[0], TESTSERVER_URL+'/service?map=foo&LAYERS=foo&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetMap&HEIGHT=512&SRS=EPSG%3A4326' '&VERSION=1.1.1&WIDTH=512&STYLES=&transparent=true' '&BBOX=-1.79663056824,-1.7963362121,1.79663056824,1.7963362121') img = resp.as_image() assert img.mode in ('P', 'RGBA') img = img.convert('RGBA') eq_(img.getpixel((5, 5))[3], 0) class MockLayer(object): def __init__(self): self.requested = [] def get_map(self, query): self.requested.append((query.bbox, query.size, query.srs)) class TestResolutionConditionalLayers(object): def setup(self): self.low = MockLayer() self.high = MockLayer() self.layer = ResolutionConditional(self.low, self.high, 10, SRS(900913), GLOBAL_GEOGRAPHIC_EXTENT) def test_resolution_low(self): self.layer.get_map(MapQuery((0, 0, 10000, 10000), (100, 100), SRS(900913))) assert self.low.requested assert not self.high.requested def test_resolution_high(self): self.layer.get_map(MapQuery((0, 0, 100, 100), (100, 100), SRS(900913))) assert not self.low.requested assert self.high.requested def test_resolution_match(self): self.layer.get_map(MapQuery((0, 0, 10, 10), (100, 100), SRS(900913))) assert not self.low.requested assert self.high.requested def test_resolution_low_transform(self): self.layer.get_map(MapQuery((0, 0, 0.1, 0.1), (100, 100), SRS(4326))) assert self.low.requested assert not self.high.requested def test_resolution_high_transform(self): self.layer.get_map(MapQuery((0, 0, 0.005, 0.005), (100, 100), SRS(4326))) assert not self.low.requested assert self.high.requested class TestSRSConditionalLayers(object): def setup(self): self.l4326 = MockLayer() self.l900913 = MockLayer() self.l32632 = MockLayer() self.layer = SRSConditional([ (self.l4326, (SRS('EPSG:4326'),)), (self.l900913, (SRS('EPSG:900913'), SRS('EPSG:31467'))), (self.l32632, (SRSConditional.PROJECTED,)), ], GLOBAL_GEOGRAPHIC_EXTENT) def test_srs_match(self): assert self.layer._select_layer(SRS(4326)) == self.l4326 assert self.layer._select_layer(SRS(900913)) == self.l900913 assert self.layer._select_layer(SRS(31467)) == self.l900913 def test_srs_match_type(self): assert self.layer._select_layer(SRS(31466)) == self.l32632 assert self.layer._select_layer(SRS(32633)) == self.l32632 def test_no_match_first_type(self): assert self.layer._select_layer(SRS(4258)) == self.l4326 class TestNeastedConditionalLayers(object): def setup(self): self.direct = MockLayer() self.l900913 = MockLayer() self.l4326 = MockLayer() self.layer = ResolutionConditional( SRSConditional([ (self.l900913, (SRS('EPSG:900913'),)), (self.l4326, (SRS('EPSG:4326'),)) ], GLOBAL_GEOGRAPHIC_EXTENT), self.direct, 10, SRS(900913), GLOBAL_GEOGRAPHIC_EXTENT ) def test_resolution_high_900913(self): self.layer.get_map(MapQuery((0, 0, 100, 100), (100, 100), SRS(900913))) assert self.direct.requested def test_resolution_high_4326(self): self.layer.get_map(MapQuery((0, 0, 0.0001, 0.0001), (100, 100), SRS(4326))) assert self.direct.requested def test_resolution_low_4326(self): self.layer.get_map(MapQuery((0, 0, 10, 10), (100, 100), SRS(4326))) assert self.l4326.requested def test_resolution_low_projected(self): self.layer.get_map(MapQuery((0, 0, 10000, 10000), (100, 100), SRS(31467))) assert self.l900913.requestedmapproxy-1.11.0/mapproxy/test/unit/test_cache_compact.py000066400000000000000000000330501320454472400234560ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2016 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import os import time import struct import shutil import tempfile from io import BytesIO from mapproxy.cache.compact import CompactCacheV1, CompactCacheV2 from mapproxy.cache.tile import Tile from mapproxy.image import ImageSource from mapproxy.image.opts import ImageOptions from mapproxy.script.defrag import defrag_compact_cache from mapproxy.test.unit.test_cache_tile import TileCacheTestBase from nose.tools import eq_ class TestCompactCacheV1(TileCacheTestBase): always_loads_metadata = True def setup(self): TileCacheTestBase.setup(self) self.cache = CompactCacheV1( cache_dir=self.cache_dir, ) def test_bundle_files(self): assert not os.path.exists(os.path.join(self.cache_dir, 'L00', 'R0000C0000.bundle')) assert not os.path.exists(os.path.join(self.cache_dir, 'L00', 'R0000C0000.bundlx')) self.cache.store_tile(self.create_tile(coord=(0, 0, 0))) assert os.path.exists(os.path.join(self.cache_dir, 'L00', 'R0000C0000.bundle')) assert os.path.exists(os.path.join(self.cache_dir, 'L00', 'R0000C0000.bundlx')) assert not os.path.exists(os.path.join(self.cache_dir, 'L12', 'R0000C0000.bundle')) assert not os.path.exists(os.path.join(self.cache_dir, 'L12', 'R0000C0000.bundlx')) self.cache.store_tile(self.create_tile(coord=(127, 127, 12))) assert os.path.exists(os.path.join(self.cache_dir, 'L12', 'R0000C0000.bundle')) assert os.path.exists(os.path.join(self.cache_dir, 'L12', 'R0000C0000.bundlx')) assert not os.path.exists(os.path.join(self.cache_dir, 'L12', 'R0100C0080.bundle')) assert not os.path.exists(os.path.join(self.cache_dir, 'L12', 'R0100C0080.bundlx')) self.cache.store_tile(self.create_tile(coord=(128, 256, 12))) assert os.path.exists(os.path.join(self.cache_dir, 'L12', 'R0100C0080.bundle')) assert os.path.exists(os.path.join(self.cache_dir, 'L12', 'R0100C0080.bundlx')) def test_bundle_files_not_created_on_is_cached(self): assert not os.path.exists(os.path.join(self.cache_dir, 'L00', 'R0000C0000.bundle')) assert not os.path.exists(os.path.join(self.cache_dir, 'L00', 'R0000C0000.bundlx')) self.cache.is_cached(Tile(coord=(0, 0, 0))) assert not os.path.exists(os.path.join(self.cache_dir, 'L00', 'R0000C0000.bundle')) assert not os.path.exists(os.path.join(self.cache_dir, 'L00', 'R0000C0000.bundlx')) def test_missing_tiles(self): self.cache.store_tile(self.create_tile(coord=(130, 200, 8))) assert os.path.exists(os.path.join(self.cache_dir, 'L08', 'R0080C0080.bundle')) assert os.path.exists(os.path.join(self.cache_dir, 'L08', 'R0080C0080.bundlx')) # test that all other tiles in this bundle are missing assert self.cache.is_cached(Tile((130, 200, 8))) for x in range(128, 255): for y in range(128, 255): if x == 130 and y == 200: continue assert not self.cache.is_cached(Tile((x, y, 8))), (x, y) assert not self.cache.load_tile(Tile((x, y, 8))), (x, y) def test_remove_level_tiles_before(self): self.cache.store_tile(self.create_tile(coord=(0, 0, 12))) assert os.path.exists(os.path.join(self.cache_dir, 'L12', 'R0000C0000.bundle')) assert os.path.exists(os.path.join(self.cache_dir, 'L12', 'R0000C0000.bundlx')) # not removed with timestamp self.cache.remove_level_tiles_before(12, time.time()) assert os.path.exists(os.path.join(self.cache_dir, 'L12', 'R0000C0000.bundle')) assert os.path.exists(os.path.join(self.cache_dir, 'L12', 'R0000C0000.bundlx')) # removed with timestamp=0 (remove_all:true in seed.yaml) self.cache.remove_level_tiles_before(12, 0) assert not os.path.exists(os.path.join(self.cache_dir, 'L12')) def test_bundle_header(self): t = Tile((5000, 1000, 12), ImageSource(BytesIO(b'a' * 4000), image_opts=ImageOptions(format='image/png'))) self.cache.store_tile(t) assert os.path.exists(os.path.join(self.cache_dir, 'L12', 'R0380C1380.bundle')) assert os.path.exists(os.path.join(self.cache_dir, 'L12', 'R0380C1380.bundlx')) def assert_header(tile_bytes_written, max_tile_bytes): with open(os.path.join(self.cache_dir, 'L12', 'R0380C1380.bundle'), 'r+b') as f: header = struct.unpack(' # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import re import os import time import random from nose.plugins.skip import SkipTest from mapproxy.cache.couchdb import CouchDBCache, CouchDBMDTemplate from mapproxy.cache.tile import Tile from mapproxy.grid import tile_grid from mapproxy.test.image import create_tmp_image_buf from mapproxy.test.unit.test_cache_tile import TileCacheTestBase from nose.tools import assert_almost_equal, eq_ tile_image = create_tmp_image_buf((256, 256), color='blue') tile_image2 = create_tmp_image_buf((256, 256), color='red') class TestCouchDBCache(TileCacheTestBase): always_loads_metadata = True def setup(self): if not os.environ.get('MAPPROXY_TEST_COUCHDB'): raise SkipTest() couch_address = os.environ['MAPPROXY_TEST_COUCHDB'] db_name = 'mapproxy_test_%d' % random.randint(0, 100000) TileCacheTestBase.setup(self) md_template = CouchDBMDTemplate({'row': '{{y}}', 'tile_column': '{{x}}', 'zoom': '{{level}}', 'time': '{{timestamp}}', 'coord': '{{wgs_tile_centroid}}'}) self.cache = CouchDBCache(couch_address, db_name, file_ext='png', tile_grid=tile_grid(3857, name='global-webmarcator'), md_template=md_template) def teardown(self): import requests requests.delete(self.cache.couch_url) TileCacheTestBase.teardown(self) def test_store_bulk_with_overwrite(self): tile = self.create_tile((0, 0, 4)) self.create_cached_tile(tile) assert self.cache.is_cached(Tile((0, 0, 4))) loaded_tile = Tile((0, 0, 4)) assert self.cache.load_tile(loaded_tile) assert loaded_tile.source_buffer().read() == tile.source_buffer().read() assert not self.cache.is_cached(Tile((1, 0, 4))) tiles = [self.create_another_tile((x, 0, 4)) for x in range(2)] assert self.cache.store_tiles(tiles) assert self.cache.is_cached(Tile((0, 0, 4))) loaded_tile = Tile((0, 0, 4)) assert self.cache.load_tile(loaded_tile) # check that tile is overwritten assert loaded_tile.source_buffer().read() != tile.source_buffer().read() assert loaded_tile.source_buffer().read() == tiles[0].source_buffer().read() def test_double_remove(self): tile = self.create_tile() self.create_cached_tile(tile) assert self.cache.remove_tile(tile) assert self.cache.remove_tile(tile) class TestCouchDBMDTemplate(object): def test_empty(self): template = CouchDBMDTemplate({}) doc = template.doc(Tile((0, 0, 1)), tile_grid(4326)) assert_almost_equal(doc['timestamp'], time.time(), 2) def test_fixed_values(self): template = CouchDBMDTemplate({'hello': 'world', 'foo': 123}) doc = template.doc(Tile((0, 0, 1)), tile_grid(4326)) assert_almost_equal(doc['timestamp'], time.time(), 2) eq_(doc['hello'], 'world') eq_(doc['foo'], 123) def test_template_values(self): template = CouchDBMDTemplate({'row': '{{y}}', 'tile_column': '{{x}}', 'zoom': '{{level}}', 'time': '{{timestamp}}', 'coord': '{{wgs_tile_centroid}}', 'datetime': '{{utc_iso}}', 'coord_webmerc': '{{tile_centroid}}'}) doc = template.doc(Tile((1, 0, 2)), tile_grid(3857)) assert_almost_equal(doc['time'], time.time(), 2) assert 'timestamp' not in doc eq_(doc['row'], 0) eq_(doc['tile_column'], 1) eq_(doc['zoom'], 2) assert_almost_equal(doc['coord'][0], -45.0) assert_almost_equal(doc['coord'][1], -79.17133464081945) assert_almost_equal(doc['coord_webmerc'][0], -5009377.085697311) assert_almost_equal(doc['coord_webmerc'][1], -15028131.257091932) assert re.match('20\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ', doc['datetime']), doc['datetime']mapproxy-1.11.0/mapproxy/test/unit/test_cache_geopackage.py000066400000000000000000000201201320454472400241100ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2016 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import os import time import sqlite3 import threading from io import BytesIO from mapproxy.image import ImageSource from mapproxy.cache.geopackage import GeopackageCache, GeopackageLevelCache from mapproxy.cache.tile import Tile from mapproxy.grid import tile_grid, TileGrid from mapproxy.test.unit.test_cache_tile import TileCacheTestBase from nose.tools import eq_ class TestGeopackageCache(TileCacheTestBase): always_loads_metadata = True def setup(self): TileCacheTestBase.setup(self) self.gpkg_file = os.path.join(self.cache_dir, 'tmp.gpkg') self.table_name = 'test_tiles' self.cache = GeopackageCache( self.gpkg_file, tile_grid=tile_grid(3857, name='global-webmarcator'), table_name=self.table_name, ) def teardown(self): if self.cache: self.cache.cleanup() TileCacheTestBase.teardown(self) def test_new_geopackage(self): assert os.path.exists(self.gpkg_file) with sqlite3.connect(self.gpkg_file) as db: cur = db.execute('''SELECT name FROM sqlite_master WHERE type='table' AND name=?''', (self.table_name,)) content = cur.fetchone() assert content[0] == self.table_name with sqlite3.connect(self.gpkg_file) as db: cur = db.execute('''SELECT table_name, data_type FROM gpkg_contents WHERE table_name = ?''', (self.table_name,)) content = cur.fetchone() assert content[0] == self.table_name assert content[1] == 'tiles' with sqlite3.connect(self.gpkg_file) as db: cur = db.execute('''SELECT table_name FROM gpkg_tile_matrix WHERE table_name = ?''', (self.table_name,)) content = cur.fetchall() assert len(content) == 20 with sqlite3.connect(self.gpkg_file) as db: cur = db.execute('''SELECT table_name FROM gpkg_tile_matrix_set WHERE table_name = ?''', (self.table_name,)) content = cur.fetchone() assert content[0] == self.table_name def test_load_empty_tileset(self): assert self.cache.load_tiles([Tile(None)]) == True assert self.cache.load_tiles([Tile(None), Tile(None), Tile(None)]) == True def test_load_more_than_2000_tiles(self): # prepare data for i in range(0, 2010): assert self.cache.store_tile(Tile((i, 0, 10), ImageSource(BytesIO(b'foo')))) tiles = [Tile((i, 0, 10)) for i in range(0, 2010)] assert self.cache.load_tiles(tiles) def test_timeouts(self): self.cache._db_conn_cache.db = sqlite3.connect(self.cache.geopackage_file, timeout=0.05) def block(): # block database by delaying the commit db = sqlite3.connect(self.cache.geopackage_file) cur = db.cursor() stmt = "INSERT OR REPLACE INTO {0} (zoom_level, tile_column, tile_row, tile_data) " \ "VALUES (?,?,?,?)".format(self.table_name) cur.execute(stmt, (3, 1, 1, '1234')) time.sleep(0.2) db.commit() try: assert self.cache.store_tile(self.create_tile((0, 0, 1))) == True t = threading.Thread(target=block) t.start() time.sleep(0.05) assert self.cache.store_tile(self.create_tile((0, 0, 1))) == False finally: t.join() assert self.cache.store_tile(self.create_tile((0, 0, 1))) == True class TestGeopackageLevelCache(TileCacheTestBase): always_loads_metadata = True def setup(self): TileCacheTestBase.setup(self) self.cache = GeopackageLevelCache( self.cache_dir, tile_grid=tile_grid(3857, name='global-webmarcator'), table_name='test_tiles', ) def teardown(self): if self.cache: self.cache.cleanup() TileCacheTestBase.teardown(self) def test_level_files(self): if os.path.exists(self.cache_dir): eq_(os.listdir(self.cache_dir), []) self.cache.store_tile(self.create_tile((0, 0, 1))) eq_(os.listdir(self.cache_dir), ['1.gpkg']) self.cache.store_tile(self.create_tile((0, 0, 5))) eq_(sorted(os.listdir(self.cache_dir)), ['1.gpkg', '5.gpkg']) def test_remove_level_files(self): self.cache.store_tile(self.create_tile((0, 0, 1))) self.cache.store_tile(self.create_tile((0, 0, 2))) eq_(sorted(os.listdir(self.cache_dir)), ['1.gpkg', '2.gpkg']) self.cache.remove_level_tiles_before(1, timestamp=0) eq_(os.listdir(self.cache_dir), ['2.gpkg']) def test_remove_level_tiles_before(self): self.cache.store_tile(self.create_tile((0, 0, 1))) self.cache.store_tile(self.create_tile((0, 0, 2))) eq_(sorted(os.listdir(self.cache_dir)), ['1.gpkg', '2.gpkg']) assert self.cache.is_cached(Tile((0, 0, 1))) self.cache.remove_level_tiles_before(1, timestamp=time.time() - 60) assert self.cache.is_cached(Tile((0, 0, 1))) self.cache.remove_level_tiles_before(1, timestamp=0) assert not self.cache.is_cached(Tile((0, 0, 1))) eq_(sorted(os.listdir(self.cache_dir)), ['1.gpkg', '2.gpkg']) assert self.cache.is_cached(Tile((0, 0, 2))) def test_bulk_store_tiles_with_different_levels(self): self.cache.store_tiles([ self.create_tile((0, 0, 1)), self.create_tile((0, 0, 2)), self.create_tile((1, 0, 2)), self.create_tile((1, 0, 1)), ]) eq_(sorted(os.listdir(self.cache_dir)), ['1.gpkg', '2.gpkg']) assert self.cache.is_cached(Tile((0, 0, 1))) assert self.cache.is_cached(Tile((1, 0, 1))) assert self.cache.is_cached(Tile((0, 0, 2))) assert self.cache.is_cached(Tile((1, 0, 2))) class TestGeopackageCacheInitErrors(object): table_name = 'cache' def test_bad_config_geopackage_srs(self): error_msg = None gpkg_file = os.path.join(os.path.join(os.path.dirname(__file__), 'fixture'), 'cache.gpkg') table_name = 'cache' try: GeopackageCache(gpkg_file, TileGrid(srs=4326), table_name) except ValueError as ve: error_msg = ve assert "srs is improperly configured." in str(error_msg) def test_bad_config_geopackage_tile(self): error_msg = None gpkg_file = os.path.join(os.path.join(os.path.dirname(__file__), 'fixture'), 'cache.gpkg') table_name = 'cache' try: GeopackageCache(gpkg_file, TileGrid(srs=900913, tile_size=(512, 512)), table_name) except ValueError as ve: error_msg = ve assert "tile_size is improperly configured." in str(error_msg) def test_bad_config_geopackage_res(self): error_msg = None gpkg_file = os.path.join(os.path.join(os.path.dirname(__file__), 'fixture'), 'cache.gpkg') table_name = 'cache' try: GeopackageCache(gpkg_file, TileGrid(srs=900913, res=[1000, 100, 10]), table_name) except ValueError as ve: error_msg = ve assert "res is improperly configured." in str(error_msg) mapproxy-1.11.0/mapproxy/test/unit/test_cache_redis.py000066400000000000000000000043411320454472400231370ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2017 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. try: import redis except ImportError: redis = None import time import os from nose.plugins.skip import SkipTest from mapproxy.cache.tile import Tile from mapproxy.cache.redis import RedisCache from mapproxy.test.unit.test_cache_tile import TileCacheTestBase class TestRedisCache(TileCacheTestBase): always_loads_metadata = False def setup(self): if not redis: raise SkipTest("redis required for Redis tests") redis_host = os.environ.get('MAPPROXY_TEST_REDIS') if not redis_host: raise SkipTest() self.host, self.port = redis_host.split(':') TileCacheTestBase.setup(self) self.cache = RedisCache(self.host, int(self.port), prefix='mapproxy-test', db=1) def teardown(self): for k in self.cache.r.keys('mapproxy-test-*'): self.cache.r.delete(k) def test_expire(self): cache = RedisCache(self.host, int(self.port), prefix='mapproxy-test', db=1, ttl=0) t1 = self.create_tile(coord=(9382, 1234, 9)) assert cache.store_tile(t1) time.sleep(0.1) t2 = Tile(t1.coord) assert cache.is_cached(t2) cache = RedisCache(self.host, int(self.port), prefix='mapproxy-test', db=1, ttl=0.05) t1 = self.create_tile(coord=(5382, 2234, 9)) assert cache.store_tile(t1) time.sleep(0.1) t2 = Tile(t1.coord) assert not cache.is_cached(t2) def test_double_remove(self): tile = self.create_tile() self.create_cached_tile(tile) assert self.cache.remove_tile(tile) assert self.cache.remove_tile(tile) mapproxy-1.11.0/mapproxy/test/unit/test_cache_riak.py000066400000000000000000000046301320454472400227600ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2013 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import random from nose.plugins.skip import SkipTest from mapproxy.cache.riak import RiakCache from mapproxy.grid import tile_grid from mapproxy.compat.modules import urlparse from mapproxy.test.image import create_tmp_image_buf from mapproxy.test.unit.test_cache_tile import TileCacheTestBase tile_image = create_tmp_image_buf((256, 256), color='blue') tile_image2 = create_tmp_image_buf((256, 256), color='red') class RiakCacheTestBase(TileCacheTestBase): always_loads_metadata = True def setup(self): if not os.environ.get(self.riak_url_env): raise SkipTest() url = os.environ[self.riak_url_env] urlparts = urlparse.urlparse(url) protocol = urlparts.scheme.lower() node = {'host': urlparts.hostname} if ':' in urlparts.hostname: if protocol == 'pbc': node['pb_port'] = urlparts.port if protocol in ('http', 'https'): node['http_port'] = urlparts.port db_name = 'mapproxy_test_%d' % random.randint(0, 100000) TileCacheTestBase.setup(self) self.cache = RiakCache([node], protocol, db_name, tile_grid=tile_grid(3857, name='global-webmarcator')) def teardown(self): import riak bucket = self.cache.bucket for k in bucket.get_keys(): riak.RiakObject(self.cache.connection, bucket, k).delete() TileCacheTestBase.teardown(self) def test_double_remove(self): tile = self.create_tile() self.create_cached_tile(tile) assert self.cache.remove_tile(tile) assert self.cache.remove_tile(tile) class TestRiakCacheHTTP(RiakCacheTestBase): riak_url_env = 'MAPPROXY_TEST_RIAK_HTTP' class TestRiakCachePBC(RiakCacheTestBase): riak_url_env = 'MAPPROXY_TEST_RIAK_PBC'mapproxy-1.11.0/mapproxy/test/unit/test_cache_s3.py000066400000000000000000000066451320454472400223670ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. try: import boto3 from moto import mock_s3 except ImportError: boto3 = None mock_s3 = None from nose.plugins.skip import SkipTest from mapproxy.cache.s3 import S3Cache from mapproxy.test.unit.test_cache_tile import TileCacheTestBase class TestS3Cache(TileCacheTestBase): always_loads_metadata = True uses_utc = True def setup(self): if not mock_s3 or not boto3: raise SkipTest("boto3 and moto required for S3 tests") TileCacheTestBase.setup(self) self.mock = mock_s3() self.mock.start() self.bucket_name = "test" dir_name = 'mapproxy' boto3.client("s3").create_bucket(Bucket=self.bucket_name) self.cache = S3Cache(dir_name, file_ext='png', directory_layout='tms', bucket_name=self.bucket_name, profile_name=None, _concurrent_writer=1, # moto is not thread safe ) def teardown(self): self.mock.stop() TileCacheTestBase.teardown(self) def check_tile_key(self, layout, tile_coord, key): cache = S3Cache('/mycache/webmercator', 'png', bucket_name=self.bucket_name, directory_layout=layout) cache.store_tile(self.create_tile(tile_coord)) # raises, if key is missing boto3.client("s3").head_object(Bucket=self.bucket_name, Key=key) def test_tile_keys(self): yield self.check_tile_key, 'mp', (12345, 67890, 2), 'mycache/webmercator/02/0001/2345/0006/7890.png' yield self.check_tile_key, 'mp', (12345, 67890, 12), 'mycache/webmercator/12/0001/2345/0006/7890.png' yield self.check_tile_key, 'tc', (12345, 67890, 2), 'mycache/webmercator/02/000/012/345/000/067/890.png' yield self.check_tile_key, 'tc', (12345, 67890, 12), 'mycache/webmercator/12/000/012/345/000/067/890.png' yield self.check_tile_key, 'tms', (12345, 67890, 2), 'mycache/webmercator/2/12345/67890.png' yield self.check_tile_key, 'tms', (12345, 67890, 12), 'mycache/webmercator/12/12345/67890.png' yield self.check_tile_key, 'quadkey', (0, 0, 0), 'mycache/webmercator/.png' yield self.check_tile_key, 'quadkey', (0, 0, 1), 'mycache/webmercator/0.png' yield self.check_tile_key, 'quadkey', (1, 1, 1), 'mycache/webmercator/3.png' yield self.check_tile_key, 'quadkey', (12345, 67890, 12), 'mycache/webmercator/200200331021.png' yield self.check_tile_key, 'arcgis', (1, 2, 3), 'mycache/webmercator/L03/R00000002/C00000001.png' yield self.check_tile_key, 'arcgis', (9, 2, 3), 'mycache/webmercator/L03/R00000002/C00000009.png' yield self.check_tile_key, 'arcgis', (10, 2, 3), 'mycache/webmercator/L03/R00000002/C0000000a.png' yield self.check_tile_key, 'arcgis', (12345, 67890, 12), 'mycache/webmercator/L12/R00010932/C00003039.png' mapproxy-1.11.0/mapproxy/test/unit/test_cache_tile.py000066400000000000000000000365441320454472400230000ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011-2013 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import calendar import datetime import os import shutil import threading import tempfile import time import sqlite3 from io import BytesIO from PIL import Image from mapproxy.cache.tile import Tile from mapproxy.cache.file import FileCache from mapproxy.cache.mbtiles import MBTilesCache, MBTilesLevelCache from mapproxy.image import ImageSource from mapproxy.image.opts import ImageOptions from mapproxy.test.image import create_tmp_image_buf, is_png from nose.tools import eq_ tile_image = create_tmp_image_buf((256, 256), color='blue') tile_image2 = create_tmp_image_buf((256, 256), color='red') class TileCacheTestBase(object): always_loads_metadata = False uses_utc = False def setup(self): self.cache_dir = tempfile.mkdtemp() def teardown(self): if hasattr(self, 'cache_dir') and os.path.exists(self.cache_dir): shutil.rmtree(self.cache_dir) def create_tile(self, coord=(3009, 589, 12)): return Tile(coord, ImageSource(tile_image, image_opts=ImageOptions(format='image/png'))) def create_another_tile(self, coord=(3009, 589, 12)): return Tile(coord, ImageSource(tile_image2, image_opts=ImageOptions(format='image/png'))) def test_is_cached_miss(self): assert not self.cache.is_cached(Tile((3009, 589, 12))) def test_is_cached_hit(self): tile = self.create_tile() self.create_cached_tile(tile) assert self.cache.is_cached(Tile((3009, 589, 12))) def test_is_cached_none(self): assert self.cache.is_cached(Tile(None)) def test_load_tile_none(self): assert self.cache.load_tile(Tile(None)) def test_load_tile_not_cached(self): tile = Tile((3009, 589, 12)) assert not self.cache.load_tile(tile) assert tile.source is None assert tile.is_missing() def test_load_tile_cached(self): tile = self.create_tile() self.create_cached_tile(tile) tile = Tile((3009, 589, 12)) assert self.cache.load_tile(tile) == True assert not tile.is_missing() def test_store_tiles(self): tiles = [self.create_tile((x, 589, 12)) for x in range(4)] tiles[0].stored = True self.cache.store_tiles(tiles) tiles = [Tile((x, 589, 12)) for x in range(4)] assert tiles[0].is_missing() assert self.cache.load_tile(tiles[0]) == False assert tiles[0].is_missing() for tile in tiles[1:]: assert tile.is_missing() assert self.cache.load_tile(tile) == True assert not tile.is_missing() def test_load_tiles_cached(self): self.cache.store_tile(self.create_tile((0, 0, 1))) self.cache.store_tile(self.create_tile((0, 1, 1))) tiles = [Tile((0, 0, 1)), Tile((0, 1, 1))] assert self.cache.load_tiles(tiles) assert not tiles[0].is_missing() assert not tiles[1].is_missing() def test_load_tiles_mixed(self): tile = self.create_tile((1, 0, 4)) self.create_cached_tile(tile) tiles = [Tile(None), Tile((0, 0, 4)), Tile((1, 0, 4))] assert self.cache.load_tiles(tiles) == False assert not tiles[0].is_missing() assert tiles[1].is_missing() assert not tiles[2].is_missing() def test_load_stored_tile(self): tile = self.create_tile((5, 12, 4)) self.cache.store_tile(tile) size = tile.size # check stored tile tile = Tile((5, 12, 4)) assert tile.source is None assert self.cache.load_tile(tile) if not self.always_loads_metadata: assert tile.source is not None assert tile.timestamp is None assert tile.size is None stored_size = len(tile.source.as_buffer().read()) assert stored_size == size # check loading of metadata (timestamp, size) tile = Tile((5, 12, 4)) assert tile.source is None assert self.cache.load_tile(tile, with_metadata=True) assert tile.source is not None if tile.timestamp: now = time.time() if self.uses_utc: now = calendar.timegm(datetime.datetime.utcnow().timetuple()) assert abs(tile.timestamp - now) <= 10 if tile.size: assert tile.size == size def test_overwrite_tile(self): tile = self.create_tile((5, 12, 4)) self.cache.store_tile(tile) tile = Tile((5, 12, 4)) self.cache.load_tile(tile) tile1_content = tile.source.as_buffer().read() assert tile1_content == tile_image.getvalue() tile = self.create_another_tile((5, 12, 4)) self.cache.store_tile(tile) tile = Tile((5, 12, 4)) self.cache.load_tile(tile) tile2_content = tile.source.as_buffer().read() assert tile2_content == tile_image2.getvalue() assert tile1_content != tile2_content def test_store_tile_already_stored(self): # tile object is marked as stored, # check that is is not stored 'again' # (used for disable_storage) tile = Tile((1234, 589, 12), ImageSource(BytesIO(b'foo'))) tile.stored = True self.cache.store_tile(tile) assert self.cache.is_cached(tile) tile = Tile((1234, 589, 12)) assert not self.cache.is_cached(tile) def test_remove(self): tile = self.create_tile((1, 0, 4)) self.create_cached_tile(tile) assert self.cache.is_cached(Tile((1, 0, 4))) self.cache.remove_tile(Tile((1, 0, 4))) assert not self.cache.is_cached(Tile((1, 0, 4))) # check if we can recreate a removed tile tile = self.create_tile((1, 0, 4)) self.create_cached_tile(tile) assert self.cache.is_cached(Tile((1, 0, 4))) def create_cached_tile(self, tile): self.cache.store_tile(tile) class TestFileTileCache(TileCacheTestBase): def setup(self): TileCacheTestBase.setup(self) self.cache = FileCache(self.cache_dir, 'png') def test_store_tile(self): tile = self.create_tile((5, 12, 4)) self.cache.store_tile(tile) tile_location = os.path.join(self.cache_dir, '04', '000', '000', '005', '000', '000', '012.png' ) assert os.path.exists(tile_location), tile_location def test_single_color_tile_store(self): img = Image.new('RGB', (256, 256), color='#ff0105') tile = Tile((0, 0, 4), ImageSource(img, image_opts=ImageOptions(format='image/png'))) self.cache.link_single_color_images = True self.cache.store_tile(tile) assert self.cache.is_cached(tile) loc = self.cache.tile_location(tile) assert os.path.islink(loc) assert os.path.realpath(loc).endswith('ff0105.png') assert is_png(open(loc, 'rb')) tile2 = Tile((0, 0, 1), ImageSource(img)) self.cache.store_tile(tile2) assert self.cache.is_cached(tile2) loc2 = self.cache.tile_location(tile2) assert os.path.islink(loc2) assert os.path.realpath(loc2).endswith('ff0105.png') assert is_png(open(loc2, 'rb')) assert loc != loc2 assert os.path.samefile(loc, loc2) def test_single_color_tile_store_w_alpha(self): img = Image.new('RGBA', (256, 256), color='#ff0105') tile = Tile((0, 0, 4), ImageSource(img, image_opts=ImageOptions(format='image/png'))) self.cache.link_single_color_images = True self.cache.store_tile(tile) assert self.cache.is_cached(tile) loc = self.cache.tile_location(tile) assert os.path.islink(loc) assert os.path.realpath(loc).endswith('ff0105ff.png') assert is_png(open(loc, 'rb')) def test_load_metadata_missing_tile(self): tile = Tile((0, 0, 0)) self.cache.load_tile_metadata(tile) assert tile.timestamp == 0 assert tile.size == 0 def create_cached_tile(self, tile): loc = self.cache.tile_location(tile, create_dir=True) with open(loc, 'wb') as f: f.write(b'foo') def check_tile_location(self, layout, tile_coord, path): cache = FileCache('/tmp/foo', 'png', directory_layout=layout) eq_(cache.tile_location(Tile(tile_coord)), path) def test_tile_locations(self): yield self.check_tile_location, 'mp', (12345, 67890, 2), '/tmp/foo/02/0001/2345/0006/7890.png' yield self.check_tile_location, 'mp', (12345, 67890, 12), '/tmp/foo/12/0001/2345/0006/7890.png' yield self.check_tile_location, 'tc', (12345, 67890, 2), '/tmp/foo/02/000/012/345/000/067/890.png' yield self.check_tile_location, 'tc', (12345, 67890, 12), '/tmp/foo/12/000/012/345/000/067/890.png' yield self.check_tile_location, 'tms', (12345, 67890, 2), '/tmp/foo/2/12345/67890.png' yield self.check_tile_location, 'tms', (12345, 67890, 12), '/tmp/foo/12/12345/67890.png' yield self.check_tile_location, 'quadkey', (0, 0, 0), '/tmp/foo/.png' yield self.check_tile_location, 'quadkey', (0, 0, 1), '/tmp/foo/0.png' yield self.check_tile_location, 'quadkey', (1, 1, 1), '/tmp/foo/3.png' yield self.check_tile_location, 'quadkey', (12345, 67890, 12), '/tmp/foo/200200331021.png' yield self.check_tile_location, 'arcgis', (1, 2, 3), '/tmp/foo/L03/R00000002/C00000001.png' yield self.check_tile_location, 'arcgis', (9, 2, 3), '/tmp/foo/L03/R00000002/C00000009.png' yield self.check_tile_location, 'arcgis', (10, 2, 3), '/tmp/foo/L03/R00000002/C0000000a.png' yield self.check_tile_location, 'arcgis', (12345, 67890, 12), '/tmp/foo/L12/R00010932/C00003039.png' def check_level_location(self, layout, level, path): cache = FileCache('/tmp/foo', 'png', directory_layout=layout) eq_(cache.level_location(level), path) def test_level_locations(self): yield self.check_level_location, 'mp', 2, '/tmp/foo/02' yield self.check_level_location, 'mp', 12, '/tmp/foo/12' yield self.check_level_location, 'tc', 2, '/tmp/foo/02' yield self.check_level_location, 'tc', 12, '/tmp/foo/12' yield self.check_level_location, 'tms', '2', '/tmp/foo/2' yield self.check_level_location, 'tms', 12, '/tmp/foo/12' yield self.check_level_location, 'arcgis', 3, '/tmp/foo/L03' yield self.check_level_location, 'arcgis', 3, '/tmp/foo/L03' yield self.check_level_location, 'arcgis', 3, '/tmp/foo/L03' yield self.check_level_location, 'arcgis', 12, '/tmp/foo/L12' def test_level_location_quadkey(self): try: self.check_level_location('quadkey', 0, None) except NotImplementedError: pass else: assert False, "expected NotImplementedError" class TestMBTileCache(TileCacheTestBase): def setup(self): TileCacheTestBase.setup(self) self.cache = MBTilesCache(os.path.join(self.cache_dir, 'tmp.mbtiles')) def teardown(self): if self.cache: self.cache.cleanup() TileCacheTestBase.teardown(self) def test_load_empty_tileset(self): assert self.cache.load_tiles([Tile(None)]) == True assert self.cache.load_tiles([Tile(None), Tile(None), Tile(None)]) == True def test_load_more_than_2000_tiles(self): # prepare data for i in range(0, 2010): assert self.cache.store_tile(Tile((i, 0, 10), ImageSource(BytesIO(b'foo')))) tiles = [Tile((i, 0, 10)) for i in range(0, 2010)] assert self.cache.load_tiles(tiles) def test_timeouts(self): self.cache._db_conn_cache.db = sqlite3.connect(self.cache.mbtile_file, timeout=0.05) def block(): # block database by delaying the commit db = sqlite3.connect(self.cache.mbtile_file) cur = db.cursor() stmt = "INSERT OR REPLACE INTO tiles (zoom_level, tile_column, tile_row, tile_data) VALUES (?,?,?,?)" cur.execute(stmt, (3, 1, 1, '1234')) time.sleep(0.2) db.commit() try: assert self.cache.store_tile(self.create_tile((0, 0, 1))) == True t = threading.Thread(target=block) t.start() time.sleep(0.05) assert self.cache.store_tile(self.create_tile((0, 0, 1))) == False finally: t.join() assert self.cache.store_tile(self.create_tile((0, 0, 1))) == True class TestQuadkeyFileTileCache(TileCacheTestBase): def setup(self): TileCacheTestBase.setup(self) self.cache = FileCache(self.cache_dir, 'png', directory_layout='quadkey') def test_store_tile(self): tile = self.create_tile((3, 4, 2)) self.cache.store_tile(tile) tile_location = os.path.join(self.cache_dir, '11.png' ) assert os.path.exists(tile_location), tile_location class TestMBTileLevelCache(TileCacheTestBase): always_loads_metadata = True def setup(self): TileCacheTestBase.setup(self) self.cache = MBTilesLevelCache(self.cache_dir) def test_level_files(self): eq_(os.listdir(self.cache_dir), []) self.cache.store_tile(self.create_tile((0, 0, 1))) eq_(os.listdir(self.cache_dir), ['1.mbtile']) self.cache.store_tile(self.create_tile((0, 0, 5))) eq_(sorted(os.listdir(self.cache_dir)), ['1.mbtile', '5.mbtile']) def test_remove_level_files(self): self.cache.store_tile(self.create_tile((0, 0, 1))) self.cache.store_tile(self.create_tile((0, 0, 2))) eq_(sorted(os.listdir(self.cache_dir)), ['1.mbtile', '2.mbtile']) self.cache.remove_level_tiles_before(1, timestamp=0) eq_(os.listdir(self.cache_dir), ['2.mbtile']) def test_remove_level_tiles_before(self): self.cache.store_tile(self.create_tile((0, 0, 1))) self.cache.store_tile(self.create_tile((0, 0, 2))) eq_(sorted(os.listdir(self.cache_dir)), ['1.mbtile', '2.mbtile']) assert self.cache.is_cached(Tile((0, 0, 1))) self.cache.remove_level_tiles_before(1, timestamp=time.time() - 60) assert self.cache.is_cached(Tile((0, 0, 1))) self.cache.remove_level_tiles_before(1, timestamp=time.time() + 60) assert not self.cache.is_cached(Tile((0, 0, 1))) eq_(sorted(os.listdir(self.cache_dir)), ['1.mbtile', '2.mbtile']) assert self.cache.is_cached(Tile((0, 0, 2))) def test_bulk_store_tiles_with_different_levels(self): self.cache.store_tiles([ self.create_tile((0, 0, 1)), self.create_tile((0, 0, 2)), self.create_tile((1, 0, 2)), self.create_tile((1, 0, 1)), ]) eq_(sorted(os.listdir(self.cache_dir)), ['1.mbtile', '2.mbtile']) assert self.cache.is_cached(Tile((0, 0, 1))) assert self.cache.is_cached(Tile((1, 0, 1))) assert self.cache.is_cached(Tile((0, 0, 2))) assert self.cache.is_cached(Tile((1, 0, 2))) mapproxy-1.11.0/mapproxy/test/unit/test_client.py000066400000000000000000000436341320454472400221740ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010-2017 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import time import sys from mapproxy.client.http import HTTPClient, HTTPClientError, supports_ssl_default_context from mapproxy.client.tile import TMSClient, TileClient, TileURLTemplate from mapproxy.client.wms import WMSClient, WMSInfoClient from mapproxy.grid import tile_grid from mapproxy.layer import MapQuery, InfoQuery from mapproxy.request.wms import WMS111MapRequest, WMS100MapRequest,\ WMS130MapRequest, WMS111FeatureInfoRequest from mapproxy.srs import SRS from mapproxy.test.unit.test_cache import MockHTTPClient from mapproxy.test.http import mock_httpd, query_eq, assert_query_eq, wms_query_eq from mapproxy.test.helper import assert_re, TempFile from nose.tools import eq_ from nose.plugins.skip import SkipTest from nose.plugins.attrib import attr TESTSERVER_ADDRESS = ('127.0.0.1', 56413) TESTSERVER_URL = 'http://%s:%s' % TESTSERVER_ADDRESS class TestHTTPClient(object): def setup(self): self.client = HTTPClient() def test_post(self): with mock_httpd(TESTSERVER_ADDRESS, [({'path': '/service?foo=bar', 'method': 'POST'}, {'status': '200', 'body': b''})]): self.client.open(TESTSERVER_URL + '/service', data=b"foo=bar") def test_internal_error_response(self): try: with mock_httpd(TESTSERVER_ADDRESS, [({'path': '/'}, {'status': '500', 'body': b''})]): self.client.open(TESTSERVER_URL + '/') except HTTPClientError as e: assert_re(e.args[0], r'HTTP Error ".*": 500') else: assert False, 'expected HTTPClientError' def test_invalid_url_type(self): try: self.client.open('htp://example.org') except HTTPClientError as e: assert_re(e.args[0], r'No response .* "htp://example.*": unknown url type') else: assert False, 'expected HTTPClientError' def test_invalid_url(self): try: self.client.open('this is not a url') except HTTPClientError as e: assert_re(e.args[0], r'URL not correct "this is not.*": unknown url type') else: assert False, 'expected HTTPClientError' def test_unknown_host(self): try: self.client.open('http://thishostshouldnotexist000136really42.org') except HTTPClientError as e: assert_re(e.args[0], r'No response .* "http://thishost.*": .*') else: assert False, 'expected HTTPClientError' def test_no_connect(self): try: self.client.open('http://localhost:53871') except HTTPClientError as e: assert_re(e.args[0], r'No response .* "http://localhost.*": Connection refused') else: assert False, 'expected HTTPClientError' @attr('online') def test_https_untrusted_root(self): if not supports_ssl_default_context: # old python versions require ssl_ca_certs raise SkipTest() self.client = HTTPClient('https://untrusted-root.badssl.com/') try: self.client.open('https://untrusted-root.badssl.com/') except HTTPClientError as e: assert_re(e.args[0], r'Could not verify connection to URL') @attr('online') def test_https_insecure(self): self.client = HTTPClient( 'https://untrusted-root.badssl.com/', insecure=True) self.client.open('https://untrusted-root.badssl.com/') @attr('online') def test_https_valid_ca_cert_file(self): # verify with fixed ca_certs file cert_file = '/etc/ssl/certs/ca-certificates.crt' if os.path.exists(cert_file): self.client = HTTPClient('https://www.google.com/', ssl_ca_certs=cert_file) self.client.open('https://www.google.com/') else: with TempFile() as tmp: with open(tmp, 'wb') as f: f.write(GOOGLE_ROOT_CERT) self.client = HTTPClient('https://www.google.com/', ssl_ca_certs=tmp) self.client.open('https://www.google.com/') @attr('online') def test_https_valid_default_cert(self): # modern python should verify by default if not supports_ssl_default_context: raise SkipTest() self.client = HTTPClient('https://www.google.com/') self.client.open('https://www.google.com/') @attr('online') def test_https_invalid_cert(self): # load 'wrong' root cert with TempFile() as tmp: with open(tmp, 'wb') as f: f.write(GOOGLE_ROOT_CERT) self.client = HTTPClient( 'https://untrusted-root.badssl.com/', ssl_ca_certs=tmp) try: self.client.open('https://untrusted-root.badssl.com/') except HTTPClientError as e: assert_re(e.args[0], r'Could not verify connection to URL') def test_timeouts(self): test_req = ({'path': '/', 'req_assert_function': lambda x: time.sleep(0.9) or True}, {'body': b'nothing'}) import mapproxy.client.http client1 = HTTPClient(timeout=0.1) client2 = HTTPClient(timeout=0.5) with mock_httpd(TESTSERVER_ADDRESS, [test_req]): try: start = time.time() client1.open(TESTSERVER_URL + '/') except HTTPClientError as ex: assert 'timed out' in ex.args[0] else: assert False, 'HTTPClientError expected' duration1 = time.time() - start with mock_httpd(TESTSERVER_ADDRESS, [test_req]): try: start = time.time() client2.open(TESTSERVER_URL + '/') except HTTPClientError as ex: assert 'timed out' in ex.args[0] else: assert False, 'HTTPClientError expected' duration2 = time.time() - start # check individual timeouts assert 0.1 <= duration1 < 0.5, duration1 assert 0.5 <= duration2 < 0.9, duration2 # root certificates for google.com, if no ca-certificates.cert # file is found GOOGLE_ROOT_CERT = b""" -----BEGIN CERTIFICATE----- MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU 1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV 5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1 MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8 eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG 3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO 291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1 MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y 7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh 1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4 -----END CERTIFICATE----- """ class TestTMSClient(object): def setup(self): self.client = TMSClient(TESTSERVER_URL) def test_get_tile(self): with mock_httpd(TESTSERVER_ADDRESS, [({'path': '/9/5/13.png'}, {'body': b'tile', 'headers': {'content-type': 'image/png'}})]): resp = self.client.get_tile((5, 13, 9)).source.read() eq_(resp, b'tile') class TestTileClient(object): def test_tc_path(self): template = TileURLTemplate(TESTSERVER_URL + '/%(tc_path)s.png') client = TileClient(template) with mock_httpd(TESTSERVER_ADDRESS, [({'path': '/09/000/000/005/000/000/013.png'}, {'body': b'tile', 'headers': {'content-type': 'image/png'}})]): resp = client.get_tile((5, 13, 9)).source.read() eq_(resp, b'tile') def test_quadkey(self): template = TileURLTemplate(TESTSERVER_URL + '/key=%(quadkey)s&format=%(format)s') client = TileClient(template) with mock_httpd(TESTSERVER_ADDRESS, [({'path': '/key=000002303&format=png'}, {'body': b'tile', 'headers': {'content-type': 'image/png'}})]): resp = client.get_tile((5, 13, 9)).source.read() eq_(resp, b'tile') def test_xyz(self): template = TileURLTemplate(TESTSERVER_URL + '/x=%(x)s&y=%(y)s&z=%(z)s&format=%(format)s') client = TileClient(template) with mock_httpd(TESTSERVER_ADDRESS, [({'path': '/x=5&y=13&z=9&format=png'}, {'body': b'tile', 'headers': {'content-type': 'image/png'}})]): resp = client.get_tile((5, 13, 9)).source.read() eq_(resp, b'tile') def test_arcgiscache_path(self): template = TileURLTemplate(TESTSERVER_URL + '/%(arcgiscache_path)s.png') client = TileClient(template) with mock_httpd(TESTSERVER_ADDRESS, [({'path': '/L09/R0000000d/C00000005.png'}, {'body': b'tile', 'headers': {'content-type': 'image/png'}})]): resp = client.get_tile((5, 13, 9)).source.read() eq_(resp, b'tile') def test_bbox(self): grid = tile_grid(4326) template = TileURLTemplate(TESTSERVER_URL + '/service?BBOX=%(bbox)s') client = TileClient(template, grid=grid) with mock_httpd(TESTSERVER_ADDRESS, [({'path': '/service?BBOX=-180.00000000,0.00000000,-90.00000000,90.00000000'}, {'body': b'tile', 'headers': {'content-type': 'image/png'}})]): resp = client.get_tile((0, 1, 2)).source.read() eq_(resp, b'tile') class TestCombinedWMSClient(object): def setup(self): self.http = MockHTTPClient() def test_combine(self): req1 = WMS111MapRequest(url=TESTSERVER_URL + '/service?map=foo', param={'layers':'foo', 'transparent': 'true'}) wms1 = WMSClient(req1, http_client=self.http) req2 = WMS111MapRequest(url=TESTSERVER_URL + '/service?map=foo', param={'layers':'bar', 'transparent': 'true'}) wms2 = WMSClient(req2, http_client=self.http) req = MapQuery((-200000, -200000, 200000, 200000), (512, 512), SRS(900913), 'png') combined = wms1.combined_client(wms2, req) eq_(combined.request_template.params.layers, ['foo', 'bar']) eq_(combined.request_template.url, TESTSERVER_URL + '/service?map=foo') def test_combine_different_url(self): req1 = WMS111MapRequest(url=TESTSERVER_URL + '/service?map=bar', param={'layers':'foo', 'transparent': 'true'}) wms1 = WMSClient(req1, http_client=self.http) req2 = WMS111MapRequest(url=TESTSERVER_URL + '/service?map=foo', param={'layers':'bar', 'transparent': 'true'}) wms2 = WMSClient(req2, http_client=self.http) req = MapQuery((-200000, -200000, 200000, 200000), (512, 512), SRS(900913), 'png') combined = wms1.combined_client(wms2, req) assert combined is None class TestWMSInfoClient(object): def test_transform_fi_request_supported_srs(self): req = WMS111FeatureInfoRequest(url=TESTSERVER_URL + '/service?map=foo', param={'layers':'foo'}) http = MockHTTPClient() wms = WMSInfoClient(req, http_client=http, supported_srs=[SRS(25832)]) fi_req = InfoQuery((8, 50, 9, 51), (512, 512), SRS(4326), (128, 64), 'text/plain') wms.get_info(fi_req) assert wms_query_eq(http.requested[0], TESTSERVER_URL+'/service?map=foo&LAYERS=foo&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetFeatureInfo&SRS=EPSG%3A25832&info_format=text/plain' '&query_layers=foo' '&VERSION=1.1.1&WIDTH=512&HEIGHT=797&STYLES=&x=135&y=101' '&BBOX=428333.552496,5538630.70275,500000.0,5650300.78652'), http.requested[0] def test_transform_fi_request(self): req = WMS111FeatureInfoRequest(url=TESTSERVER_URL + '/service?map=foo', param={'layers':'foo', 'srs': 'EPSG:25832'}) http = MockHTTPClient() wms = WMSInfoClient(req, http_client=http) fi_req = InfoQuery((8, 50, 9, 51), (512, 512), SRS(4326), (128, 64), 'text/plain') wms.get_info(fi_req) assert wms_query_eq(http.requested[0], TESTSERVER_URL+'/service?map=foo&LAYERS=foo&SERVICE=WMS&FORMAT=image%2Fpng' '&REQUEST=GetFeatureInfo&SRS=EPSG%3A25832&info_format=text/plain' '&query_layers=foo' '&VERSION=1.1.1&WIDTH=512&HEIGHT=797&STYLES=&x=135&y=101' '&BBOX=428333.552496,5538630.70275,500000.0,5650300.78652'), http.requested[0] class TestWMSMapRequest100(object): def setup(self): self.r = WMS100MapRequest(param=dict(layers='foo', version='1.1.1', request='GetMap')) self.r.params = self.r.adapt_params_to_version() def test_version(self): eq_(self.r.params['WMTVER'], '1.0.0') assert 'VERSION' not in self.r.params def test_service(self): assert 'SERVICE' not in self.r.params def test_request(self): eq_(self.r.params['request'], 'map') def test_str(self): assert_query_eq(str(self.r.params), 'layers=foo&styles=&request=map&wmtver=1.0.0') class TestWMSMapRequest130(object): def setup(self): self.r = WMS130MapRequest(param=dict(layers='foo', WMTVER='1.0.0')) self.r.params = self.r.adapt_params_to_version() def test_version(self): eq_(self.r.params['version'], '1.3.0') assert 'WMTVER' not in self.r.params def test_service(self): eq_(self.r.params['service'], 'WMS' ) def test_request(self): eq_(self.r.params['request'], 'GetMap') def test_str(self): query_eq(str(self.r.params), 'layers=foo&styles=&service=WMS&request=GetMap&version=1.3.0') class TestWMSMapRequest111(object): def setup(self): self.r = WMS111MapRequest(param=dict(layers='foo', WMTVER='1.0.0')) self.r.params = self.r.adapt_params_to_version() def test_version(self): eq_(self.r.params['version'], '1.1.1') assert 'WMTVER' not in self.r.params mapproxy-1.11.0/mapproxy/test/unit/test_client_arcgis.py000066400000000000000000000057051320454472400235210ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from io import BytesIO from mapproxy.client.arcgis import ArcGISInfoClient from mapproxy.layer import InfoQuery from mapproxy.request.arcgis import ArcGISIdentifyRequest from mapproxy.srs import SRS from mapproxy.test.http import assert_query_eq TESTSERVER_ADDRESS = ('127.0.0.1', 56413) TESTSERVER_URL = 'http://%s:%s' % TESTSERVER_ADDRESS class MockHTTPClient(object): def __init__(self): self.requested = [] def open(self, url, data=None): self.requested.append(url) result = BytesIO(b'{}') result.seek(0) result.headers = {} return result class TestArcGISInfoClient(object): def test_fi_request(self): req = ArcGISIdentifyRequest(url=TESTSERVER_URL + '/MapServer/export?map=foo', param={'layers':'foo'}) http = MockHTTPClient() wms = ArcGISInfoClient(req, http_client=http, supported_srs=[SRS(4326)]) fi_req = InfoQuery((8, 50, 9, 51), (512, 512), SRS(4326), (128, 64), 'text/plain') wms.get_info(fi_req) assert_query_eq(http.requested[0], TESTSERVER_URL+'/MapServer/identify?map=foo' '&imageDisplay=512,512,96&sr=4326&f=json' '&layers=foo&tolerance=5&returnGeometry=false' '&geometryType=esriGeometryPoint&geometry=8.250000,50.875000' '&mapExtent=8,50,9,51', fuzzy_number_compare=True) def test_transform_fi_request_supported_srs(self): req = ArcGISIdentifyRequest(url=TESTSERVER_URL + '/MapServer/export?map=foo', param={'layers':'foo'}) http = MockHTTPClient() wms = ArcGISInfoClient(req, http_client=http, supported_srs=[SRS(25832)]) fi_req = InfoQuery((8, 50, 9, 51), (512, 512), SRS(4326), (128, 64), 'text/plain') wms.get_info(fi_req) assert_query_eq(http.requested[0], TESTSERVER_URL+'/MapServer/identify?map=foo' '&imageDisplay=512,797,96&sr=25832&f=json' '&layers=foo&tolerance=5&returnGeometry=false' '&geometryType=esriGeometryPoint&geometry=447229.979084,5636149.370634' '&mapExtent=428333.552496,5538630.70275,500000.0,5650300.78652', fuzzy_number_compare=True)mapproxy-1.11.0/mapproxy/test/unit/test_client_cgi.py000066400000000000000000000110041320454472400230000ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import shutil import stat import tempfile from mapproxy.client.http import HTTPClientError from mapproxy.client.cgi import CGIClient, split_cgi_response from mapproxy.source import SourceError from nose.tools import eq_ class TestSplitHTTPResponse(object): def test_n(self): eq_(split_cgi_response(b'header1: foo\nheader2: bar\n\ncontent\n\ncontent'), ({'Header1': 'foo', 'Header2': 'bar'}, b'content\n\ncontent')) def test_rn(self): eq_(split_cgi_response(b'header1\r\nheader2\r\n\r\ncontent\r\n\r\ncontent'), ({'Header1': None, 'Header2': None}, b'content\r\n\r\ncontent')) def test_mixed(self): eq_(split_cgi_response(b'header1: bar:foo\r\nheader2\n\r\ncontent\r\n\r\ncontent'), ({'Header1': 'bar:foo', 'Header2': None}, b'content\r\n\r\ncontent')) eq_(split_cgi_response(b'header1\r\nheader2\n\ncontent\r\n\r\ncontent'), ({'Header1': None, 'Header2': None}, b'content\r\n\r\ncontent')) eq_(split_cgi_response(b'header1\nheader2\r\n\r\ncontent\r\n\r\ncontent'), ({'Header1': None, 'Header2': None}, b'content\r\n\r\ncontent')) def test_no_header(self): eq_(split_cgi_response(b'content\r\ncontent'), ({}, b'content\r\ncontent')) TEST_CGI_SCRIPT = br"""#! /usr/bin/env python import sys import os w = sys.stdout.write w("Content-type: text/plain\r\n") w("\r\n") w(os.environ['QUERY_STRING']) """ TEST_CGI_SCRIPT_FAIL = TEST_CGI_SCRIPT + b'\nexit(1)' TEST_CGI_SCRIPT_CWD = TEST_CGI_SCRIPT + br""" if not os.path.exists('testfile'): exit(2) """ class TestCGIClient(object): def setup(self): self.script_dir = tempfile.mkdtemp() def teardown(self): shutil.rmtree(self.script_dir) def create_script(self, script=TEST_CGI_SCRIPT, executable=True): script_file = os.path.join(self.script_dir, 'cgi.py') with open(script_file, 'wb') as f: f.write(script) if executable: os.chmod(script_file, stat.S_IXUSR | stat.S_IRUSR | stat.S_IWUSR) return script_file def test_missing_script(self): client = CGIClient('/tmp/doesnotexist') try: client.open('http://example.org/service?hello=bar') except SourceError: pass else: assert False, 'expected SourceError' def test_script_not_executable(self): script = self.create_script(executable=False) client = CGIClient(script) try: client.open('http://example.org/service?hello=bar') except SourceError: pass else: assert False, 'expected SourceError' def test_call(self): script = self.create_script() client = CGIClient(script) resp = client.open('http://example.org/service?hello=bar') eq_(resp.headers['Content-type'], 'text/plain') eq_(resp.read(), b'hello=bar') def test_failed_call(self): script = self.create_script(TEST_CGI_SCRIPT_FAIL) client = CGIClient(script) try: client.open('http://example.org/service?hello=bar') except HTTPClientError: pass else: assert False, 'expected HTTPClientError' def test_working_directory(self): tmp_work_dir = os.path.join(self.script_dir, 'tmp') os.mkdir(tmp_work_dir) tmp_file = os.path.join(tmp_work_dir, 'testfile') open(tmp_file, 'wb') # start script in default directory script = self.create_script(TEST_CGI_SCRIPT_CWD) client = CGIClient(script) try: client.open('http://example.org/service?hello=bar') except HTTPClientError: pass else: assert False, 'expected HTTPClientError' # start in tmp_work_dir client = CGIClient(script, working_directory=tmp_work_dir) client.open('http://example.org/service?hello=bar') mapproxy-1.11.0/mapproxy/test/unit/test_collections.py000066400000000000000000000057701320454472400232330ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.util.collections import LRU, ImmutableDictList from nose.tools import eq_, raises class TestLRU(object): @raises(KeyError) def test_missing_key(self): lru = LRU(10) lru['foo'] def test_contains(self): lru = LRU(10) lru['foo1'] = 1 assert 'foo1' in lru assert 'foo2' not in lru def test_repr(self): lru = LRU(10) lru['foo1'] = 1 assert 'size=10' in repr(lru) assert 'foo1' in repr(lru) def test_getitem(self): lru = LRU(10) lru['foo1'] = 1 lru['foo2'] = 2 eq_(lru['foo1'], 1) eq_(lru['foo2'], 2) def test_get(self): lru = LRU(10) lru['foo1'] = 1 eq_(lru.get('foo1'), 1) eq_(lru.get('foo1', 2), 1) def test_get_default(self): lru = LRU(10) lru['foo1'] = 1 eq_(lru.get('foo2'), None) eq_(lru.get('foo2', 2), 2) def test_delitem(self): lru = LRU(10) lru['foo1'] = 1 assert 'foo1' in lru del lru['foo1'] assert 'foo1' not in lru def test_empty(self): lru = LRU(10) assert bool(lru) == False lru['foo1'] = '1' assert bool(lru) == True def test_setitem_overflow(self): lru = LRU(2) lru['foo1'] = 1 lru['foo2'] = 2 lru['foo3'] = 3 assert 'foo1' not in lru assert 'foo2' in lru assert 'foo3' in lru def test_length(self): lru = LRU(2) eq_(len(lru), 0) lru['foo1'] = 1 eq_(len(lru), 1) lru['foo2'] = 2 eq_(len(lru), 2) lru['foo3'] = 3 eq_(len(lru), 2) del lru['foo3'] eq_(len(lru), 1) class TestImmutableDictList(object): def test_named(self): res = ImmutableDictList([('one', 10), ('two', 5), ('three', 3)]) assert res[0] == 10 assert res[2] == 3 assert res['one'] == 10 assert res['three'] == 3 assert len(res) == 3 def test_named_iteritems(self): res = ImmutableDictList([('one', 10), ('two', 5), ('three', 3)]) itr = res.iteritems() eq_(next(itr), ('one', 10)) eq_(next(itr), ('two', 5)) eq_(next(itr), ('three', 3)) try: next(itr) except StopIteration: pass else: assert False, 'StopIteration expected'mapproxy-1.11.0/mapproxy/test/unit/test_concat_legends.py000066400000000000000000000030521320454472400236540ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.compat.image import Image from mapproxy.image import ImageSource from mapproxy.image.merge import concat_legends from mapproxy.test.image import is_png class Test_Concat_Legends(object): def test_concatenation(self): legends = [] img_1 = Image.new(mode='RGBA', size=(30,10), color="red") img_2 = Image.new(mode='RGBA', size=(10,10), color="black") img_3 = Image.new(mode='RGBA', size=(50,80), color="blue") legends.append(ImageSource(img_1)) legends.append(ImageSource(img_2)) legends.append(ImageSource(img_3)) source = concat_legends(legends) src_img = source.as_image() assert src_img.getpixel((0,90)) == (255,0,0,255) assert src_img.getpixel((0,80)) == (0,0,0,255) assert src_img.getpixel((0,0)) == (0,0,255,255) assert src_img.getpixel((49,99)) == (255,255,255,0) assert is_png(source.as_buffer()) mapproxy-1.11.0/mapproxy/test/unit/test_conf_loader.py000066400000000000000000000726101320454472400231650ustar00rootroot00000000000000# -:- encoding: UTF8 -:- # This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import yaml import time from mapproxy.srs import SRS from mapproxy.config.loader import ( ProxyConfiguration, load_configuration, merge_dict, ConfigurationError, ) from mapproxy.config.coverage import load_coverage from mapproxy.config.spec import validate_options from mapproxy.cache.tile import TileManager from mapproxy.seed.spec import validate_seed_conf from mapproxy.test.helper import TempFile from mapproxy.test.unit.test_grid import assert_almost_equal_bbox from mapproxy.util.geom import EmptyGeometryError from nose.tools import eq_, assert_raises class TestLayerConfiguration(object): def _test_conf(self, yaml_part): base = {'sources': {'s': {'type': 'wms', 'req': {'url': ''}}}} base.update(yaml.load(yaml_part)) return base def test_legacy_ordered(self): conf = self._test_conf(''' layers: - one: title: Layer One sources: [s] - two: title: Layer Two sources: [s] - three: title: Layer Three sources: [s] ''') conf = ProxyConfiguration(conf) root = conf.wms_root_layer.wms_layer() # no root layer defined eq_(root.title, None) eq_(root.name, None) layers = root.child_layers() # names are in order eq_(layers.keys(), ['one', 'two', 'three']) eq_(len(layers), 3) eq_(layers['one'].title, 'Layer One') eq_(layers['two'].title, 'Layer Two') eq_(layers['three'].title, 'Layer Three') layers_conf = conf.layers eq_(len(layers_conf), 3) def test_legacy_unordered(self): conf = self._test_conf(''' layers: one: title: Layer One sources: [s] two: title: Layer Two sources: [s] three: title: Layer Three sources: [s] ''') conf = ProxyConfiguration(conf) root = conf.wms_root_layer.wms_layer() # no root layer defined eq_(root.title, None) eq_(root.name, None) layers = root.child_layers() # names might not be in order # layers.keys() != ['one', 'two', 'three'] eq_(len(layers), 3) eq_(layers['one'].title, 'Layer One') eq_(layers['two'].title, 'Layer Two') eq_(layers['three'].title, 'Layer Three') def test_with_root(self): conf = self._test_conf(''' layers: name: root title: Root Layer layers: - name: one title: Layer One sources: [s] - name: two title: Layer Two sources: [s] ''') conf = ProxyConfiguration(conf) root = conf.wms_root_layer.wms_layer() eq_(root.title, 'Root Layer') eq_(root.name, 'root') layers = root.child_layers() # names are in order eq_(layers.keys(), ['root', 'one', 'two']) eq_(len(layers), 3) eq_(layers['root'].title, 'Root Layer') eq_(layers['one'].title, 'Layer One') eq_(layers['two'].title, 'Layer Two') layers_conf = conf.layers eq_(len(layers_conf), 2) def test_with_unnamed_root(self): conf = self._test_conf(''' layers: title: Root Layer layers: - name: one title: Layer One sources: [s] - name: two title: Layer Two sources: [s] ''') conf = ProxyConfiguration(conf) root = conf.wms_root_layer.wms_layer() eq_(root.title, 'Root Layer') eq_(root.name, None) layers = root.child_layers() # names are in order eq_(layers.keys(), ['one', 'two']) def test_without_root(self): conf = self._test_conf(''' layers: - name: one title: Layer One sources: [s] - name: two title: Layer Two sources: [s] ''') conf = ProxyConfiguration(conf) root = conf.wms_root_layer.wms_layer() eq_(root.title, None) eq_(root.name, None) layers = root.child_layers() # names are in order eq_(layers.keys(), ['one', 'two']) def test_hierarchy(self): conf = self._test_conf(''' layers: title: Root Layer layers: - name: one title: Layer One layers: - name: onea title: Layer One A sources: [s] - name: oneb title: Layer One B layers: - name: oneba title: Layer One B A sources: [s] - name: onebb title: Layer One B B sources: [s] - name: two title: Layer Two sources: [s] ''') conf = ProxyConfiguration(conf) root = conf.wms_root_layer.wms_layer() eq_(root.title, 'Root Layer') eq_(root.name, None) layers = root.child_layers() # names are in order eq_(layers.keys(), ['one', 'onea', 'oneb', 'oneba', 'onebb', 'two']) layers_conf = conf.layers eq_(len(layers_conf), 4) eq_(layers_conf.keys(), ['onea', 'oneba', 'onebb', 'two']) eq_(layers_conf['onea'].conf['title'], 'Layer One A') eq_(layers_conf['onea'].conf['name'], 'onea') eq_(layers_conf['onea'].conf['sources'], ['s']) def test_hierarchy_root_is_list(self): conf = self._test_conf(''' layers: - title: Root Layer layers: - name: one title: Layer One sources: [s] - name: two title: Layer Two sources: [s] ''') conf = ProxyConfiguration(conf) root = conf.wms_root_layer.wms_layer() eq_(root.title, 'Root Layer') eq_(root.name, None) layers = root.child_layers() # names are in order eq_(layers.keys(), ['one', 'two']) def test_without_sources_or_layers(self): conf = self._test_conf(''' services: wms: layers: title: Root Layer layers: - name: one title: Layer One ''') conf = ProxyConfiguration(conf) assert conf.wms_root_layer.wms_layer() == None try: conf.services.services() except ConfigurationError: # found no WMS layer pass else: assert False, 'expected ConfigurationError' class TestGridConfiguration(object): def test_default_grids(self): conf = {} conf = ProxyConfiguration(conf) grid = conf.grids['GLOBAL_MERCATOR'].tile_grid() eq_(grid.srs, SRS(900913)) grid = conf.grids['GLOBAL_GEODETIC'].tile_grid() eq_(grid.srs, SRS(4326)) def test_simple(self): conf = {'grids': {'grid': {'srs': 'EPSG:4326', 'bbox': [5, 50, 10, 55]}}} conf = ProxyConfiguration(conf) grid = conf.grids['grid'].tile_grid() eq_(grid.srs, SRS(4326)) def test_with_base(self): conf = {'grids': { 'base_grid': {'srs': 'EPSG:4326', 'bbox': [5, 50, 10, 55]}, 'grid': {'base': 'base_grid'} }} conf = ProxyConfiguration(conf) grid = conf.grids['grid'].tile_grid() eq_(grid.srs, SRS(4326)) def test_with_num_levels(self): conf = {'grids': {'grid': {'srs': 'EPSG:4326', 'bbox': [5, 50, 10, 55], 'num_levels': 8}}} conf = ProxyConfiguration(conf) grid = conf.grids['grid'].tile_grid() eq_(len(grid.resolutions), 8) def test_with_bbox_srs(self): conf = {'grids': {'grid': {'srs': 'EPSG:25832', 'bbox': [5, 50, 10, 55], 'bbox_srs': 'EPSG:4326'}}} conf = ProxyConfiguration(conf) grid = conf.grids['grid'].tile_grid() assert_almost_equal_bbox([213372, 5538660, 571666, 6102110], grid.bbox, -3) def test_with_min_res(self): conf = {'grids': {'grid': {'srs': 'EPSG:4326', 'bbox': [5, 50, 10, 55], 'min_res': 0.0390625}}} conf = ProxyConfiguration(conf) grid = conf.grids['grid'].tile_grid() assert_almost_equal_bbox([5, 50, 10, 55], grid.bbox, 2) eq_(grid.resolution(0), 0.0390625) eq_(grid.resolution(1), 0.01953125) def test_with_max_res(self): conf = {'grids': {'grid': {'srs': 'EPSG:4326', 'bbox': [5, 50, 10, 55], 'max_res': 0.0048828125}}} conf = ProxyConfiguration(conf) grid = conf.grids['grid'].tile_grid() assert_almost_equal_bbox([5, 50, 10, 55], grid.bbox, 2) eq_(grid.resolution(0), 0.01953125) eq_(grid.resolution(1), 0.01953125/2) class TestWMSSourceConfiguration(object): def test_simple_grid(self): conf_dict = { 'grids': { 'grid': {'srs': 'EPSG:4326', 'bbox': [5, 50, 10, 55]}, }, 'sources': { 'osm': { 'type': 'wms', 'req': { 'url': 'http://localhost/service?', 'layers': 'base', }, }, }, 'caches': { 'osm': { 'sources': ['osm'], 'grids': ['grid'], } } } conf = ProxyConfiguration(conf_dict) caches = conf.caches['osm'].caches() eq_(len(caches), 1) grid, extent, manager = caches[0] eq_(grid.srs, SRS(4326)) eq_(grid.bbox, (5.0, 50.0, 10.0, 55.0)) assert isinstance(manager, TileManager) def check_source_layers(self, conf_dict, layers): conf = ProxyConfiguration(conf_dict) caches = conf.caches['osm'].caches() eq_(len(caches), 1) grid, extent, manager = caches[0] source_layers = manager.sources[0].client.request_template.params.layers eq_(source_layers, layers) def test_tagged_source(self): conf_dict = { 'sources': { 'osm': { 'type': 'wms', 'req': { 'url': 'http://localhost/service?', }, }, }, 'caches': { 'osm': { 'sources': ['osm:base,roads'], 'grids': ['GLOBAL_MERCATOR'], } } } self.check_source_layers(conf_dict, ['base', 'roads']) def test_tagged_source_with_layers(self): conf_dict = { 'sources': { 'osm': { 'type': 'wms', 'req': { 'url': 'http://localhost/service?', 'layers': 'base,roads,poi' }, }, }, 'caches': { 'osm': { 'sources': ['osm:base,roads'], 'grids': ['GLOBAL_MERCATOR'], } } } self.check_source_layers(conf_dict, ['base', 'roads']) def test_tagged_source_with_layers_missing(self): conf_dict = { 'sources': { 'osm': { 'type': 'wms', 'req': { 'url': 'http://localhost/service?', 'layers': 'base,poi' }, }, }, 'caches': { 'osm': { 'sources': ['osm:base,roads'], 'grids': ['GLOBAL_MERCATOR'], } } } conf = ProxyConfiguration(conf_dict) try: conf.caches['osm'].caches() except ConfigurationError as ex: assert 'base,roads' in ex.args[0] assert ('base,poi' in ex.args[0] or 'poi,base' in ex.args[0]) else: assert False, 'expected ConfigurationError' def test_tagged_source_on_non_wms_source(self): conf_dict = { 'sources': { 'osm': { 'type': 'tile', 'url': 'http://example.org/' }, }, 'caches': { 'osm': { 'sources': ['osm:base,roads'], 'grids': ['GLOBAL_MERCATOR'], } } } conf = ProxyConfiguration(conf_dict) try: conf.caches['osm'].caches() except ConfigurationError as ex: assert 'osm:base,roads' in ex.args[0] else: assert False, 'expected ConfigurationError' def test_layer_tagged_source(self): conf_dict = { 'layers': [ { 'name': 'osm', 'title': 'OSM', 'sources': ['osm:base,roads'] } ], 'sources': { 'osm': { 'type': 'wms', 'req': { 'url': 'http://localhost/service?', }, }, }, } conf = ProxyConfiguration(conf_dict) wms_layer = conf.layers['osm'].wms_layer() layers = wms_layer.map_layers[0].client.request_template.params.layers eq_(layers, ['base', 'roads']) def test_tagged_source_encoding(self): conf_dict = { 'layers': [ { 'name': 'osm', 'title': 'OSM', 'sources': [u'osm:☃'] } ], 'sources': { 'osm': { 'type': 'wms', 'req': { 'url': 'http://localhost/service?', }, }, }, 'caches': { 'osm': { 'sources': [u'osm:☃'], 'grids': ['GLOBAL_MERCATOR'], } } } # from source conf = ProxyConfiguration(conf_dict) wms_layer = conf.layers['osm'].wms_layer() layers = wms_layer.map_layers[0].client.request_template.params.layers eq_(layers, [u'☃']) # from cache self.check_source_layers(conf_dict, [u'☃']) def test_https_source_insecure(self): conf_dict = { 'sources': { 'osm': { 'type': 'wms', 'http':{'ssl_no_cert_checks': True}, 'req': { 'url': 'https://foo:bar@localhost/service?', 'layers': 'base', }, }, }, } conf = ProxyConfiguration(conf_dict) try: conf.sources['osm'].source({'format': 'image/png'}) except ImportError: raise SkipTest('no ssl support') class TestBandMergeConfig(object): def test_invalid_band(self): conf_dict = { 'caches': { 'osm': { 'sources': {'f': [{'source': 'foo', 'band': 1}]}, 'grids': ['GLOBAL_WEBMERCATOR'], } } } errors, informal_only = validate_options(conf_dict) eq_(len(errors), 1) assert "unknown 'f' in caches" in errors[0] def test_no_band_cache(self): conf_dict = { 'caches': { 'osm': { 'sources': {'l': [{'source': 'foo'}]}, 'grids': ['GLOBAL_WEBMERCATOR'], } } } errors, informal_only = validate_options(conf_dict) eq_(len(errors), 1) assert "missing 'band', not in caches" in errors[0], errors def load_services(conf_file): conf = load_configuration(conf_file) return conf.configured_services() class TestConfLoading(object): yaml_string = b""" services: wms: layers: - name: osm title: OSM sources: [osm] sources: osm: type: wms supported_srs: ['EPSG:31467'] req: url: http://foo layers: base """ def test_loading(self): with TempFile() as f: open(f, 'wb').write(self.yaml_string) services = load_services(f) assert 'service' in services[0].names def test_loading_broken_yaml(self): with TempFile() as f: open(f, 'wb').write(b'\tbroken:foo') try: load_services(f) except ConfigurationError: pass else: assert False, 'expected configuration error' class TestConfImport(object): yaml_string = """ globals: http: client_timeout: 1 headers: baz: quux """ yaml_parent = """ globals: http: client_timeout: 2 headers: foo: bar bar: qux baz: qax """ yaml_grand_parent = """ globals: http: client_timeout: 3 method: GET headers: bar: baz """ def test_loading(self): with TempFile() as gp: open(gp, 'wb').write(self.yaml_grand_parent.encode('utf-8')) self.yaml_parent = """ base: - %s %s """ % (gp, self.yaml_parent) with TempFile() as p: open(p, 'wb').write(self.yaml_parent.encode("utf-8")) self.yaml_string = """ base: [%s] %s """ % (p, self.yaml_string) with TempFile() as cfg: open(cfg, 'wb').write(self.yaml_string.encode("utf-8")) config = load_configuration(cfg) http = config.globals.get_value('http') eq_(http['client_timeout'], 1) eq_(http['headers']['bar'], 'qux') eq_(http['headers']['foo'], 'bar') eq_(http['headers']['baz'], 'quux') eq_(http['method'], 'GET') config_files = config.config_files() eq_(set(config_files.keys()), set([gp, p, cfg])) assert abs(config_files[gp] - time.time()) < 10 assert abs(config_files[p] - time.time()) < 10 assert abs(config_files[cfg] - time.time()) < 10 class TestConfMerger(object): def test_empty_base(self): a = {'a': 1, 'b': [12, 13]} b = {} m = merge_dict(a, b) eq_(a, m) def test_empty_conf(self): a = {} b = {'a': 1, 'b': [12, 13]} m = merge_dict(a, b) eq_(b, m) def test_differ(self): a = {'a': 12} b = {'b': 42} m = merge_dict(a, b) eq_({'a': 12, 'b': 42}, m) def test_recursive(self): a = {'a': {'aa': 12, 'a':{'aaa': 100}}} b = {'a': {'aa': 11, 'ab': 13, 'a':{'aaa': 101, 'aab': 101}}} m = merge_dict(a, b) eq_({'a': {'aa': 12, 'ab': 13, 'a':{'aaa': 100, 'aab': 101}}}, m) class TestLoadConfiguration(object): def test_with_warnings(object): with TempFile() as f: open(f, 'wb').write(b""" services: unknown: """) load_configuration(f) # defaults to ignore_warnings=True assert_raises(ConfigurationError, load_configuration, f, ignore_warnings=False) class TestImageOptions(object): def test_default_format(self): conf_dict = { } conf = ProxyConfiguration(conf_dict) image_opts = conf.globals.image_options.image_opts({}, 'image/png') eq_(image_opts.format, 'image/png') eq_(image_opts.mode, None) eq_(image_opts.colors, 256) eq_(image_opts.transparent, None) eq_(image_opts.resampling, 'bicubic') def test_default_format_paletted_false(self): conf_dict = {'globals': {'image': { 'paletted': False }}} conf = ProxyConfiguration(conf_dict) image_opts = conf.globals.image_options.image_opts({}, 'image/png') eq_(image_opts.format, 'image/png') eq_(image_opts.mode, None) eq_(image_opts.colors, None) eq_(image_opts.transparent, None) eq_(image_opts.resampling, 'bicubic') def test_update_default_format(self): conf_dict = {'globals': {'image': {'formats': { 'image/png': {'colors': 16, 'resampling_method': 'nearest', 'encoding_options': {'quantizer': 'mediancut'}} }}}} conf = ProxyConfiguration(conf_dict) image_opts = conf.globals.image_options.image_opts({}, 'image/png') eq_(image_opts.format, 'image/png') eq_(image_opts.mode, None) eq_(image_opts.colors, 16) eq_(image_opts.transparent, None) eq_(image_opts.resampling, 'nearest') eq_(image_opts.encoding_options['quantizer'], 'mediancut') def test_custom_format(self): conf_dict = {'globals': {'image': {'resampling_method': 'bilinear', 'formats': { 'image/foo': {'mode': 'RGBA', 'colors': 42} } }}} conf = ProxyConfiguration(conf_dict) image_opts = conf.globals.image_options.image_opts({}, 'image/foo') eq_(image_opts.format, 'image/foo') eq_(image_opts.mode, 'RGBA') eq_(image_opts.colors, 42) eq_(image_opts.transparent, None) eq_(image_opts.resampling, 'bilinear') def test_format_grid(self): conf_dict = { 'globals': { 'image': { 'resampling_method': 'bilinear', } }, 'caches': { 'test': { 'sources': [], 'grids': ['GLOBAL_MERCATOR'], 'format': 'image/png', } } } conf = ProxyConfiguration(conf_dict) image_opts = conf.caches['test'].image_opts() eq_(image_opts.format, 'image/png') eq_(image_opts.mode, None) eq_(image_opts.colors, 256) eq_(image_opts.transparent, None) eq_(image_opts.resampling, 'bilinear') def test_custom_format_grid(self): conf_dict = { 'globals': { 'image': { 'resampling_method': 'bilinear', 'formats': { 'png8': {'mode': 'P', 'colors': 256}, 'image/png': {'mode': 'RGBA', 'transparent': True} }, } }, 'caches': { 'test': { 'sources': [], 'grids': ['GLOBAL_MERCATOR'], 'format': 'png8', 'image': { 'colors': 16, } }, 'test2': { 'sources': [], 'grids': ['GLOBAL_MERCATOR'], 'format': 'image/png', 'image': { 'colors': 8, } } } } conf = ProxyConfiguration(conf_dict) image_opts = conf.caches['test'].image_opts() eq_(image_opts.format, 'image/png') eq_(image_opts.mode, 'P') eq_(image_opts.colors, 16) eq_(image_opts.transparent, None) eq_(image_opts.resampling, 'bilinear') image_opts = conf.caches['test2'].image_opts() eq_(image_opts.format, 'image/png') eq_(image_opts.mode, 'RGBA') eq_(image_opts.colors, 8) eq_(image_opts.transparent, True) eq_(image_opts.resampling, 'bilinear') def test_custom_format_source(self): conf_dict = { 'globals': { 'image': { 'resampling_method': 'bilinear', 'formats': { 'png8': {'mode': 'P', 'colors': 256, 'format': 'image/png'}, 'image/png': {'mode': 'RGBA', 'transparent': True} }, } }, 'caches': { 'test': { 'sources': ['test_source'], 'grids': ['GLOBAL_MERCATOR'], 'format': 'png8', 'image': { 'colors': 16, } }, }, 'sources': { 'test_source': { 'type': 'wms', 'req': { 'url': 'http://example.org/', 'layers': 'foo', } } } } conf = ProxyConfiguration(conf_dict) _grid, _extent, tile_mgr = conf.caches['test'].caches()[0] image_opts = tile_mgr.image_opts eq_(image_opts.format, 'image/png') eq_(image_opts.mode, 'P') eq_(image_opts.colors, 16) eq_(image_opts.transparent, None) eq_(image_opts.resampling, 'bilinear') image_opts = tile_mgr.sources[0].image_opts eq_(image_opts.format, 'image/png') eq_(image_opts.mode, 'P') eq_(image_opts.colors, 256) eq_(image_opts.transparent, None) eq_(image_opts.resampling, 'bilinear') conf_dict['caches']['test']['request_format'] = 'image/tiff' conf = ProxyConfiguration(conf_dict) _grid, _extent, tile_mgr = conf.caches['test'].caches()[0] image_opts = tile_mgr.image_opts eq_(image_opts.format, 'image/png') eq_(image_opts.mode, 'P') eq_(image_opts.colors, 16) eq_(image_opts.transparent, None) eq_(image_opts.resampling, 'bilinear') image_opts = tile_mgr.sources[0].image_opts eq_(image_opts.format, 'image/tiff') eq_(image_opts.mode, None) eq_(image_opts.colors, None) eq_(image_opts.transparent, None) eq_(image_opts.resampling, 'bilinear') def test_encoding_options_errors(self): conf_dict = { 'globals': { 'image': { 'formats': { 'image/jpeg': { 'encoding_options': { 'foo': 'baz', } } }, } }, } try: conf = ProxyConfiguration(conf_dict) except ConfigurationError: pass else: raise False('expected ConfigurationError') conf_dict['globals']['image']['formats']['image/jpeg']['encoding_options'] = { 'quantizer': 'foo' } try: conf = ProxyConfiguration(conf_dict) except ConfigurationError: pass else: raise False('expected ConfigurationError') conf_dict['globals']['image']['formats']['image/jpeg']['encoding_options'] = {} conf = ProxyConfiguration(conf_dict) try: conf.globals.image_options.image_opts({'encoding_options': {'quantizer': 'foo'}}, 'image/jpeg') except ConfigurationError: pass else: raise False('expected ConfigurationError') conf_dict['globals']['image']['formats']['image/jpeg']['encoding_options'] = { 'quantizer': 'fastoctree' } conf = ProxyConfiguration(conf_dict) conf.globals.image_options.image_opts({}, 'image/jpeg') class TestCoverageValidation(object): def test_union(self): conf = { 'coverages': { 'covname': { 'union': [ {'bbox': [0, 0, 10, 10], 'srs': 'EPSG:4326'}, {'bbox': [10, 0, 20, 10], 'srs': 'EPSG:4326', 'unknown': True}, ], }, }, } errors, informal_only = validate_seed_conf(conf) assert informal_only assert len(errors) == 1 eq_(errors[0], "unknown 'unknown' in coverages.covname.union[1]") class TestLoadCoverage(object): def test_load_empty_geojson(self): with TempFile() as tf: with open(tf, 'wb') as f: f.write(b'{"type": "FeatureCollection", "features": []}') conf = {'datasource': tf, 'srs': 'EPSG:4326'} assert_raises(EmptyGeometryError, load_coverage, conf) def test_load_empty_geojson_ogr(self): with TempFile() as tf: with open(tf, 'wb') as f: f.write(b'{"type": "FeatureCollection", "features": []}') conf = {'datasource': tf, 'where': '0 != 1', 'srs': 'EPSG:4326'} assert_raises(EmptyGeometryError, load_coverage, conf) mapproxy-1.11.0/mapproxy/test/unit/test_conf_validator.py000066400000000000000000000264551320454472400237120ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2015 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function import yaml from mapproxy.config.validator import validate_references from nose.tools import eq_ class TestValidator(object): def _test_conf(self, yaml_part=None): base = yaml.load(''' services: wms: md: title: MapProxy layers: - name: one title: One sources: [one_cache] caches: one_cache: grids: [GLOBAL_MERCATOR] sources: [one_source] sources: one_source: type: wms req: url: http://localhost/service? layers: one ''') if yaml_part is not None: base.update(yaml.load(yaml_part)) return base def test_valid_config(self): conf = self._test_conf() errors = validate_references(conf) eq_(errors, []) def test_missing_layer_source(self): conf = self._test_conf() del conf['caches']['one_cache'] errors = validate_references(conf) eq_(errors, [ "Source 'one_cache' for layer 'one' not in cache or source section" ]) def test_empty_layer_sources(self): conf = self._test_conf(''' layers: - name: one title: One sources: [] ''') errors = validate_references(conf) eq_(errors, [ "Missing sources for layer 'one'" ]) def test_missing_cache_source(self): conf = self._test_conf() del conf['sources']['one_source'] errors = validate_references(conf) eq_(errors, [ "Source 'one_source' for cache 'one_cache' not found in config" ]) def test_missing_layers_section(self): conf = self._test_conf() del conf['layers'] errors = validate_references(conf) eq_(errors, [ 'Missing layers section' ]) def test_missing_services_section(self): conf = self._test_conf() del conf['services'] errors = validate_references(conf) eq_(errors, [ 'Missing services section' ]) def test_tile_source(self): conf = self._test_conf(''' layers: - name: one tile_sources: [missing] ''') errors = validate_references(conf) eq_(errors, [ "Tile source 'missing' for layer 'one' not in cache section" ]) def test_missing_grid(self): conf = self._test_conf(''' caches: one_cache: grids: [MYGRID_OTHERGRID] grids: MYGRID: base: GLOBAL_GEODETIC ''') errors = validate_references(conf) eq_(errors, [ "Grid 'MYGRID_OTHERGRID' for cache 'one_cache' not found in config" ]) def test_misconfigured_wms_source(self): conf = self._test_conf() del conf['sources']['one_source']['req']['layers'] errors = validate_references(conf) eq_(errors, [ "Missing 'layers' for source 'one_source'" ]) def test_misconfigured_mapserver_source_without_globals(self): conf = self._test_conf(''' sources: one_source: type: mapserver req: map: foo.map mapserver: binary: /foo/bar/baz ''') errors = validate_references(conf) eq_(errors, [ 'Could not find mapserver binary (/foo/bar/baz)' ]) del conf['sources']['one_source']['mapserver']['binary'] errors = validate_references(conf) eq_(errors, [ "Missing mapserver binary for source 'one_source'" ]) del conf['sources']['one_source']['mapserver'] errors = validate_references(conf) eq_(errors, [ "Missing mapserver binary for source 'one_source'" ]) def test_misconfigured_mapserver_source_with_globals(self): conf = self._test_conf(''' sources: one_source: type: mapserver req: map: foo.map globals: mapserver: binary: /foo/bar/baz ''') errors = validate_references(conf) eq_(errors, [ 'Could not find mapserver binary (/foo/bar/baz)' ]) del conf['globals']['mapserver']['binary'] errors = validate_references(conf) eq_(errors, [ "Missing mapserver binary for source 'one_source'" ]) def test_tagged_sources_with_layers(self): conf = self._test_conf(''' caches: one_cache: grids: [GLOBAL_MERCATOR] sources: ['one_source:foo,bar'] ''') errors = validate_references(conf) eq_(errors, [ "Supported layers for source 'one_source' are 'one' but tagged source " "requested layers 'foo, bar'" ]) def test_tagged_source_without_layers(self): conf = self._test_conf(''' caches: one_cache: grids: [GLOBAL_MERCATOR] sources: ['one_source:foo,bar'] ''') del conf['sources']['one_source']['req']['layers'] errors = validate_references(conf) eq_(errors, []) def test_tagged_source_with_colons(self): conf = self._test_conf(''' caches: one_cache: grids: [GLOBAL_MERCATOR] sources: ['one_source:ns:foo,ns:bar'] ''') del conf['sources']['one_source']['req']['layers'] errors = validate_references(conf) eq_(errors, []) def test_with_grouped_layer(self): conf = self._test_conf(''' layers: - name: group title: Group layers: - name: one title: One sources: [one_cache] ''') errors = validate_references(conf) eq_(errors, []) def test_without_cache(self): conf = self._test_conf(''' layers: - name: one title: One sources: [one_source] ''') errors = validate_references(conf) eq_(errors, []) def test_mapserver_with_tagged_layers(self): conf = self._test_conf(''' sources: one_source: type: mapserver req: map: foo.map layers: one mapserver: binary: /foo/bar/baz caches: one_cache: grids: [GLOBAL_MERCATOR] sources: ['one_source:foo,bar'] ''') errors = validate_references(conf) eq_(errors, [ 'Could not find mapserver binary (/foo/bar/baz)', "Supported layers for source 'one_source' are 'one' but tagged source " "requested layers 'foo, bar'" ]) def test_mapnik_with_tagged_layers(self): conf = self._test_conf(''' sources: one_source: type: mapnik mapfile: foo.map layers: one caches: one_cache: grids: [GLOBAL_MERCATOR] sources: ['one_source:foo,bar'] ''') errors = validate_references(conf) eq_(errors, [ "Supported layers for source 'one_source' are 'one' but tagged source " "requested layers 'foo, bar'" ]) def test_tagged_layers_for_unsupported_source_type(self): conf = self._test_conf(''' sources: one_source: type: tile url: http://localhost/tiles/ caches: one_cache: grids: [GLOBAL_MERCATOR] sources: ['one_source:foo,bar'] ''') errors = validate_references(conf) eq_(errors, [ "Found tagged source 'one_source' in cache 'one_cache' but tagged sources " "only supported for 'wms, mapserver, mapnik' sources" ]) def test_cascaded_caches(self): conf = self._test_conf(''' caches: one_cache: sources: [two_cache] two_cache: grids: [GLOBAL_MERCATOR] sources: ['one_source'] ''') errors = validate_references(conf) eq_(errors, []) def test_with_int_0_as_names_and_layers(self): conf = self._test_conf(''' services: wms: md: title: MapProxy layers: - name: 0 title: One sources: [0] caches: 0: grids: [GLOBAL_MERCATOR] sources: [0] sources: 0: type: wms req: url: http://localhost/service? layers: 0 ''') errors = validate_references(conf) eq_(errors, []) def test_band_merge_missing_source(self): conf = self._test_conf(''' caches: one_cache: sources: l: - source: dop band: 1 factor: 0.4 - source: missing1 band: 2 factor: 0.2 - source: cache_missing_source band: 2 factor: 0.2 grids: [GLOBAL_MERCATOR] cache_missing_source: sources: [missing2] grids: [GLOBAL_MERCATOR] sources: dop: type: wms req: url: http://localhost/service? layers: dop ''') errors = validate_references(conf) eq_(errors, [ "Source 'missing1' for cache 'one_cache' not found in config", "Source 'missing2' for cache 'cache_missing_source' not found in config", ]) mapproxy-1.11.0/mapproxy/test/unit/test_config.py000066400000000000000000000073611320454472400221600ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.config import Options, base_config, load_base_config from mapproxy.test.helper import TempFiles def teardown_module(): load_base_config(clear_existing=True) class TestOptions(object): def test_update_overwrite(self): d = Options(foo='bar', baz=4) d.update(Options(baz=5)) assert d.baz == 5 assert d.foo == 'bar' def test_update_new(self): d = Options(foo='bar', baz=4) d.update(Options(biz=5)) assert d.baz == 4 assert d.biz == 5 assert d.foo == 'bar' def test_update_recursive(self): d = Options( foo='bar', baz=Options(ham=2, eggs=4)) d.update(Options(baz=Options(eggs=5))) assert d.foo == 'bar' assert d.baz.ham == 2 assert d.baz.eggs == 5 def test_compare(self): assert Options(foo=4) == Options(foo=4) assert Options(foo=Options(bar=4)) == Options(foo=Options(bar=4)) class TestDefaultsLoading(object): defaults_yaml = b""" foo: bar: ham: 2 eggs: 4 biz: 'foobar' wiz: 'foobar' """ def test_defaults(self): with TempFiles() as tmp: with open(tmp[0], 'wb') as f: f.write(TestDefaultsLoading.defaults_yaml) load_base_config(config_file=tmp[0], clear_existing=True) assert base_config().biz == 'foobar' assert base_config().wiz == 'foobar' assert base_config().foo.bar.ham == 2 assert base_config().foo.bar.eggs == 4 assert not hasattr(base_config(), 'wms') def test_defaults_overwrite(self): with TempFiles(2) as tmp: with open(tmp[0], 'wb') as f: f.write(TestDefaultsLoading.defaults_yaml) with open(tmp[1], 'wb') as f: f.write(b""" baz: [9, 2, 1, 4] biz: 'barfoo' foo: bar: eggs: 5 """) load_base_config(config_file=tmp[0], clear_existing=True) load_base_config(config_file=tmp[1]) assert base_config().biz == 'barfoo' assert base_config().wiz == 'foobar' assert base_config().baz == [9, 2, 1, 4] assert base_config().foo.bar.ham == 2 assert base_config().foo.bar.eggs == 5 assert not hasattr(base_config(), 'wms') class TestSRSConfig(object): def setup(self): import mapproxy.config.config mapproxy.config.config._config.pop() def test_user_srs_definitions(self): user_yaml = b""" srs: axis_order_ne: ['EPSG:9999'] """ with TempFiles() as tmp: with open(tmp[0], 'wb') as f: f.write(user_yaml) load_base_config(config_file=tmp[0]) assert 'EPSG:9999' in base_config().srs.axis_order_ne assert 'EPSG:9999' not in base_config().srs.axis_order_en #defaults still there assert 'EPSG:31468' in base_config().srs.axis_order_ne assert 'CRS:84' in base_config().srs.axis_order_en mapproxy-1.11.0/mapproxy/test/unit/test_decorate_img.py000066400000000000000000000143451320454472400233350ustar00rootroot00000000000000from mapproxy.grid import tile_grid from mapproxy.image import BlankImageSource from mapproxy.image import ImageSource from mapproxy.image.opts import ImageOptions from mapproxy.layer import MapLayer, DefaultMapExtent from mapproxy.compat.image import Image from mapproxy.service.base import Server from mapproxy.service.tile import TileServer from mapproxy.service.wms import WMSGroupLayer, WMSServer from mapproxy.service.wmts import WMTSServer from mapproxy.test.http import make_wsgi_env from mapproxy.util.ext.odict import odict from nose.tools import eq_ class DummyLayer(MapLayer): transparent = True extent = DefaultMapExtent() has_legend = False queryable = False def __init__(self, name): MapLayer.__init__(self) self.name = name self.requested = False self.queried = False def get_map(self, query): self.requested = True def get_info(self, query): self.queried = True def map_layers_for_query(self, query): return [(self.name, self)] def info_layers_for_query(self, query): return [(self.name, self)] class DummyTileLayer(object): def __init__(self, name): self.requested = False self.name = name self.grid = tile_grid(900913) def tile_bbox(self, request, use_profiles=False): # this dummy code does not handle profiles and different tile origins! return self.grid.tile_bbox(request.tile) def render(self, tile_request, use_profiles=None, coverage=None, decorate_img=None): self.requested = True resp = BlankImageSource((256, 256), image_opts=ImageOptions(format='image/png')) resp.timestamp = 0 return resp class TestDecorateImg(object): def setup(self): # Base server self.server = Server() # WMS Server root_layer = WMSGroupLayer(None, 'root layer', None, [DummyLayer('wms_cache')]) self.wms_server = WMSServer( md={}, root_layer=root_layer, srs=['EPSG:4326'], image_formats={'image/png': ImageOptions(format='image/png')} ) # Tile Servers layers = odict() layers["wms_cache_EPSG900913"] = DummyTileLayer('wms_cache') self.tile_server = TileServer(layers, {}) self.wmts_server = WMTSServer(layers, {}) # Common arguments self.query_extent = ('EPSG:27700', (0, 0, 700000, 1300000)) def test_original_imagesource_returned_when_no_callback(self): img_src1 = ImageSource(Image.new('RGBA', (100, 100))) env = make_wsgi_env('', extra_environ={}) img_src2 = self.server.decorate_img( img_src1, 'wms.map', ['layer1'], env, self.query_extent ) eq_(img_src1, img_src2) def test_returns_imagesource(self): img_src1 = ImageSource(Image.new('RGBA', (100, 100))) env = make_wsgi_env('', extra_environ={}) img_src2 = self.server.decorate_img( img_src1, 'wms.map', ['layer1'], env, self.query_extent ) assert isinstance(img_src2, ImageSource) def set_called_callback(self, img_src, service, layers, **kw): self.called = True return img_src def test_calls_callback(self): img_src1 = ImageSource(Image.new('RGBA', (100, 100))) self.called = False env = make_wsgi_env('', extra_environ={'mapproxy.decorate_img': self.set_called_callback}) img_src2 = self.server.decorate_img( img_src1, 'wms.map', ['layer1'], env, self.query_extent ) eq_(self.called, True) def return_new_imagesource_callback(self, img_src, service, layers, **kw): new_img_src = ImageSource(Image.new('RGBA', (100, 100))) self.new_img_src = new_img_src return new_img_src def test_returns_callbacks_return_value(self): img_src1 = ImageSource(Image.new('RGBA', (100, 100))) env = make_wsgi_env('', extra_environ={'mapproxy.decorate_img': self.return_new_imagesource_callback}) self.new_img_src = None img_src2 = self.server.decorate_img( img_src1, 'wms.map', ['layer1'], env, self.query_extent ) eq_(img_src2, self.new_img_src) def test_wms_server(self): ''' Test that the decorate_img method is available on a WMSServer instance ''' img_src1 = ImageSource(Image.new('RGBA', (100, 100))) self.called = False env = make_wsgi_env('', extra_environ={'mapproxy.decorate_img': self.set_called_callback}) img_src2 = self.wms_server.decorate_img( img_src1, 'wms.map', ['layer1'], env, self.query_extent ) eq_(self.called, True) def test_tile_server(self): ''' Test that the decorate_img method is available on a TileServer instance ''' img_src1 = ImageSource(Image.new('RGBA', (100, 100))) self.called = False env = make_wsgi_env('', extra_environ={'mapproxy.decorate_img': self.set_called_callback}) img_src2 = self.tile_server.decorate_img( img_src1, 'tms', ['layer1'], env, self.query_extent ) eq_(self.called, True) def test_wmts_server(self): ''' Test that the decorate_img method is available on a WMTSServer instance ''' img_src1 = ImageSource(Image.new('RGBA', (100, 100))) self.called = False env = make_wsgi_env('', extra_environ={'mapproxy.decorate_img': self.set_called_callback}) img_src2 = self.wmts_server.decorate_img( img_src1, 'wmts', ['layer1'], env, self.query_extent ) eq_(self.called, True) def test_args(self): def callback(img_src, service, layers, environ, query_extent, **kw): assert isinstance(img_src, ImageSource) eq_('wms.map', service) assert isinstance(layers, list) assert isinstance(environ, dict) assert len(query_extent) == 2 assert len(query_extent[1]) == 4 return img_src img_src1 = ImageSource(Image.new('RGBA', (100, 100))) env = make_wsgi_env('', extra_environ={'mapproxy.decorate_img': callback}) img_src2 = self.tile_server.decorate_img( img_src1, 'wms.map', ['layer1'], env, self.query_extent ) mapproxy-1.11.0/mapproxy/test/unit/test_exceptions.py000066400000000000000000000237301320454472400230720ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.compat.image import Image from io import BytesIO from mapproxy.test.helper import Mocker, validate_with_dtd, validate_with_xsd from mapproxy.test.image import is_png from mapproxy.request.wms import WMSMapRequest from mapproxy.request import url_decode from mapproxy.exception import RequestError from mapproxy.request.wms.exception import (WMS100ExceptionHandler, WMS111ExceptionHandler, WMS130ExceptionHandler, WMS110ExceptionHandler) from nose.tools import eq_ class ExceptionHandlerTest(Mocker): def setup(self): Mocker.setup(self) req = url_decode("""LAYERS=foo&FORMAT=image%2Fpng&SERVICE=WMS&VERSION=1.1.1& REQUEST=GetMap&STYLES=&EXCEPTIONS=application%2Fvnd.ogc.se_xml&SRS=EPSG%3A900913& BBOX=8,4,9,5&WIDTH=150&HEIGHT=100""".replace('\n', '')) self.req = req class TestWMS111ExceptionHandler(Mocker): def test_render(self): req = self.mock(WMSMapRequest) req_ex = RequestError('the exception message', request=req) ex_handler = WMS111ExceptionHandler() self.expect(req.exception_handler).result(ex_handler) self.replay() response = req_ex.render() assert response.content_type == 'application/vnd.ogc.se_xml' expected_resp = b""" the exception message """ assert expected_resp.strip() == response.data assert validate_with_dtd(response.data, 'wms/1.1.1/exception_1_1_1.dtd') def test_render_w_code(self): req = self.mock(WMSMapRequest) req_ex = RequestError('the exception message', code='InvalidFormat', request=req) ex_handler = WMS111ExceptionHandler() self.expect(req.exception_handler).result(ex_handler) self.replay() response = req_ex.render() assert response.content_type == 'application/vnd.ogc.se_xml' expected_resp = b""" the exception message """ assert expected_resp.strip() == response.data assert validate_with_dtd(response.data, 'wms/1.1.1/exception_1_1_1.dtd') class TestWMS110ExceptionHandler(Mocker): def test_render(self): req = self.mock(WMSMapRequest) req_ex = RequestError('the exception message', request=req) ex_handler = WMS110ExceptionHandler() self.expect(req.exception_handler).result(ex_handler) self.replay() response = req_ex.render() assert response.content_type == 'application/vnd.ogc.se_xml' expected_resp = b""" the exception message """ assert expected_resp.strip() == response.data assert validate_with_dtd(response.data, 'wms/1.1.0/exception_1_1_0.dtd') def test_render_w_code(self): req = self.mock(WMSMapRequest) req_ex = RequestError('the exception message', code='InvalidFormat', request=req) ex_handler = WMS110ExceptionHandler() self.expect(req.exception_handler).result(ex_handler) self.replay() response = req_ex.render() assert response.content_type == 'application/vnd.ogc.se_xml' expected_resp = b""" the exception message """ eq_(expected_resp.strip(), response.data) assert validate_with_dtd(response.data, 'wms/1.1.0/exception_1_1_0.dtd') class TestWMS130ExceptionHandler(Mocker): def test_render(self): req = self.mock(WMSMapRequest) req_ex = RequestError('the exception message', request=req) ex_handler = WMS130ExceptionHandler() self.expect(req.exception_handler).result(ex_handler) self.replay() response = req_ex.render() assert response.content_type == 'text/xml; charset=utf-8' expected_resp = b""" the exception message """ assert expected_resp.strip() == response.data assert validate_with_xsd(response.data, 'wms/1.3.0/exceptions_1_3_0.xsd') def test_render_w_code(self): req = self.mock(WMSMapRequest) req_ex = RequestError('the exception message', code='InvalidFormat', request=req) ex_handler = WMS130ExceptionHandler() self.expect(req.exception_handler).result(ex_handler) self.replay() response = req_ex.render() assert response.content_type == 'text/xml; charset=utf-8' expected_resp = b""" the exception message """ assert expected_resp.strip() == response.data assert validate_with_xsd(response.data, 'wms/1.3.0/exceptions_1_3_0.xsd') class TestWMS100ExceptionHandler(Mocker): def test_render(self): req = self.mock(WMSMapRequest) req_ex = RequestError('the exception message', request=req) ex_handler = WMS100ExceptionHandler() self.expect(req.exception_handler).result(ex_handler) self.replay() response = req_ex.render() assert response.content_type == 'text/xml' expected_resp = b""" the exception message """ assert expected_resp.strip() == response.data class TestWMSImageExceptionHandler(ExceptionHandlerTest): def test_exception(self): self.req.set('exceptions', 'inimage') self.req.set('transparent', 'true' ) req = WMSMapRequest(self.req) req_ex = RequestError('the exception message', request=req) response = req_ex.render() assert response.content_type == 'image/png' data = BytesIO(response.data) assert is_png(data) img = Image.open(data) assert img.size == (150, 100) def test_exception_w_transparent(self): self.req.set('exceptions', 'inimage') self.req.set('transparent', 'true' ) req = WMSMapRequest(self.req) req_ex = RequestError('the exception message', request=req) response = req_ex.render() assert response.content_type == 'image/png' data = BytesIO(response.data) assert is_png(data) img = Image.open(data) assert img.size == (150, 100) eq_(sorted([x for x in img.histogram() if x > 25]), [377, 14623]) img = img.convert('RGBA') eq_(img.getpixel((0, 0))[3], 0) class TestWMSBlankExceptionHandler(ExceptionHandlerTest): def test_exception(self): self.req['exceptions'] = 'blank' req = WMSMapRequest(self.req) req_ex = RequestError('the exception message', request=req) response = req_ex.render() assert response.content_type == 'image/png' data = BytesIO(response.data) assert is_png(data) img = Image.open(data) assert img.size == (150, 100) eq_(img.getpixel((0, 0)), 0) #pallete image eq_(img.getpalette()[0:3], [255, 255, 255]) def test_exception_w_bgcolor(self): self.req.set('exceptions', 'blank') self.req.set('bgcolor', '0xff00ff') req = WMSMapRequest(self.req) req_ex = RequestError('the exception message', request=req) response = req_ex.render() assert response.content_type == 'image/png' data = BytesIO(response.data) assert is_png(data) img = Image.open(data) assert img.size == (150, 100) eq_(img.getpixel((0, 0)), 0) #pallete image eq_(img.getpalette()[0:3], [255, 0, 255]) def test_exception_w_transparent(self): self.req.set('exceptions', 'blank') self.req.set('transparent', 'true' ) req = WMSMapRequest(self.req) req_ex = RequestError('the exception message', request=req) response = req_ex.render() assert response.content_type == 'image/png' data = BytesIO(response.data) assert is_png(data) img = Image.open(data) assert img.size == (150, 100) assert img.mode == 'P' img = img.convert('RGBA') eq_(img.getpixel((0, 0))[3], 0) mapproxy-1.11.0/mapproxy/test/unit/test_featureinfo.py000066400000000000000000000151631320454472400232210ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import tempfile from lxml import etree, html from nose.tools import eq_ from mapproxy.featureinfo import ( combined_inputs, XSLTransformer, XMLFeatureInfoDoc, HTMLFeatureInfoDoc, JSONFeatureInfoDoc, ) from mapproxy.test.helper import strip_whitespace def test_combined_inputs(): foo = 'foo' bar = 'bar' result = combined_inputs([foo, bar]) result = etree.tostring(result) eq_(result, b'foobar') class TestXSLTransformer(object): def setup(self): fd_, self.xsl_script = tempfile.mkstemp('.xsl') xsl = b""" """.strip() open(self.xsl_script, 'wb').write(xsl) def teardown(self): os.remove(self.xsl_script) def test_transformer(self): t = XSLTransformer(self.xsl_script) doc = t.transform(XMLFeatureInfoDoc('Text')) eq_(strip_whitespace(doc.as_string()), b'Text') def test_multiple(self): t = XSLTransformer(self.xsl_script) doc = t.transform(XMLFeatureInfoDoc.combine([ XMLFeatureInfoDoc(x) for x in [b'ab', b'ab1ab2ab3', b'ab1acab2', ]])) eq_(strip_whitespace(doc.as_string()), strip_whitespace(b''' ab ab1ab2ab3 ab1ab2 ''')) eq_(doc.info_type, 'xml') class TestXMLFeatureInfoDocs(object): def test_as_string(self): input_tree = etree.fromstring('') doc = XMLFeatureInfoDoc(input_tree) eq_(strip_whitespace(doc.as_string()), b'') def test_as_etree(self): doc = XMLFeatureInfoDoc('hello') eq_(doc.as_etree().getroot().text, 'hello') def test_combine(self): docs = [ XMLFeatureInfoDoc('foo'), XMLFeatureInfoDoc('bar'), XMLFeatureInfoDoc('baz'), ] result = XMLFeatureInfoDoc.combine(docs) eq_(strip_whitespace(result.as_string()), strip_whitespace(b'foobarbaz')) eq_(result.info_type, 'xml') class TestXMLFeatureInfoDocsNoLXML(object): def setup(self): from mapproxy import featureinfo self.old_etree = featureinfo.etree featureinfo.etree = None def teardown(self): from mapproxy import featureinfo featureinfo.etree = self.old_etree def test_combine(self): docs = [ XMLFeatureInfoDoc(b'foo'), XMLFeatureInfoDoc(b'bar'), XMLFeatureInfoDoc(b'baz'), ] result = XMLFeatureInfoDoc.combine(docs) eq_(b'foo\nbar\nbaz', result.as_string()) eq_(result.info_type, 'text') class TestHTMLFeatureInfoDocs(object): def test_as_string(self): input_tree = html.fromstring('

Foo') doc = HTMLFeatureInfoDoc(input_tree) assert b'

Foo

' in strip_whitespace(doc.as_string()) def test_as_etree(self): doc = HTMLFeatureInfoDoc('

hello

') eq_(doc.as_etree().find('body/p').text, 'hello') def test_combine(self): docs = [ HTMLFeatureInfoDoc(b'Hello<body><p>baz</p><p>baz2'), HTMLFeatureInfoDoc(b'<p>foo</p>'), HTMLFeatureInfoDoc(b'<body><p>bar</p></body>'), ] result = HTMLFeatureInfoDoc.combine(docs) assert b'<title>Hello' in result.as_string() assert (b'

baz

baz2

foo

bar

' in result.as_string()) eq_(result.info_type, 'html') def test_combine_parts(self): docs = [ HTMLFeatureInfoDoc('

foo

'), HTMLFeatureInfoDoc('

bar

'), HTMLFeatureInfoDoc('Hello<body><p>baz</p><p>baz2'), ] result = HTMLFeatureInfoDoc.combine(docs) assert (b'<body><p>foo</p><p>bar</p><p>baz</p><p>baz2</p></body>' in result.as_string()) eq_(result.info_type, 'html') class TestHTMLFeatureInfoDocsNoLXML(object): def setup(self): from mapproxy import featureinfo self.old_etree = featureinfo.etree featureinfo.etree = None def teardown(self): from mapproxy import featureinfo featureinfo.etree = self.old_etree def test_combine(self): docs = [ HTMLFeatureInfoDoc(b'<html><head><title>Hello<body><p>baz</p><p>baz2'), HTMLFeatureInfoDoc(b'<p>foo</p>'), HTMLFeatureInfoDoc(b'<body><p>bar</p></body>'), ] result = HTMLFeatureInfoDoc.combine(docs) eq_(b"<html><head><title>Hello<body><p>baz</p>" b"<p>baz2\n<p>foo</p>\n<body><p>bar</p></body>", result.as_string()) eq_(result.info_type, 'text') class TestJSONFeatureInfoDocs(object): def test_combine(self): docs = [ JSONFeatureInfoDoc('{}'), JSONFeatureInfoDoc('{"results": [{"foo": 1}]}'), JSONFeatureInfoDoc('{"results": [{"bar": 2}]}'), ] result = JSONFeatureInfoDoc.combine(docs) eq_('''{"results": [{"foo": 1}, {"bar": 2}]}''', result.as_string()) eq_(result.info_type, 'json') �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_file_lock_load.py�������������������������������������������0000664�0000000�0000000�00000001635�13204544724�0023637�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import tempfile import os import shutil import time import threading import multiprocessing from mapproxy.util.lock import FileLock from nose.tools import eq_ lock_dir = tempfile.mkdtemp() lock_file = os.path.join(lock_dir, 'lock.lck') count_file = os.path.join(lock_dir, 'count.txt') open(count_file, 'w').write('0') def lock(p=None): l = FileLock(lock_file, timeout=60) l.lock() counter = int(open(count_file).read()) open(count_file, 'w').write(str(counter+1)) time.sleep(0.001) l.unlock() def test_file_lock_load(): def lock_x(): for x in range(5): time.sleep(0.01) lock() threads = [threading.Thread(target=lock_x) for _ in range(20)] p = multiprocessing.Pool(5) [t.start() for t in threads] p.map(lock, range(50)) [t.join() for t in threads] eq_(int(open(count_file).read()), 150) def teardown(): shutil.rmtree(lock_dir) ���������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_geom.py�����������������������������������������������������0000664�0000000�0000000�00000054163�13204544724�0021644�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import os import tempfile import shutil from mapproxy.srs import SRS, bbox_equals from mapproxy.util.geom import ( load_polygons, load_datasource, load_geojson, load_expire_tiles, transform_geometry, geom_support, bbox_polygon, build_multipolygon, ) from mapproxy.util.coverage import ( coverage, MultiCoverage, union_coverage, diff_coverage, intersection_coverage, ) from mapproxy.layer import MapExtent, DefaultMapExtent from mapproxy.test.helper import TempFile if not geom_support: from nose.plugins.skip import SkipTest raise SkipTest('requires Shapely') from mapproxy.util.coverage import BBOXCoverage import shapely import shapely.prepared try: # shapely >=1.6 from shapely.errors import ReadingError except ImportError: from shapely.geos import ReadingError from nose.tools import eq_, raises VALID_POLYGON1 = b"""POLYGON ((953296.704552185838111 7265916.626927595585585, 944916.907243740395643 7266183.505430161952972, 943803.712335807620548 7266450.200959664769471, 935361.798751499853097 7269866.750814219936728, 934743.530299633974209 7270560.353549793362617, 934743.530299633974209 7271628.176921582780778, 935794.720251194899902 7274619.979839355684817, 936567.834114754223265 7275849.767033117823303, 937959.439069160842337 7277078.402297221124172, 940062.041611264110543 7278254.31110474281013, 941948.350382756092586 7278948.856433514505625, 950513.717282353783958 7279590.533784243278205, 951905.099597778869793 7279323.193848768249154, 953976.97796042333357 7278628.807455806992948, 955337.636096389498562 7277987.20964437816292, 955646.770322322496213 7277612.74426197167486, 955894.122230865177698 7277238.489366835914552, 956759.965230255154893 7273070.375410236418247, 956790.912048695725389 7272483.464432151056826, 954255.388006897410378 7266929.622660100460052, 953760.684189812047407 7266129.1298723295331, 953296.704552185838111 7265916.626927595585585))""".replace(b'\n', b' ') VALID_POLYGON2 = b"""POLYGON ((929919.722805089084432 7252212.673410807736218, 929393.960850072442554 7252372.056830812245607, 928651.905124444281682 7252957.449742536991835, 927507.763398071052507 7254289.325379111804068, 923735.145855087204836 7261007.430086207576096, 923394.953491222811863 7261914.35770049970597, 923333.171173832495697 7262554.628265766426921, 923580.523082375293598 7263621.350993251428008, 924786.558445629663765 7266503.041579172946513, 925281.262262714910321 7267303.380754604935646, 928713.687441834714264 7270453.271698194555938, 929486.801305394037627 7271041.567251891829073, 929950.558304038597271 7271201.337567078880966, 930414.426622174330987 7270987.157654598355293, 935083.722663498250768 7255089.941797585226595, 931527.621530107106082 7252531.635323006659746, 931125.535529361688532 7252317.969672014936805, 929919.722805089084432 7252212.673410807736218))""".replace(b'\n', b' ') INTERSECTING_POLYGONS = """POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0)) POLYGON ((5 0, 15 0, 15 10, 5 10, 5 0)) """ class TestPolygonLoading(object): def test_loading_polygon(self): with TempFile() as fname: with open(fname, 'wb') as f: f.write(VALID_POLYGON1) polygon = load_polygons(fname) bbox, polygon = build_multipolygon(polygon, simplify=True) assert polygon.is_valid eq_(polygon.type, 'Polygon') def test_loading_multipolygon(self): with TempFile() as fname: with open(fname, 'wb') as f: f.write(VALID_POLYGON1) f.write(b'\n') f.write(VALID_POLYGON2) polygon = load_polygons(fname) bbox, polygon = build_multipolygon(polygon, simplify=True) assert polygon.is_valid eq_(polygon.type, 'MultiPolygon') @raises(ReadingError) def test_loading_broken(self): with TempFile() as fname: with open(fname, 'wb') as f: f.write(b"POLYGON((") polygon = load_polygons(fname) assert polygon.is_valid bbox, polygon = build_multipolygon(polygon, simplify=True) def test_loading_skip_non_polygon(self): with TempFile() as fname: with open(fname, 'wb') as f: f.write(b"POINT(0 0)\n") f.write(VALID_POLYGON1) polygon = load_polygons(fname) bbox, polygon = build_multipolygon(polygon, simplify=True) assert polygon.is_valid eq_(polygon.type, 'Polygon') def test_loading_intersecting_polygons(self): # check that the self intersection is eliminated # otherwise the geometry will be invalid with TempFile() as fname: with open(fname, 'w') as f: f.write(INTERSECTING_POLYGONS) polygon = load_polygons(fname) bbox, polygon = build_multipolygon(polygon, simplify=True) assert polygon.is_valid eq_(polygon.type, 'Polygon') assert polygon.equals(shapely.geometry.Polygon([(0, 0), (15, 0), (15, 10), (0, 10)])) class TestGeoJSONLoading(object): def test_geojson(self): yield (self.check_geojson, '''{"type": "Polygon", "coordinates": [[[0, 0], [10, 0], [10, 10], [0, 0]]]}''', shapely.geometry.Polygon([[0, 0], [10, 0], [10, 10], [0, 0]]), ) yield (self.check_geojson, '''{"type": "MultiPolygon", "coordinates": [[[[0, 0], [10, 0], [10, 10], [0, 0]]], [[[20, 0], [30, 0], [20, 10], [20, 0]]]]}''', shapely.geometry.Polygon([[0, 0], [10, 0], [10, 10], [0, 0]]).union(shapely.geometry.Polygon([[20, 0], [30, 0], [20, 10], [20, 0]])), ) yield (self.check_geojson, '''{"type": "Feature", "geometry": {"type": "Polygon", "coordinates": [[[0, 0], [10, 0], [10, 10], [0, 0]]]}}''', shapely.geometry.Polygon([[0, 0], [10, 0], [10, 10], [0, 0]]), ) yield (self.check_geojson, '''{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Polygon", "coordinates": [[[0, 0], [10, 0], [10, 10], [0, 0]]]}}]}''', shapely.geometry.Polygon([[0, 0], [10, 0], [10, 10], [0, 0]]), ) def check_geojson(self, geojson, geometry): with TempFile() as fname: with open(fname, 'w') as f: f.write(geojson) polygon = load_geojson(fname) bbox, polygon = build_multipolygon(polygon, simplify=True) assert polygon.is_valid assert polygon.type in ('Polygon', 'MultiPolygon'), polygon.type assert polygon.equals(geometry) class TestTransform(object): def test_polygon_transf(self): p1 = shapely.geometry.Polygon([(0, 0), (10, 0), (10, 10), (0, 10)]) p2 = transform_geometry(SRS(4326), SRS(900913), p1) assert p2.contains(shapely.geometry.Point((1000000, 1000000))) p3 = transform_geometry(SRS(900913), SRS(4326), p2) assert p3.symmetric_difference(p1).area < 0.00001 def test_multipolygon_transf(self): p1 = shapely.geometry.Polygon([(0, 0), (10, 0), (10, 10), (0, 10)]) p2 = shapely.geometry.Polygon([(20, 20), (30, 20), (30, 30), (20, 30)]) mp1 = shapely.geometry.MultiPolygon([p1, p2]) mp2 = transform_geometry(SRS(4326), SRS(900913), mp1) assert mp2.contains(shapely.geometry.Point((1000000, 1000000))) assert not mp2.contains(shapely.geometry.Point((2000000, 2000000))) assert mp2.contains(shapely.geometry.Point((3000000, 3000000))) mp3 = transform_geometry(SRS(900913), SRS(4326), mp2) assert mp3.symmetric_difference(mp1).area < 0.00001 @raises(ValueError) def test_invalid_transf(self): p = shapely.geometry.Point((0, 0)) transform_geometry(SRS(4326), SRS(900913), p) class TestBBOXPolygon(object): def test_bbox_polygon(self): p = bbox_polygon([5, 53, 6, 54]) eq_(p.type, 'Polygon') class TestGeomCoverage(object): def setup(self): # box from 10 10 to 80 80 with small spike/corner to -10 60 (upper left) self.geom = shapely.wkt.loads( "POLYGON((10 10, 10 50, -10 60, 10 80, 80 80, 80 10, 10 10))") self.coverage = coverage(self.geom, SRS(4326)) def test_bbox(self): assert bbox_equals(self.coverage.bbox, [-10, 10, 80, 80], 0.0001) def test_geom(self): eq_(self.coverage.geom.type, 'Polygon') def test_contains(self): assert self.coverage.contains((15, 15, 20, 20), SRS(4326)) assert self.coverage.contains((15, 15, 80, 20), SRS(4326)) assert not self.coverage.contains((9, 10, 20, 20), SRS(4326)) def test_intersects(self): assert self.coverage.intersects((15, 15, 20, 20), SRS(4326)) assert self.coverage.intersects((15, 15, 80, 20), SRS(4326)) assert self.coverage.intersects((9, 10, 20, 20), SRS(4326)) assert self.coverage.intersects((-30, 10, -8, 70), SRS(4326)) assert not self.coverage.intersects((-30, 10, -11, 70), SRS(4326)) assert not self.coverage.intersects((0, 0, 1000, 1000), SRS(900913)) assert self.coverage.intersects((0, 0, 1500000, 1500000), SRS(900913)) def test_prepared(self): assert hasattr(self.coverage, '_prepared_max') self.coverage._prepared_max = 100 for i in range(110): assert self.coverage.intersects((-30, 10, -8, 70), SRS(4326)) def test_eq(self): g1 = shapely.wkt.loads("POLYGON((10 10, 10 50, -10 60, 10 80, 80 80, 80 10, 10 10))") g2 = shapely.wkt.loads("POLYGON((10 10, 10 50, -10 60, 10 80, 80 80, 80 10, 10 10))") assert coverage(g1, SRS(4326)) == coverage(g2, SRS(4326)) assert coverage(g1, SRS(4326)) != coverage(g2, SRS(31467)) g3 = shapely.wkt.loads("POLYGON((10.0 10, 10 50.0, -10 60, 10 80, 80 80, 80 10, 10 10))") assert coverage(g1, SRS(4326)) == coverage(g3, SRS(4326)) g4 = shapely.wkt.loads("POLYGON((10 10, 10.1 50, -10 60, 10 80, 80 80, 80 10, 10 10))") assert coverage(g1, SRS(4326)) != coverage(g4, SRS(4326)) class TestBBOXCoverage(object): def setup(self): self.coverage = coverage([-10, 10, 80, 80], SRS(4326)) def test_bbox(self): assert bbox_equals(self.coverage.bbox, [-10, 10, 80, 80], 0.0001) def test_geom(self): eq_(self.coverage.geom, None) def test_contains(self): assert self.coverage.contains((15, 15, 20, 20), SRS(4326)) assert self.coverage.contains((15, 15, 79, 20), SRS(4326)) assert self.coverage.contains((9, 10, 20, 20), SRS(4326)) assert not self.coverage.contains((9, 9.99999999, 20, 20), SRS(4326)) def test_intersects(self): assert self.coverage.intersects((15, 15, 20, 20), SRS(4326)) assert self.coverage.intersects((15, 15, 80, 20), SRS(4326)) assert self.coverage.intersects((9, 10, 20, 20), SRS(4326)) assert self.coverage.intersects((-30, 10, -8, 70), SRS(4326)) assert not self.coverage.intersects((-30, 10, -11, 70), SRS(4326)) assert not self.coverage.intersects((0, 0, 1000, 1000), SRS(900913)) assert self.coverage.intersects((0, 0, 1500000, 1500000), SRS(900913)) def test_intersection(self): eq_(self.coverage.intersection((15, 15, 20, 20), SRS(4326)), BBOXCoverage((15, 15, 20, 20), SRS(4326))) eq_(self.coverage.intersection((15, 15, 80, 20), SRS(4326)), BBOXCoverage((15, 15, 80, 20), SRS(4326))) eq_(self.coverage.intersection((9, 10, 20, 20), SRS(4326)), BBOXCoverage((9, 10, 20, 20), SRS(4326))) eq_(self.coverage.intersection((-30, 10, -8, 70), SRS(4326)), BBOXCoverage((-10, 10, -8, 70), SRS(4326))) eq_(self.coverage.intersection((-30, 10, -11, 70), SRS(4326)), None) eq_(self.coverage.intersection((0, 0, 1000, 1000), SRS(900913)), None) assert bbox_equals( self.coverage.intersection((0, 0, 1500000, 1500000), SRS(900913)).bbox, (0.0, 10, 13.47472926179282, 13.352207626707813) ) def test_eq(self): assert coverage([-10, 10, 80, 80], SRS(4326)) == coverage([-10, 10, 80, 80], SRS(4326)) assert coverage([-10, 10, 80, 80], SRS(4326)) == coverage([-10, 10.0, 80.0, 80], SRS(4326)) assert coverage([-10, 10, 80, 80], SRS(4326)) != coverage([-10.1, 10.0, 80.0, 80], SRS(4326)) assert coverage([-10, 10, 80, 80], SRS(4326)) != coverage([-10, 10.0, 80.0, 80], SRS(31467)) class TestUnionCoverage(object): def setup(self): self.coverage = union_coverage([ coverage([0, 0, 10, 10], SRS(4326)), coverage(shapely.wkt.loads("POLYGON((10 0, 20 0, 20 10, 10 10, 10 0))"), SRS(4326)), coverage(shapely.wkt.loads("POLYGON((-1000000 0, 0 0, 0 1000000, -1000000 1000000, -1000000 0))"), SRS(3857)), ]) def test_bbox(self): assert bbox_equals(self.coverage.bbox, [-8.98315284, 0.0, 20.0, 10.0], 0.0001), self.coverage.bbox def test_contains(self): assert self.coverage.contains((0, 0, 5, 5), SRS(4326)) assert self.coverage.contains((-50000, 0, -20000, 20000), SRS(3857)) assert not self.coverage.contains((-50000, -100, -20000, 20000), SRS(3857)) def test_intersects(self): assert self.coverage.intersects((0, 0, 5, 5), SRS(4326)) assert self.coverage.intersects((5, 0, 25, 5), SRS(4326)) assert self.coverage.intersects((-50000, 0, -20000, 20000), SRS(3857)) assert self.coverage.intersects((-50000, -100, -20000, 20000), SRS(3857)) class TestDiffCoverage(object): def setup(self): g1 = coverage(shapely.wkt.loads("POLYGON((-10 0, 20 0, 20 10, -10 10, -10 0))"), SRS(4326)) g2 = coverage([0, 2, 8, 8], SRS(4326)) g3 = coverage(shapely.wkt.loads("POLYGON((-1000000 500000, 0 500000, 0 1000000, -1000000 1000000, -1000000 500000))"), SRS(3857)) self.coverage = diff_coverage([g1, g2, g3]) def test_bbox(self): assert bbox_equals(self.coverage.bbox, [-10, 0.0, 20.0, 10.0], 0.0001), self.coverage.bbox def test_contains(self): assert self.coverage.contains((0, 0, 1, 1), SRS(4326)) assert self.coverage.contains((-1100000, 510000, -1050000, 600000), SRS(3857)) assert not self.coverage.contains((-1100000, 510000, -990000, 600000), SRS(3857)) # touches # g3 assert not self.coverage.contains((4, 4, 5, 5), SRS(4326)) # in g2 def test_intersects(self): assert self.coverage.intersects((0, 0, 1, 1), SRS(4326)) assert self.coverage.intersects((-1100000, 510000, -1050000, 600000), SRS(3857)) assert self.coverage.intersects((-1100000, 510000, -990000, 600000), SRS(3857)) # touches # g3 assert not self.coverage.intersects((4, 4, 5, 5), SRS(4326)) # in g2 class TestIntersectionCoverage(object): def setup(self): g1 = coverage(shapely.wkt.loads("POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))"), SRS(4326)) g2 = coverage([5, 5, 15, 15], SRS(4326)) self.coverage = intersection_coverage([g1, g2]) def test_bbox(self): assert bbox_equals(self.coverage.bbox, [5.0, 5.0, 10.0, 10.0], 0.0001), self.coverage.bbox def test_contains(self): assert not self.coverage.contains((0, 0, 1, 1), SRS(4326)) assert self.coverage.contains((6, 6, 7, 7), SRS(4326)) def test_intersects(self): assert self.coverage.intersection((3, 6, 7, 7), SRS(4326)) assert self.coverage.intersection((6, 6, 7, 7), SRS(4326)) assert not self.coverage.intersects((0, 0, 1, 1), SRS(4326)) class TestMultiCoverage(object): def setup(self): # box from 10 10 to 80 80 with small spike/corner to -10 60 (upper left) self.geom = shapely.wkt.loads( "POLYGON((10 10, 10 50, -10 60, 10 80, 80 80, 80 10, 10 10))") self.coverage1 = coverage(self.geom, SRS(4326)) self.coverage2 = coverage([100, 0, 120, 20], SRS(4326)) self.coverage = MultiCoverage([self.coverage1, self.coverage2]) def test_bbox(self): assert bbox_equals(self.coverage.bbox, [-10, 0, 120, 80], 0.0001) def test_contains(self): assert self.coverage.contains((15, 15, 20, 20), SRS(4326)) assert self.coverage.contains((15, 15, 79, 20), SRS(4326)) assert not self.coverage.contains((9, 10, 20, 20), SRS(4326)) assert self.coverage.contains((110, 5, 115, 15), SRS(4326)) def test_intersects(self): assert self.coverage.intersects((15, 15, 20, 20), SRS(4326)) assert self.coverage.intersects((15, 15, 80, 20), SRS(4326)) assert self.coverage.intersects((9, 10, 20, 20), SRS(4326)) assert self.coverage.intersects((-30, 10, -8, 70), SRS(4326)) assert not self.coverage.intersects((-30, 10, -11, 70), SRS(4326)) assert not self.coverage.intersects((0, 0, 1000, 1000), SRS(900913)) assert self.coverage.intersects((0, 0, 1500000, 1500000), SRS(900913)) assert self.coverage.intersects((110, 5, 115, 15), SRS(4326)) assert self.coverage.intersects((90, 5, 105, 15), SRS(4326)) def test_eq(self): g1 = shapely.wkt.loads("POLYGON((10 10, 10 50, -10 60, 10 80, 80 80, 80 10, 10 10))") g2 = shapely.wkt.loads("POLYGON((10 10, 10 50, -10 60, 10 80, 80 80, 80 10, 10 10))") assert MultiCoverage([coverage(g1, SRS(4326))]) == MultiCoverage([coverage(g2, SRS(4326))]) assert MultiCoverage([coverage(g1, SRS(4326))]) != MultiCoverage([coverage(g2, SRS(31467))]) c = coverage([-10, 10, 80, 80], SRS(4326)) assert MultiCoverage([c, coverage(g1, SRS(4326))]) != MultiCoverage([c, coverage(g2, SRS(31467))]) class TestMapExtent(object): def setup(self): self.extent = MapExtent([-10, 10, 80, 80], SRS(4326)) def test_bbox(self): assert bbox_equals(self.extent.bbox, [-10, 10, 80, 80], 0.0001) def test_contains(self): assert self.extent.contains(MapExtent((15, 15, 20, 20), SRS(4326))) assert self.extent.contains(MapExtent((15, 15, 79, 20), SRS(4326))) assert self.extent.contains(MapExtent((9, 10, 20, 20), SRS(4326))) assert not self.extent.contains(MapExtent((9, 9.99999999, 20, 20), SRS(4326))) def test_intersects(self): assert self.extent.intersects(MapExtent((15, 15, 20, 20), SRS(4326))) assert self.extent.intersects(MapExtent((15, 15, 80, 20), SRS(4326))) assert self.extent.intersects(MapExtent((9, 10, 20, 20), SRS(4326))) assert self.extent.intersects(MapExtent((-30, 10, -8, 70), SRS(4326))) assert not self.extent.intersects(MapExtent((-30, 10, -11, 70), SRS(4326))) assert not self.extent.intersects(MapExtent((0, 0, 1000, 1000), SRS(900913))) assert self.extent.intersects(MapExtent((0, 0, 1500000, 1500000), SRS(900913))) def test_eq(self): assert MapExtent([-10, 10, 80, 80], SRS(4326)) == MapExtent([-10, 10, 80, 80], SRS(4326)) assert MapExtent([-10, 10, 80, 80], SRS(4326)) == MapExtent([-10, 10.0, 80.0, 80], SRS(4326)) assert MapExtent([-10, 10, 80, 80], SRS(4326)) != MapExtent([-10.1, 10.0, 80.0, 80], SRS(4326)) assert MapExtent([-10, 10, 80, 80], SRS(4326)) != MapExtent([-10, 10.0, 80.0, 80], SRS(31467)) def test_intersection(self): assert (DefaultMapExtent().intersection(MapExtent((0, 0, 10, 10), SRS(4326))) == MapExtent((0, 0, 10, 10), SRS(4326))) assert (MapExtent((0, 0, 10, 10), SRS(4326)).intersection(MapExtent((20, 20, 30, 30), SRS(4326))) == None) sub = MapExtent((0, 0, 10, 10), SRS(4326)).intersection(MapExtent((-1000, -1000, 100000, 100000), SRS(3857))) bbox = SRS(3857).transform_bbox_to(SRS(4326), (0, 0, 100000, 100000), 0) assert bbox_equals(bbox, sub.bbox) class TestLoadDatasource(object): def test_shp(self): polygon_file = os.path.join(os.path.dirname(__file__), 'polygons', 'polygons.shp') geoms = load_datasource(polygon_file) eq_(len(geoms), 3) def test_wkt(self): with TempFile() as fname: with open(fname, 'wb') as f: f.write(VALID_POLYGON1) f.write(b"\n") f.write(VALID_POLYGON1) geoms = load_datasource(fname) eq_(len(geoms), 2) def test_geojson(self): with TempFile() as fname: with open(fname, 'wb') as f: f.write(b'''{"type": "FeatureCollection", "features": [ {"type": "Feature", "geometry": {"type": "Polygon", "coordinates": [[[0, 0], [10, 0], [10, 10], [0, 0]]]} }, {"type": "Feature", "geometry": {"type": "MultiPolygon", "coordinates": [[[[0, 0], [10, 0], [10, 10], [0, 0]]], [[[0, 0], [10, 0], [10, 10], [0, 0]]], [[[0, 0], [10, 0], [10, 10], [0, 0]]]]} }, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0, 0]} } ]}''') geoms = load_datasource(fname) eq_(len(geoms), 4) def test_expire_tiles_dir(self): dirname = tempfile.mkdtemp() try: fname = os.path.join(dirname, 'tiles') with open(fname, 'wb') as f: f.write(b"4/2/5\n") f.write(b"4/2/6\n") f.write(b"4/4/3\n") geoms = load_expire_tiles(dirname) eq_(len(geoms), 3) finally: shutil.rmtree(dirname) def test_expire_tiles_file(self): with TempFile() as fname: with open(fname, 'wb') as f: f.write(b"4/2/5\n") f.write(b"4/2/6\n") f.write(b"error\n") f.write(b"4/2/1\n") # rest of file is ignored geoms = load_expire_tiles(fname) eq_(len(geoms), 2) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_grid.py�����������������������������������������������������0000664�0000000�0000000�00000135024�13204544724�0021636�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function, division from nose.tools import eq_, assert_almost_equal, raises from mapproxy.grid import ( MetaGrid, TileGrid, _create_tile_list, bbox_intersects, bbox_contains, NoTiles, tile_grid, resolutions, ResolutionRange, resolution_range, merge_resolution_range, ) from mapproxy.srs import SRS, TransformationError class TestResolution(object): def test_min_res(self): conf = dict(min_res=1000) res = resolutions(**conf) eq_(res[:5], [1000, 500.0, 250.0, 125.0, 62.5]) eq_(len(res), 20) def test_min_res_max_res(self): conf = dict(min_res=1000, max_res=80) res = resolutions(**conf) eq_(res, [1000, 500.0, 250.0, 125.0]) def test_min_res_levels(self): conf = dict(min_res=1600, num_levels=5) res = resolutions(**conf) eq_(res, [1600, 800.0, 400.0, 200.0, 100.0]) def test_min_res_levels_res_factor(self): conf = dict(min_res=1600, num_levels=4, res_factor=4.0) res = resolutions(**conf) eq_(res, [1600, 400.0, 100.0, 25.0]) def test_min_res_levels_sqrt2(self): conf = dict(min_res=1600, num_levels=5, res_factor='sqrt2') res = resolutions(**conf) eq_(list(map(round, res)), [1600.0, 1131.0, 800.0, 566.0, 400.0]) def test_min_res_max_res_levels(self): conf = dict(min_res=1600, max_res=10, num_levels=10) res = resolutions(**conf) eq_(len(res), 10) # will calculate log10 based factor of 1.75752... assert_almost_equal(res[0], 1600) assert_almost_equal(res[1], 1600/1.75752, 2) assert_almost_equal(res[8], 1600/1.75752**8, 2) assert_almost_equal(res[9], 10) def test_bbox_levels(self): conf = dict(bbox=[0,40,15,50], num_levels=10, tile_size=(256, 256)) res = resolutions(**conf) eq_(len(res), 10) assert_almost_equal(res[0], 15/256) assert_almost_equal(res[1], 15/512) class TestAlignedGrid(object): def test_epsg_4326_bbox(self): base = tile_grid(srs='epsg:4326') bbox = (10.0, -20.0, 40.0, 10.0) sub = tile_grid(align_with=base, bbox=bbox) eq_(sub.bbox, bbox) eq_(sub.resolution(0), 180/256/8) abbox, grid_size, tiles = sub.get_affected_level_tiles(bbox, 0) eq_(abbox, (10.0, -20.0, 55.0, 25.0)) eq_(grid_size, (2, 2)) eq_(list(tiles), [(0, 1, 0), (1, 1, 0), (0, 0, 0), (1, 0, 0)]) def test_epsg_4326_bbox_from_sqrt2(self): base = tile_grid(srs='epsg:4326', res_factor='sqrt2') bbox = (10.0, -20.0, 40.0, 10.0) sub = tile_grid(align_with=base, bbox=bbox, res_factor=2.0) eq_(sub.bbox, bbox) eq_(sub.resolution(0), base.resolution(8)) eq_(sub.resolution(1), base.resolution(10)) eq_(sub.resolution(2), base.resolution(12)) def test_epsg_4326_bbox_to_sqrt2(self): base = tile_grid(srs='epsg:4326', res_factor=2.0) bbox = (10.0, -20.0, 40.0, 10.0) sub = tile_grid(align_with=base, bbox=bbox, res_factor='sqrt2') eq_(sub.bbox, bbox) eq_(sub.resolution(0), base.resolution(4)) eq_(sub.resolution(2), base.resolution(5)) eq_(sub.resolution(4), base.resolution(6)) assert sub.resolution(0) > sub.resolution(1) > sub.resolution(3) eq_(sub.resolution(3)/2, sub.resolution(5)) def test_metagrid_tiles(): mgrid = MetaGrid(grid=TileGrid(), meta_size=(2, 2)) assert list(mgrid.meta_tile((0, 0, 0)).tile_patterns) == \ [((0, 0, 0), (0, 0))] assert list(mgrid.meta_tile((0, 1, 1)).tile_patterns) == \ [((0, 1, 1), (0, 0)), ((1, 1, 1), (256, 0)), ((0, 0, 1), (0, 256)), ((1, 0, 1), (256, 256))] assert list(mgrid.meta_tile((1, 2, 2)).tile_patterns) == \ [((0, 3, 2), (0, 0)), ((1, 3, 2), (256, 0)), ((0, 2, 2), (0, 256)), ((1, 2, 2), (256, 256))] def test_metagrid_tiles_w_meta_size(): mgrid = MetaGrid(grid=TileGrid(), meta_size=(4, 2)) assert list(mgrid.meta_tile((1, 2, 2)).tile_patterns) == \ [((0, 3, 2), (0, 0)), ((1, 3, 2), (256, 0)), ((2, 3, 2), (512, 0)), ((3, 3, 2), (768, 0)), ((0, 2, 2), (0, 256)), ((1, 2, 2), (256, 256)), ((2, 2, 2), (512, 256)), ((3, 2, 2), (768, 256))] class TestMetaGridGeodetic(object): def setup(self): self.mgrid = MetaGrid(grid=tile_grid('EPSG:4326'), meta_size=(2, 2), meta_buffer=10) def test_meta_bbox_level_0(self): eq_(self.mgrid._meta_bbox((0, 0, 0)), ((-180, -90, 180, 90), (0, 0, 0, -128))) eq_(self.mgrid._meta_bbox((0, 0, 0), limit_to_bbox=False), ((-194.0625, -104.0625, 194.0625, 284.0625), (10, 10, 10, 10))) eq_(self.mgrid.meta_tile((0, 0, 0)).size, (256, 128)) def test_tiles_level_0(self): meta_tile = self.mgrid.meta_tile((0, 0, 0)) eq_(meta_tile.size, (256, 128)) eq_(meta_tile.grid_size, (1, 1)) eq_(meta_tile.tile_patterns, [((0, 0, 0), (0, -128))]) def test_meta_bbox_level_1(self): eq_(self.mgrid._meta_bbox((0, 0, 1)), ((-180, -90, 180, 90), (0, 0, 0, 0))) eq_(self.mgrid._meta_bbox((0, 0, 1), limit_to_bbox=False), ((-187.03125, -97.03125, 187.03125, 97.03125), (10, 10, 10, 10))) eq_(self.mgrid.meta_tile((0, 0, 1)).size, (512, 256)) def test_tiles_level_1(self): eq_(list(self.mgrid.meta_tile((0, 0, 1)).tile_patterns), [ ((0, 0, 1), (0, 0)), ((1, 0, 1), (256, 0)) ]) def test_tile_list_level_1(self): eq_(list(self.mgrid.tile_list((0, 0, 1))), [(0, 0, 1), (1, 0, 1)]) def test_meta_bbox_level_2(self): eq_(self.mgrid._meta_bbox((0, 0, 2)), ((-180, -90, 3.515625, 90), (0, 0, 10, 0))) eq_(self.mgrid._meta_bbox((0, 0, 2), limit_to_bbox=False), ((-183.515625, -93.515625, 3.515625, 93.515625), (10, 10, 10, 10))) eq_(self.mgrid.meta_tile((0, 0, 2)).size, (522, 512)) eq_(self.mgrid._meta_bbox((2, 0, 2)), ((-3.515625, -90, 180, 90), (10, 0, 0, 0))) meta_tile = self.mgrid.meta_tile((2, 0, 2)) eq_(meta_tile.size, (522, 512)) eq_(meta_tile.grid_size, (2, 2)) def test_tiles_level_2(self): eq_(list(self.mgrid.meta_tile((0, 0, 2)).tile_patterns), [ ((0, 1, 2), (0, 0)), ((1, 1, 2), (256, 0)), ((0, 0, 2), (0, 256)), ((1, 0, 2), (256, 256)), ]) eq_(list(self.mgrid.meta_tile((2, 0, 2)).tile_patterns), [ ((2, 1, 2), (10, 0)), ((3, 1, 2), (266, 0)), ((2, 0, 2), (10, 256)), ((3, 0, 2), (266, 256)), ]) def test_tile_list_level_2(self): eq_(list(self.mgrid.tile_list((0, 0, 2))), [(0, 1, 2), (1, 1, 2), (0, 0, 2), (1, 0, 2)]) eq_(list(self.mgrid.tile_list((1, 1, 2))), [(0, 1, 2), (1, 1, 2), (0, 0, 2), (1, 0, 2)]) def test_tiles_level_3(self): eq_(list(self.mgrid.meta_tile((2, 0, 3)).tile_patterns), [ ((2, 1, 3), (10, 10)), ((3, 1, 3), (266, 10)), ((2, 0, 3), (10, 266)), ((3, 0, 3), (266, 266)), ]) eq_(list(self.mgrid.meta_tile((2, 2, 3)).tile_patterns), [ ((2, 3, 3), (10, 0)), ((3, 3, 3), (266, 0)), ((2, 2, 3), (10, 256)), ((3, 2, 3), (266, 256)), ]) class TestMetaGridGeodeticUL(object): def setup(self): self.tile_grid = tile_grid('EPSG:4326', origin='ul') self.mgrid = MetaGrid(grid=self.tile_grid, meta_size=(2, 2), meta_buffer=10) def test_tiles_level_0(self): meta_tile = self.mgrid.meta_tile((0, 0, 0)) eq_(meta_tile.bbox, (-180, -90, 180, 90)) eq_(meta_tile.size, (256, 128)) eq_(meta_tile.grid_size, (1, 1)) eq_(meta_tile.tile_patterns, [((0, 0, 0), (0, 0))]) def test_tiles_level_1(self): meta_tile = self.mgrid.meta_tile((0, 0, 1)) eq_(meta_tile.bbox, (-180, -90, 180, 90)) eq_(meta_tile.size, (512, 256)) eq_(meta_tile.grid_size, (2, 1)) eq_(list(meta_tile.tile_patterns), [ ((0, 0, 1), (0, 0)), ((1, 0, 1), (256, 0)) ]) def test_tile_list_level_1(self): eq_(list(self.mgrid.tile_list((0, 0, 1))), [(0, 0, 1), (1, 0, 1)]) def test_tiles_level_2(self): meta_tile = self.mgrid.meta_tile((0, 0, 2)) eq_(meta_tile.bbox, (-180, -90, 3.515625, 90)) eq_(meta_tile.size, (522, 512)) eq_(meta_tile.grid_size, (2, 2)) eq_(meta_tile.tile_patterns, [ ((0, 0, 2), (0, 0)), ((1, 0, 2), (256, 0)), ((0, 1, 2), (0, 256)), ((1, 1, 2), (256, 256)), ]) eq_(list(self.mgrid.meta_tile((2, 0, 2)).tile_patterns), [ ((2, 0, 2), (10, 0)), ((3, 0, 2), (266, 0)), ((2, 1, 2), (10, 256)), ((3, 1, 2), (266, 256)), ]) def test_tile_list_level_2(self): eq_(list(self.mgrid.tile_list((0, 0, 2))), [(0, 0, 2), (1, 0, 2), (0, 1, 2), (1, 1, 2)]) eq_(list(self.mgrid.tile_list((1, 1, 2))), [(0, 0, 2), (1, 0, 2), (0, 1, 2), (1, 1, 2)]) def test_tiles_level_3(self): meta_tile = self.mgrid.meta_tile((2, 0, 3)) eq_(meta_tile.bbox, (-91.7578125, -1.7578125, 1.7578125, 90)) eq_(meta_tile.size, (532, 522)) eq_(meta_tile.grid_size, (2, 2)) eq_(list(self.mgrid.meta_tile((2, 0, 3)).tile_patterns), [ ((2, 0, 3), (10, 0)), ((3, 0, 3), (266, 0)), ((2, 1, 3), (10, 256)), ((3, 1, 3), (266, 256)), ]) eq_(list(self.mgrid.meta_tile((2, 2, 3)).tile_patterns), [ ((2, 2, 3), (10, 10)), ((3, 2, 3), (266, 10)), ((2, 3, 3), (10, 266)), ((3, 3, 3), (266, 266)), ]) class TestMetaTile(object): def setup(self): self.mgrid = MetaGrid(grid=tile_grid('EPSG:4326'), meta_size=(2, 2), meta_buffer=10) def test_meta_tile(self): meta_tile = self.mgrid.meta_tile((2, 0, 2)) eq_(meta_tile.size, (522, 512)) def test_metatile_bbox(self): mgrid = MetaGrid(grid=TileGrid(), meta_size=(2, 2)) meta_tile = mgrid.meta_tile((0, 0, 2)) assert meta_tile.bbox == (-20037508.342789244, -20037508.342789244, 0.0, 0.0) meta_tile = mgrid.meta_tile((1, 1, 2)) assert meta_tile.bbox == (-20037508.342789244, -20037508.342789244, 0.0, 0.0) meta_tile = mgrid.meta_tile((4, 5, 3)) assert_almost_equal_bbox(meta_tile.bbox, (0.0, 0.0, 10018754.171394622, 10018754.171394622)) def test_metatile_non_default_meta_size(self): mgrid = MetaGrid(grid=TileGrid(), meta_size=(4, 2)) meta_tile = mgrid.meta_tile((4, 5, 3)) assert_almost_equal_bbox(meta_tile.bbox, (0.0, 0.0, 20037508.342789244, 10018754.171394622)) eq_(meta_tile.size, (1024, 512)) eq_(meta_tile.grid_size, (4, 2)) class TestMetaTileSQRT2(object): def setup(self): self.grid = tile_grid('EPSG:4326', res_factor='sqrt2') self.mgrid = MetaGrid(grid=self.grid, meta_size=(4, 4), meta_buffer=10) def test_meta_tile(self): meta_tile = self.mgrid.meta_tile((0, 0, 8)) eq_(meta_tile.size, (1034, 1034)) def test_metatile_bbox(self): meta_tile = self.mgrid.meta_tile((0, 0, 2)) eq_(meta_tile.bbox, (-180, -90, 180, 90)) eq_(meta_tile.size, (512, 256)) eq_(meta_tile.grid_size, (2, 1)) eq_(meta_tile.tile_patterns, [((0, 0, 2), (0, 0)), ((1, 0, 2), (256, 0))]) meta_tile = self.mgrid.meta_tile((1, 0, 2)) eq_(meta_tile.bbox, (-180.0, -90, 180.0, 90.0)) eq_(meta_tile.size, (512, 256)) eq_(meta_tile.grid_size, (2, 1)) meta_tile = self.mgrid.meta_tile((0, 0, 3)) eq_(meta_tile.bbox, (-180.0, -90, 180.0, 90.0)) eq_(meta_tile.size, (724, 362)) eq_(meta_tile.tile_patterns, [((0, 1, 3), (0, -149)), ((1, 1, 3), (256, -149)), ((2, 1, 3), (512, -149)), ((0, 0, 3), (0, 107)), ((1, 0, 3), (256, 107)), ((2, 0, 3), (512, 107))]) def test_metatile_non_default_meta_size(self): mgrid = MetaGrid(grid=self.grid, meta_size=(4, 2), meta_buffer=0) meta_tile = mgrid.meta_tile((4, 3, 6)) eq_(meta_tile.bbox, (0.0, 0.0, 180.0, 90.0)) eq_(meta_tile.size, (1024, 512)) eq_(meta_tile.grid_size, (4, 2)) eq_(meta_tile.tile_patterns, [((4, 3, 6), (0, 0)), ((5, 3, 6), (256, 0)), ((6, 3, 6), (512, 0)), ((7, 3, 6), (768, 0)), ((4, 2, 6), (0, 256)), ((5, 2, 6), (256, 256)), ((6, 2, 6), (512, 256)), ((7, 2, 6), (768, 256))]) class TestMinimalMetaTile(object): def setup(self): self.mgrid = MetaGrid(grid=tile_grid('EPSG:4326'), meta_size=(2, 2), meta_buffer=10) def test_minimal_tiles(self): sgrid = self.mgrid.minimal_meta_tile([(0, 0, 2), (1, 0, 2)]) eq_(sgrid.grid_size, (2, 1)) eq_(list(sgrid.tile_patterns), [ ((0, 0, 2), (0, 10)), ((1, 0, 2), (256, 10)), ] ) eq_(sgrid.bbox, (-180.0, -90.0, 3.515625, 3.515625)) def test_minimal_tiles_fragmented(self): sgrid = self.mgrid.minimal_meta_tile( [ (2, 3, 3), (1, 2, 3), (2, 1, 3), ]) eq_(sgrid.grid_size, (2, 3)) eq_(list(sgrid.tile_patterns), [ ((1, 3, 3), (10, 0)), ((2, 3, 3), (266, 0)), ((1, 2, 3), (10, 256)), ((2, 2, 3), (266, 256)), ((1, 1, 3), (10, 512)), ((2, 1, 3), (266, 512)), ] ) eq_(sgrid.bbox, (-136.7578125, -46.7578125, -43.2421875, 90.0)) def test_minimal_tiles_fragmented_ul(self): self.mgrid = MetaGrid(grid=tile_grid('EPSG:4326', origin='ul'), meta_size=(2, 2), meta_buffer=10) sgrid = self.mgrid.minimal_meta_tile( [ (2, 0, 3), (1, 1, 3), (2, 2, 3), ]) eq_(sgrid.grid_size, (2, 3)) eq_(list(sgrid.tile_patterns), [ ((1, 0, 3), (10, 0)), ((2, 0, 3), (266, 0)), ((1, 1, 3), (10, 256)), ((2, 1, 3), (266, 256)), ((1, 2, 3), (10, 512)), ((2, 2, 3), (266, 512)), ] ) eq_(sgrid.bbox, (-136.7578125, -46.7578125, -43.2421875, 90.0)) class TestMetaGridLevelMetaTiles(object): def __init__(self): self.meta_grid = MetaGrid(TileGrid(), meta_size=(2, 2)) def test_full_grid_0(self): bbox = (-20037508.34, -20037508.34, 20037508.34, 20037508.34) abbox, tile_grid, meta_tiles = \ self.meta_grid.get_affected_level_tiles(bbox, 0) meta_tiles = list(meta_tiles) assert_almost_equal_bbox(bbox, abbox) eq_(len(meta_tiles), 1) eq_(meta_tiles[0], (0, 0, 0)) def test_full_grid_2(self): bbox = (-20037508.34, -20037508.34, 20037508.34, 20037508.34) abbox, tile_grid, meta_tiles = \ self.meta_grid.get_affected_level_tiles(bbox, 2) meta_tiles = list(meta_tiles) assert_almost_equal_bbox(bbox, abbox) eq_(tile_grid, (2, 2)) eq_(len(meta_tiles), 4) eq_(meta_tiles[0], (0, 2, 2)) eq_(meta_tiles[1], (2, 2, 2)) eq_(meta_tiles[2], (0, 0, 2)) eq_(meta_tiles[3], (2, 0, 2)) class TestMetaGridLevelMetaTilesGeodetic(object): def __init__(self): self.meta_grid = MetaGrid(TileGrid(is_geodetic=True), meta_size=(2, 2)) def test_full_grid_2(self): bbox = (-180.0, -90.0, 180.0, 90) abbox, tile_grid, meta_tiles = \ self.meta_grid.get_affected_level_tiles(bbox, 2) meta_tiles = list(meta_tiles) assert_almost_equal_bbox(bbox, abbox) eq_(tile_grid, (2, 1)) eq_(len(meta_tiles), 2) eq_(meta_tiles[0], (0, 0, 2)) eq_(meta_tiles[1], (2, 0, 2)) def test_partial_grid_3(self): bbox = (0.0, 5.0, 45, 40) abbox, tile_grid, meta_tiles = \ self.meta_grid.get_affected_level_tiles(bbox, 3) meta_tiles = list(meta_tiles) assert_almost_equal_bbox((0.0, 0.0, 90.0, 90.0), abbox) eq_(tile_grid, (1, 1)) eq_(len(meta_tiles), 1) eq_(meta_tiles[0], (4, 2, 3)) def assert_grid_size(grid, level, grid_size): print(grid.grid_sizes[level], "==", grid_size) assert grid.grid_sizes[level] == grid_size res = grid.resolutions[level] x, y = grid_size assert res * x * 256 >= grid.bbox[2] - grid.bbox[0] assert res * y * 256 >= grid.bbox[3] - grid.bbox[1] class TileGridTest(object): def check_grid(self, level, grid_size): assert_grid_size(self.grid, level, grid_size) class TestTileGridResolutions(object): def test_explicit_grid(self): grid = TileGrid(res=[0.1, 0.05, 0.01]) eq_(grid.resolution(0), 0.1) eq_(grid.resolution(1), 0.05) eq_(grid.resolution(2), 0.01) eq_(grid.closest_level(0.00001), 2) def test_factor_grid(self): grid = TileGrid(is_geodetic=True, res=1/0.75, tile_size=(360, 180)) eq_(grid.resolution(0), 1.0) eq_(grid.resolution(1), 0.75) eq_(grid.resolution(2), 0.75*0.75) def test_sqrt_grid(self): grid = TileGrid(is_geodetic=True, res='sqrt2', tile_size=(360, 180)) eq_(grid.resolution(0), 1.0) assert_almost_equal(grid.resolution(2), 0.5) assert_almost_equal(grid.resolution(4), 0.25) class TestWGS84TileGrid(object): def setup(self): self.grid = TileGrid(is_geodetic=True) def test_resolution(self): assert_almost_equal(self.grid.resolution(0), 1.40625) assert_almost_equal(self.grid.resolution(1), 1.40625/2) def test_bbox(self): eq_(self.grid.bbox, (-180.0, -90.0, 180.0, 90.0)) def test_grid_size(self): eq_(self.grid.grid_sizes[0], (1, 1)) eq_(self.grid.grid_sizes[1], (2, 1)) eq_(self.grid.grid_sizes[2], (4, 2)) def test_affected_tiles(self): bbox, grid, tiles = self.grid.get_affected_tiles((-180,-90,180,90), (512,256)) eq_(bbox, (-180.0, -90.0, 180.0, 90.0)) eq_(grid, (2, 1)) eq_(list(tiles), [(0, 0, 1), (1, 0, 1)]) def test_affected_level_tiles(self): bbox, grid, tiles = self.grid.get_affected_level_tiles((-180,-90,180,90), 1) eq_(grid, (2, 1)) eq_(bbox, (-180.0, -90.0, 180.0, 90.0)) eq_(list(tiles), [(0, 0, 1), (1, 0, 1)]) bbox, grid, tiles = self.grid.get_affected_level_tiles((0,0,180,90), 2) eq_(grid, (2, 1)) eq_(bbox, (0.0, 0.0, 180.0, 90.0)) eq_(list(tiles), [(2, 1, 2), (3, 1, 2)]) class TestWGS83TileGridUL(object): def setup(self): self.grid = TileGrid(4326, bbox=(-180, -90, 180, 90), origin='ul') def test_resolution(self): assert_almost_equal(self.grid.resolution(0), 1.40625) assert_almost_equal(self.grid.resolution(1), 1.40625/2) def test_bbox(self): eq_(self.grid.bbox, (-180.0, -90.0, 180.0, 90.0)) def test_tile_bbox(self): eq_(self.grid.tile_bbox((0, 0, 0)), (-180.0, -270.0, 180.0, 90.0)) eq_(self.grid.tile_bbox((0, 0, 0), limit=True), (-180.0, -90.0, 180.0, 90.0)) eq_(self.grid.tile_bbox((0, 0, 1)), (-180.0, -90.0, 0.0, 90.0)) def test_tile(self): eq_(self.grid.tile(-170, -80, 0), (0, 0, 0)) eq_(self.grid.tile(-170, -80, 1), (0, 0, 1)) eq_(self.grid.tile(-170, -80, 2), (0, 1, 2)) def test_grid_size(self): eq_(self.grid.grid_sizes[0], (1, 1)) eq_(self.grid.grid_sizes[1], (2, 1)) eq_(self.grid.grid_sizes[2], (4, 2)) def test_affected_tiles(self): bbox, grid, tiles = self.grid.get_affected_tiles((-180,-90,180,90), (512,256)) eq_(bbox, (-180.0, -90.0, 180.0, 90.0)) eq_(grid, (2, 1)) eq_(list(tiles), [(0, 0, 1), (1, 0, 1)]) bbox, grid, tiles = self.grid.get_affected_tiles((-180,-90,0,90), (512, 512)) eq_(bbox, (-180.0, -90.0, 0.0, 90.0)) eq_(grid, (2, 2)) eq_(list(tiles), [(0, 0, 2), (1, 0, 2), (0, 1, 2), (1, 1, 2)]) def test_affected_level_tiles(self): bbox, grid, tiles = self.grid.get_affected_level_tiles((-180,-90,180,90), 1) eq_(grid, (2, 1)) eq_(bbox, (-180.0, -90.0, 180.0, 90.0)) eq_(list(tiles), [(0, 0, 1), (1, 0, 1)]) bbox, grid, tiles = self.grid.get_affected_level_tiles((0,0,180,90), 2) eq_(grid, (2, 1)) eq_(list(tiles), [(2, 0, 2), (3, 0, 2)]) eq_(bbox, (0.0, 0.0, 180.0, 90.0)) bbox, grid, tiles = self.grid.get_affected_level_tiles((0,-90,180,90), 2) eq_(grid, (2, 2)) eq_(list(tiles), [(2, 0, 2), (3, 0, 2), (2, 1, 2), (3, 1, 2)]) eq_(bbox, (0.0, -90.0, 180.0, 90.0)) class TestGKTileGrid(TileGridTest): def setup(self): self.grid = TileGrid(SRS(31467), bbox=(3250000, 5230000, 3930000, 6110000)) def test_bbox(self): assert self.grid.bbox == (3250000, 5230000, 3930000, 6110000) def test_resolution(self): res = self.grid.resolution(0) width = self.grid.bbox[2] - self.grid.bbox[0] height = self.grid.bbox[3] - self.grid.bbox[1] assert height == 880000.0 and width == 680000.0 assert res == 880000.0/256 def test_tile_bbox(self): tile_bbox = self.grid.tile_bbox((0, 0, 0)) assert tile_bbox == (3250000.0, 5230000.0, 4130000.0, 6110000.0) def test_tile(self): x, y = 3450000, 5890000 assert [self.grid.tile(x, y, level) for level in range(5)] == \ [(0, 0, 0), (0, 1, 1), (0, 3, 2), (1, 6, 3), (3, 12, 4)] def test_grids(self): for level, grid_size in [(0, (1, 1)), (1, (2, 2)), (2, (4, 4)), (3, (7, 8))]: yield self.check_grid, level, grid_size def test_closest_level(self): assert self.grid.closest_level(880000.0/256) == 0 assert self.grid.closest_level(600000.0/256) == 1 assert self.grid.closest_level(440000.0/256) == 1 assert self.grid.closest_level(420000.0/256) == 1 def test_adjacent_tile_bbox(self): t1 = self.grid.tile_bbox((0, 0, 1)) t2 = self.grid.tile_bbox((1, 0, 1)) t3 = self.grid.tile_bbox((0, 1, 1)) assert t1[1] == t2[1] assert t1[3] == t2[3] assert t1[2] == t2[0] assert t1[0] == t3[0] assert t1[3] == t3[1] class TestGKTileGridUL(TileGridTest): """ Custom grid with ul origin. """ def setup(self): self.grid = TileGrid(SRS(31467), bbox=(3300000, 5300000, 3900000, 6000000), origin='ul', res=[1500, 1000, 500, 300, 150, 100]) def test_bbox(self): assert self.grid.bbox == (3300000, 5300000, 3900000, 6000000) def test_tile_bbox(self): eq_(self.grid.tile_bbox((0, 0, 0)), (3300000.0, 5616000.0, 3684000.0, 6000000.0)) eq_(self.grid.tile_bbox((1, 0, 0)), (3684000.0, 5616000.0, 4068000.0, 6000000.0)) eq_(self.grid.tile_bbox((1, 1, 0)), (3684000.0, 5232000.0, 4068000.0, 5616000.0)) def test_tile(self): x, y = 3310000, 5990000 eq_(self.grid.tile(x, y, 0), (0, 0, 0)) eq_(self.grid.tile(x, y, 1), (0, 0, 1)) eq_(self.grid.tile(x, y, 2), (0, 0, 2)) x, y = 3890000, 5310000 eq_(self.grid.tile(x, y, 0), (1, 1, 0)) eq_(self.grid.tile(x, y, 1), (2, 2, 1)) eq_(self.grid.tile(x, y, 2), (4, 5, 2)) def test_grids(self): assert_grid_size(self.grid, 0, (2, 2)) assert_grid_size(self.grid, 1, (3, 3)) assert_grid_size(self.grid, 2, (5, 6)) def test_closest_level(self): assert self.grid.closest_level(1500) == 0 assert self.grid.closest_level(1000) == 1 assert self.grid.closest_level(900) == 1 assert self.grid.closest_level(600) == 2 def test_adjacent_tile_bbox(self): t1 = self.grid.tile_bbox((0, 0, 1)) t2 = self.grid.tile_bbox((1, 0, 1)) t3 = self.grid.tile_bbox((0, 1, 1)) assert t1[1] == t2[1] assert t1[3] == t2[3] assert t1[2] == t2[0] assert t1[0] == t3[0] assert t1[1] == t3[3] class TestClosestLevelTinyResFactor(object): def setup(self): self.grid = TileGrid(SRS(31467), bbox=[420000,30000,900000,350000], origin='ul', res=[4000,3750,3500,3250,3000,2750,2500,2250,2000,1750,1500,1250,1000,750,650,500,250,100,50,20,10,5,2.5,2,1.5,1,0.5], ) def test_closest_level(self): eq_(self.grid.closest_level(5000), 0) eq_(self.grid.closest_level(4000), 0) eq_(self.grid.closest_level(3750), 1) eq_(self.grid.closest_level(3500), 2) eq_(self.grid.closest_level(3250), 3) eq_(self.grid.closest_level(3000), 4) class TestOrigins(object): def test_basic(self): grid = tile_grid(4326, bbox=(-180, -90, 180, 90), origin='ll') assert grid.supports_access_with_origin('ll') assert not grid.supports_access_with_origin('ul') grid = tile_grid(4326, bbox=(-180, -90, 180, 90), origin='ul') assert not grid.supports_access_with_origin('ll') assert grid.supports_access_with_origin('ul') def test_basic_no_level_zero(self): grid = tile_grid(4326, bbox=(-180, -90, 180, 90), origin='ll', min_res=360/256/2) assert grid.supports_access_with_origin('ll') assert grid.supports_access_with_origin('ul') grid = tile_grid(4326, bbox=(-180, -90, 180, 90), origin='ul', min_res=360/256/2) assert grid.supports_access_with_origin('ll') assert grid.supports_access_with_origin('ul') def test_basic_mixed_name(self): grid = tile_grid(4326, bbox=(-180, -90, 180, 90), origin='ll') assert grid.supports_access_with_origin('sw') assert not grid.supports_access_with_origin('nw') grid = tile_grid(4326, bbox=(-180, -90, 180, 90), origin='ul') assert not grid.supports_access_with_origin('sw') assert grid.supports_access_with_origin('nw') def test_custom_with_match(self): # height is divisible by res*tile_size grid = tile_grid(4326, bbox=(0, 0, 1024, 1024), origin='ll', min_res=1) assert grid.supports_access_with_origin('ll') assert grid.supports_access_with_origin('ul') grid = tile_grid(4326, bbox=(0, 0, 1024, 1024), origin='ul', min_res=1) assert grid.supports_access_with_origin('ll') assert grid.supports_access_with_origin('ul') def test_custom_without_match(self): # height is not divisible by res*tile_size grid = tile_grid(4326, bbox=(0, 0, 1024, 1000), origin='ll', min_res=1) assert grid.supports_access_with_origin('ll') assert not grid.supports_access_with_origin('ul') grid = tile_grid(4326, bbox=(0, 0, 1024, 1000), origin='ul', min_res=1) assert not grid.supports_access_with_origin('ll') assert grid.supports_access_with_origin('ul') def test_custom_res_with_match(self): grid = tile_grid(4326, bbox=(0, 0, 1024, 1024), origin='ll', res=[1, 0.5, 0.25]) assert grid.supports_access_with_origin('ll') assert grid.supports_access_with_origin('ul') grid = tile_grid(4326, bbox=(0, 0, 1024, 1024), origin='ul', res=[1, 0.5, 0.25]) assert grid.supports_access_with_origin('ll') assert grid.supports_access_with_origin('ul') def test_custom_res_without_match(self): grid = tile_grid(4326, bbox=(0, 0, 1024, 1023), origin='ll', res=[1, 0.5, 0.25]) assert grid.supports_access_with_origin('ll') assert not grid.supports_access_with_origin('ul') grid = tile_grid(4326, bbox=(0, 0, 1024, 1023), origin='ul', res=[1, 0.5, 0.25]) assert not grid.supports_access_with_origin('ll') assert grid.supports_access_with_origin('ul') class TestFixedResolutionsTileGrid(TileGridTest): def setup(self): self.res = [1000.0, 500.0, 200.0, 100.0, 50.0, 20.0, 5.0] bbox = (3250000, 5230000, 3930000, 6110000) self.grid = TileGrid(SRS(31467), bbox=bbox, res=self.res) def test_resolution(self): for level, res in enumerate(self.res): assert res == self.grid.resolution(level) def test_closest_level(self): assert self.grid.closest_level(2000) == 0 assert self.grid.closest_level(1000) == 0 assert self.grid.closest_level(950) == 0 assert self.grid.closest_level(210) == 2 def test_affected_tiles(self): req_bbox = (3250000, 5230000, 3930000, 6110000) self.grid.max_shrink_factor = 10 bbox, grid_size, tiles = \ self.grid.get_affected_tiles(req_bbox, (256, 256)) assert bbox == (req_bbox[0], req_bbox[1], req_bbox[0]+1000*256*3, req_bbox[1]+1000*256*4) assert grid_size == (3, 4) tiles = list(tiles) assert tiles == [(0, 3, 0), (1, 3, 0), (2, 3, 0), (0, 2, 0), (1, 2, 0), (2, 2, 0), (0, 1, 0), (1, 1, 0), (2, 1, 0), (0, 0, 0), (1, 0, 0), (2, 0, 0), ] def test_affected_tiles_2(self): req_bbox = (3250000, 5230000, 3930000, 6110000) self.grid.max_shrink_factor = 2.0 try: bbox, grid_size, tiles = \ self.grid.get_affected_tiles(req_bbox, (256, 256)) except NoTiles: pass else: assert False, 'got no exception' def test_grid(self): for level, grid_size in [(0, (3, 4)), (1, (6, 7)), (2, (14, 18))]: yield self.check_grid, level, grid_size def test_tile_bbox(self): tile_bbox = self.grid.tile_bbox((0, 0, 0)) # w: 1000x256 assert tile_bbox == (3250000.0, 5230000.0, 3506000.0, 5486000.0) tile_bbox = self.grid.tile_bbox((0, 0, 1)) # w: 500x256 assert tile_bbox == (3250000.0, 5230000.0, 3378000.0, 5358000.0) tile_bbox = self.grid.tile_bbox((0, 0, 2)) # w: 200x256 assert tile_bbox == (3250000.0, 5230000.0, 3301200.0, 5281200.0) class TestGeodeticTileGrid(TileGridTest): def setup(self): self.grid = TileGrid(is_geodetic=True, ) def test_auto_resolution(self): grid = TileGrid(is_geodetic=True, bbox=(-10, 30, 10, 40), tile_size=(20, 20)) tile_bbox = grid.tile_bbox((0, 0, 0)) assert tile_bbox == (-10, 30, 10, 50) assert grid.resolution(0) == 1.0 def test_grid(self): for level, grid_size in [(0, (1, 1)), (1, (2, 1)), (2, (4, 2))]: yield self.check_grid, level, grid_size def test_adjacent_tile_bbox(self): grid = TileGrid(is_geodetic=True, bbox=(-10, 30, 10, 40), tile_size=(20, 20)) t1 = grid.tile_bbox((0, 0, 2)) t2 = grid.tile_bbox((1, 0, 2)) t3 = grid.tile_bbox((0, 1, 2)) assert t1[1] == t2[1] assert t1[3] == t2[3] assert t1[2] == t2[0] assert t1[0] == t3[0] assert t1[2] == t3[2] assert t1[3] == t3[1] def test_w_resolution(self): res = [1, 0.5, 0.2] grid = TileGrid(is_geodetic=True, bbox=(-10, 30, 10, 40), tile_size=(20, 20), res=res) assert grid.grid_sizes[0] == (1, 1) assert grid.grid_sizes[1] == (2, 1) assert grid.grid_sizes[2] == (5, 3) def test_tile(self): assert self.grid.tile(-180, -90, 0) == (0, 0, 0) assert self.grid.tile(-180, -90, 1) == (0, 0, 1) assert self.grid.tile(-180, -90, 2) == (0, 0, 2) assert self.grid.tile(180-0.001, 90-0.001, 0) == (0, 0, 0) assert self.grid.tile(10, 50, 1) == (1, 0, 1) def test_affected_tiles(self): bbox, grid_size, tiles = \ self.grid.get_affected_tiles((-45,-45,45,45), (512,512)) assert self.grid.grid_sizes[3] == (8, 4) assert bbox == (-45.0, -45.0, 45.0, 45.0) assert grid_size == (2, 2) tiles = list(tiles) assert tiles == [(3, 2, 3), (4, 2, 3), (3, 1, 3), (4, 1, 3)] class TestTileGrid(object): def test_tile_out_of_grid_bounds(self): grid = TileGrid(is_geodetic=True) eq_(grid.tile(-180.01, 50, 1), (-1, 0, 1)) def test_affected_tiles_out_of_grid_bounds(self): grid = TileGrid() #bbox from open layers req_bbox = (-30056262.509599999, -10018754.170400001, -20037508.339999996, -0.00080000050365924835) bbox, grid_size, tiles = \ grid.get_affected_tiles(req_bbox, (256, 256)) assert_almost_equal_bbox(bbox, req_bbox) eq_(grid_size, (1, 1)) tiles = list(tiles) eq_(tiles, [None]) def test_broken_bbox(self): grid = TileGrid() # broken request from "ArcGIS Client Using WinInet" req_bbox = (-10000855.0573254,2847125.18913603,-9329367.42767611,4239924.78564583) try: grid.get_affected_tiles(req_bbox, (256, 256), req_srs=SRS(31467)) except TransformationError: pass else: assert False, 'Expected TransformationError' class TestTileGridThreshold(object): def test_lower_bound(self): # thresholds near the next lower res value grid = TileGrid(res=[1000, 500, 250, 100, 50], threshold_res=[300, 110]) grid.stretch_factor = 1.1 eq_(grid.closest_level(1100), 0) # regular transition (w/stretchfactor) eq_(grid.closest_level(950), 0) eq_(grid.closest_level(800), 1) eq_(grid.closest_level(500), 1) # transition at threshold eq_(grid.closest_level(301), 1) eq_(grid.closest_level(300), 2) eq_(grid.closest_level(250), 2) # transition at threshold eq_(grid.closest_level(111), 2) eq_(grid.closest_level(110), 3) eq_(grid.closest_level(100), 3) # regular transition (w/stretchfactor) eq_(grid.closest_level(92), 3) eq_(grid.closest_level(90), 4) def test_upper_bound(self): # thresholds near the next upper res value (within threshold) grid = TileGrid(res=[1000, 500, 250, 100, 50], threshold_res=[495, 240]) grid.stretch_factor = 1.1 eq_(grid.closest_level(1100), 0) # regular transition (w/stretchfactor) eq_(grid.closest_level(950), 0) eq_(grid.closest_level(800), 1) eq_(grid.closest_level(500), 1) # transition at threshold eq_(grid.closest_level(496), 1) eq_(grid.closest_level(495), 2) eq_(grid.closest_level(250), 2) # transition at threshold (within strechfactor) eq_(grid.closest_level(241), 2) eq_(grid.closest_level(240), 3) eq_(grid.closest_level(100), 3) # regular transition (w/stretchfactor) eq_(grid.closest_level(92), 3) eq_(grid.closest_level(90), 4) def test_above_first_res(self): grid = TileGrid(res=[1000, 500, 250, 100, 50], threshold_res=[1100, 750]) grid.stretch_factor = 1.1 eq_(grid.closest_level(1200), 0) eq_(grid.closest_level(1100), 0) eq_(grid.closest_level(1000), 0) eq_(grid.closest_level(800), 0) eq_(grid.closest_level(750.1), 0) eq_(grid.closest_level(750), 1) class TestCreateTileList(object): def test(self): xs = list(range(-1, 2)) ys = list(range(-2, 3)) grid_size = (1, 2) tiles = list(_create_tile_list(xs, ys, 3, grid_size)) expected = [None, None, None, None, None, None, None, (0, 0, 3), None, None, (0, 1, 3), None, None, None, None] eq_(expected, tiles) def _create_tile_list(self, xs, ys, level, grid_size): x_limit = grid_size[0] y_limit = grid_size[1] for y in ys: for x in xs: if x < 0 or y < 0 or x >= x_limit or y >= y_limit: yield None else: yield x, y, level class TestBBOXIntersects(object): def test_no_intersect(self): b1 = (0, 0, 10, 10) b2 = (20, 20, 30, 30) assert not bbox_intersects(b1, b2) assert not bbox_intersects(b2, b1) def test_no_intersect_only_vertical(self): b1 = (0, 0, 10, 10) b2 = (20, 0, 30, 10) assert not bbox_intersects(b1, b2) assert not bbox_intersects(b2, b1) def test_no_intersect_touch_point(self): b1 = (0, 0, 10, 10) b2 = (10, 10, 20, 20) assert not bbox_intersects(b1, b2) assert not bbox_intersects(b2, b1) def test_no_intersect_touch_side(self): b1 = (0, 0, 10, 10) b2 = (0, 10, 10, 20) assert not bbox_intersects(b1, b2) assert not bbox_intersects(b2, b1) def test_full_contains(self): b1 = (0, 0, 10, 10) b2 = (2, 2, 8, 8) assert bbox_intersects(b1, b2) assert bbox_intersects(b2, b1) def test_overlap(self): b1 = (0, 0, 10, 10) b2 = (-5, -5, 5, 5) assert bbox_intersects(b1, b2) assert bbox_intersects(b2, b1) class TestBBOXContains(object): def test_no_intersect(self): b1 = (0, 0, 10, 10) b2 = (20, 20, 30, 30) assert not bbox_contains(b1, b2) assert not bbox_contains(b2, b1) def test_no_intersect_only_vertical(self): b1 = (0, 0, 10, 10) b2 = (20, 0, 30, 10) assert not bbox_contains(b1, b2) assert not bbox_contains(b2, b1) def test_no_intersect_touch_point(self): b1 = (0, 0, 10, 10) b2 = (10, 10, 20, 20) assert not bbox_contains(b1, b2) assert not bbox_contains(b2, b1) def test_no_intersect_touch_side(self): b1 = (0, 0, 10, 10) b2 = (0, 10, 10, 20) assert not bbox_contains(b1, b2) assert not bbox_contains(b2, b1) def test_full_contains(self): b1 = (0, 0, 10, 10) b2 = (2, 2, 8, 8) assert bbox_contains(b1, b2) assert not bbox_contains(b2, b1) def test_contains_touch(self): b1 = (0, 0, 10, 10) b2 = (0, 0, 8, 8) assert bbox_contains(b1, b2) assert not bbox_contains(b2, b1) def test_overlap(self): b1 = (0, 0, 10, 10) b2 = (-5, -5, 5, 5) assert not bbox_contains(b1, b2) assert not bbox_contains(b2, b1) def assert_almost_equal_bbox(bbox1, bbox2, places=2): for coord1, coord2 in zip(bbox1, bbox2): assert_almost_equal(coord1, coord2, places, msg='%s != %s' % (bbox1, bbox2)) class TestResolutionRange(object): def test_meter(self): res_range = ResolutionRange(1000, 10) assert not res_range.contains([0, 0, 100000, 100000], (10, 10), SRS(900913)) assert not res_range.contains([0, 0, 100000, 100000], (99, 99), SRS(900913)) # min is exclusive but there is a delta assert res_range.contains([0, 0, 100000, 100000], (100, 100), SRS(900913)) assert res_range.contains([0, 0, 100000, 100000], (1000, 1000), SRS(900913)) # max is inclusive assert res_range.contains([0, 0, 100000, 100000], (10000, 10000), SRS(900913)) assert not res_range.contains([0, 0, 100000, 100000], (10001, 10001), SRS(900913)) def test_deg(self): res_range = ResolutionRange(100000, 1000) assert not res_range.contains([0, 0, 10, 10], (10, 10), SRS(4326)) assert not res_range.contains([0, 0, 10, 10], (11, 11), SRS(4326)) assert res_range.contains([0, 0, 10, 10], (12, 12), SRS(4326)) assert res_range.contains([0, 0, 10, 10], (100, 100), SRS(4326)) assert res_range.contains([0, 0, 10, 10], (1000, 1000), SRS(4326)) assert res_range.contains([0, 0, 10, 10], (1100, 1100), SRS(4326)) assert not res_range.contains([0, 0, 10, 10], (1200, 1200), SRS(4326)) def test_no_min(self): res_range = ResolutionRange(None, 10) assert res_range.contains([0, 0, 100000, 100000], (1, 1), SRS(900913)) assert res_range.contains([0, 0, 100000, 100000], (10, 10), SRS(900913)) assert res_range.contains([0, 0, 100000, 100000], (99, 99), SRS(900913)) assert res_range.contains([0, 0, 100000, 100000], (100, 100), SRS(900913)) assert res_range.contains([0, 0, 100000, 100000], (1000, 1000), SRS(900913)) # max is inclusive assert res_range.contains([0, 0, 100000, 100000], (10000, 10000), SRS(900913)) assert not res_range.contains([0, 0, 100000, 100000], (10001, 10001), SRS(900913)) def test_no_max(self): res_range = ResolutionRange(1000, None) assert not res_range.contains([0, 0, 100000, 100000], (10, 10), SRS(900913)) assert not res_range.contains([0, 0, 100000, 100000], (99, 99), SRS(900913)) # min is exclusive but there is a delta assert res_range.contains([0, 0, 100000, 100000], (100, 100), SRS(900913)) assert res_range.contains([0, 0, 100000, 100000], (1000, 1000), SRS(900913)) assert res_range.contains([0, 0, 100000, 100000], (10000, 10000), SRS(900913)) assert res_range.contains([0, 0, 100000, 100000], (10001, 10001), SRS(900913)) assert res_range.contains([0, 0, 100000, 100000], (1000000, 100000), SRS(900913)) def test_none(self): res_range = resolution_range(None, None) assert res_range == None def test_from_scale(self): res_range = resolution_range(max_scale=1e6, min_scale=1e3) assert_almost_equal(res_range.min_res, 280) assert_almost_equal(res_range.max_res, 0.28) @raises(ValueError) def check_invalid_combination(self, min_res, max_res, max_scale, min_scale): resolution_range(min_res, max_res, max_scale, min_scale) def test_invalid_combinations(self): yield self.check_invalid_combination, 10, None, 10, None yield self.check_invalid_combination, 10, 20, 10, None yield self.check_invalid_combination, 10, None, 10, 20 yield self.check_invalid_combination, 10, 20, 10, 20 @raises(AssertionError) def test_wrong_order_res(self): resolution_range(min_res=10, max_res=100) @raises(AssertionError) def test_wrong_order_scale(self): resolution_range(min_scale=100, max_scale=10) def test_merge_resolutions(self): res_range = merge_resolution_range( ResolutionRange(None, 10), ResolutionRange(1000, None)) eq_(res_range, None) res_range = merge_resolution_range( ResolutionRange(10000, 10), ResolutionRange(1000, None)) eq_(res_range.min_res, 10000) eq_(res_range.max_res, None) res_range = merge_resolution_range( ResolutionRange(10000, 10), ResolutionRange(1000, 1)) eq_(res_range.min_res, 10000) eq_(res_range.max_res, 1) res_range = merge_resolution_range( ResolutionRange(10000, 10), ResolutionRange(None, None)) eq_(res_range, None) res_range = merge_resolution_range( None, ResolutionRange(None, None)) eq_(res_range, None) res_range = merge_resolution_range( ResolutionRange(10000, 10), None) eq_(res_range, None) def test_eq(self): assert resolution_range(None, None) == resolution_range(None, None) assert resolution_range(None, 100) == resolution_range(None, 100.0) assert resolution_range(None, 100) != resolution_range(None, 100.1) assert resolution_range(1000, 100) == resolution_range(1000, 100) assert resolution_range(1000, 100) == resolution_range(1000.0, 100) assert resolution_range(1000, 100) != resolution_range(1000.1, 100) class TestGridSubset(object): def test_different_srs(self): g1 = tile_grid(SRS(4326)) g2 = tile_grid(SRS(3857)) assert not g1.is_subset_of(g2) def test_same_grid(self): g1 = tile_grid(SRS(900913)) assert g1.is_subset_of(g1) def test_similar_srs(self): g1 = tile_grid(SRS(900913)) g2 = tile_grid(SRS(3857)) assert g1.is_subset_of(g2) def test_less_levels(self): g1 = tile_grid(SRS(3857), num_levels=10) g2 = tile_grid(SRS(3857)) assert g1.is_subset_of(g2) def test_more_levels(self): g1 = tile_grid(SRS(3857)) g2 = tile_grid(SRS(3857), num_levels=10) assert not g1.is_subset_of(g2) def test_res_subset(self): g1 = tile_grid(SRS(3857), res=[50000, 10000, 100, 1]) g2 = tile_grid(SRS(3857), res=[100000, 50000, 10000, 1000, 100, 10, 1, 0.5]) assert g1.tile_bbox((0, 0, 0)) != g2.tile_bbox((0, 0, 0)) assert g1.is_subset_of(g2) g1 = tile_grid(SRS(3857), bbox=[0, 0, 20037508.342789244, 20037508.342789244], min_res=78271.51696402048, num_levels=18) g2 = tile_grid(SRS(3857), origin='nw') assert g1.is_subset_of(g2) def test_subbbox(self): g2 = tile_grid(SRS(4326)) g1 = tile_grid(SRS(4326), num_levels=10, min_res=g2.resolutions[3], bbox=(0, 0, 180, 90)) assert g1.is_subset_of(g2) def test_incompatible_subbbox(self): g2 = tile_grid(SRS(4326)) g1 = tile_grid(SRS(4326), min_res=g2.resolutions[3], num_levels=10, bbox=(-10, 0, 180, 90)) assert not g1.is_subset_of(g2) def test_tile_size(self): g1 = tile_grid(SRS(4326), tile_size=(128, 128)) g2 = tile_grid(SRS(4326)) assert not g1.is_subset_of(g2) def test_non_matching_bboxfor_origins(self): g1 = tile_grid(SRS(21781), bbox=[420000, 30000, 900000, 360000], res=[250], origin='nw') g2 = tile_grid(SRS(21781), bbox=[420000, 30000, 900000, 360000], res=[250], origin='sw') assert not g1.is_subset_of(g2) def test_no_tile_errors(self): # g1 is not a subset, check that we don't get any NoTile errors g1 = tile_grid(SRS(3857), res=[100000, 50000, 10000, 1000, 100, 10, 1, 0.5]) g2 = tile_grid(SRS(3857), res=[100, 1]) assert not g1.is_subset_of(g2) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_image.py����������������������������������������������������0000664�0000000�0000000�00000100056�13204544724�0021770�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -:- encoding: utf8 -:- # This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os from io import BytesIO from mapproxy.compat.image import Image, ImageDraw from mapproxy.image import ( ImageSource, BlankImageSource, ReadBufWrapper, is_single_color_image, peek_image_format, _make_transparent as make_transparent, SubImageSource, img_has_transparency, quantize, ) from mapproxy.image.merge import merge_images, BandMerger from mapproxy.image.opts import ImageOptions from mapproxy.image.tile import TileMerger, TileSplitter from mapproxy.image.transform import ImageTransformer, transform_meshes from mapproxy.test.image import is_png, is_jpeg, is_tiff, create_tmp_image_file, check_format, create_debug_img, create_image from mapproxy.srs import SRS from nose.tools import eq_, assert_almost_equal from mapproxy.test.image import assert_img_colors_eq from nose.plugins.skip import SkipTest PNG_FORMAT = ImageOptions(format='image/png') JPEG_FORMAT = ImageOptions(format='image/jpeg') TIFF_FORMAT = ImageOptions(format='image/tiff') class TestImageSource(object): def setup(self): self.tmp_filename = create_tmp_image_file((100, 100)) def teardown(self): os.remove(self.tmp_filename) def test_from_filename(self): ir = ImageSource(self.tmp_filename, PNG_FORMAT) assert is_png(ir.as_buffer()) assert ir.as_image().size == (100, 100) def test_from_file(self): with open(self.tmp_filename, 'rb') as tmp_file: ir = ImageSource(tmp_file, 'png') assert ir.as_buffer() == tmp_file assert ir.as_image().size == (100, 100) def test_from_image(self): img = Image.new('RGBA', (100, 100)) ir = ImageSource(img, (100, 100), PNG_FORMAT) assert ir.as_image() == img assert is_png(ir.as_buffer()) def test_from_non_seekable_file(self): with open(self.tmp_filename, 'rb') as tmp_file: data = tmp_file.read() class FileLikeDummy(object): # "file" without seek, like urlopen response def read(self): return data ir = ImageSource(FileLikeDummy(), 'png') assert ir.as_buffer(seekable=True).read() == data assert ir.as_image().size == (100, 100) assert ir.as_buffer().read() == data def test_output_formats(self): img = Image.new('RGB', (100, 100)) for format in ['png', 'gif', 'tiff', 'jpeg', 'GeoTIFF', 'bmp']: ir = ImageSource(img, (100, 100), image_opts=ImageOptions(format=format)) yield check_format, ir.as_buffer(), format def test_converted_output(self): ir = ImageSource(self.tmp_filename, (100, 100), PNG_FORMAT) assert is_png(ir.as_buffer()) assert is_jpeg(ir.as_buffer(JPEG_FORMAT)) assert is_jpeg(ir.as_buffer()) assert is_tiff(ir.as_buffer(TIFF_FORMAT)) assert is_tiff(ir.as_buffer()) def test_output_formats_greyscale_png(self): img = Image.new('L', (100, 100)) ir = ImageSource(img, image_opts=PNG_FORMAT) img = Image.open(ir.as_buffer(ImageOptions(colors=256, transparent=True, format='image/png'))) assert img.mode == 'P' assert img.getpixel((0, 0)) == 255 def test_output_formats_greyscale_alpha_png(self): img = Image.new('LA', (100, 100)) ir = ImageSource(img, image_opts=PNG_FORMAT) img = Image.open(ir.as_buffer(ImageOptions(colors=256, transparent=True, format='image/png'))) assert img.mode == 'LA' assert img.getpixel((0, 0)) == (0, 0) def test_output_formats_png8(self): img = Image.new('RGBA', (100, 100)) ir = ImageSource(img, image_opts=PNG_FORMAT) img = Image.open(ir.as_buffer(ImageOptions(colors=256, transparent=True, format='image/png'))) assert img.mode == 'P' assert img.getpixel((0, 0)) == 255 def test_output_formats_png24(self): img = Image.new('RGBA', (100, 100)) image_opts = PNG_FORMAT.copy() image_opts.colors = 0 # TODO image_opts ir = ImageSource(img, image_opts=image_opts) img = Image.open(ir.as_buffer()) eq_(img.mode, 'RGBA') assert img.getpixel((0, 0)) == (0, 0, 0, 0) def test_save_with_unsupported_transparency(self): # check if encoding of non-RGB image with tuple as transparency # works. workaround for Pillow #2633 img = Image.new('P', (100, 100)) img.info['transparency'] = (0, 0, 0) image_opts = PNG_FORMAT.copy() ir = ImageSource(img, image_opts=image_opts) img = Image.open(ir.as_buffer()) eq_(img.mode, 'P') class TestSubImageSource(object): def test_full(self): sub_img = create_image((100, 100), color=[100, 120, 130, 140]) img = SubImageSource(sub_img, size=(100, 100), offset=(0, 0), image_opts=ImageOptions()).as_image() eq_(img.getcolors(), [(100*100, (100, 120, 130, 140))]) def test_larger(self): sub_img = create_image((150, 150), color=[100, 120, 130, 140]) img = SubImageSource(sub_img, size=(100, 100), offset=(0, 0), image_opts=ImageOptions()).as_image() eq_(img.getcolors(), [(100*100, (100, 120, 130, 140))]) def test_negative_offset(self): sub_img = create_image((150, 150), color=[100, 120, 130, 140]) img = SubImageSource(sub_img, size=(100, 100), offset=(-50, 0), image_opts=ImageOptions()).as_image() eq_(img.getcolors(), [(100*100, (100, 120, 130, 140))]) def test_overlap_right(self): sub_img = create_image((50, 50), color=[100, 120, 130, 140]) img = SubImageSource(sub_img, size=(100, 100), offset=(75, 25), image_opts=ImageOptions(transparent=True)).as_image() eq_(sorted(img.getcolors()), [(25*50, (100, 120, 130, 140)), (100*100-25*50, (255, 255, 255, 0))]) def test_outside(self): sub_img = create_image((50, 50), color=[100, 120, 130, 140]) img = SubImageSource(sub_img, size=(100, 100), offset=(200, 0), image_opts=ImageOptions(transparent=True)).as_image() eq_(img.getcolors(), [(100*100, (255, 255, 255, 0))]) class ROnly(object): def __init__(self): self.data = [b'Hello World!'] def read(self): if self.data: return self.data.pop() return b'' def __iter__(self): it = iter(self.data) self.data = [] return it class TestReadBufWrapper(object): def setup(self): rbuf = ROnly() self.rbuf_wrapper = ReadBufWrapper(rbuf) def test_read(self): assert self.rbuf_wrapper.read() == b'Hello World!' self.rbuf_wrapper.seek(0) eq_(self.rbuf_wrapper.read(), b'') def test_seek_read(self): self.rbuf_wrapper.seek(0) assert self.rbuf_wrapper.read() == b'Hello World!' self.rbuf_wrapper.seek(0) assert self.rbuf_wrapper.read() == b'Hello World!' def test_iter(self): data = list(self.rbuf_wrapper) eq_(data, [b'Hello World!']) self.rbuf_wrapper.seek(0) data = list(self.rbuf_wrapper) eq_(data, []) def test_seek_iter(self): self.rbuf_wrapper.seek(0) data = list(self.rbuf_wrapper) eq_(data, [b'Hello World!']) self.rbuf_wrapper.seek(0) data = list(self.rbuf_wrapper) eq_(data, [b'Hello World!']) def test_hasattr(self): assert hasattr(self.rbuf_wrapper, 'seek') assert hasattr(self.rbuf_wrapper, 'readline') class TestMergeAll(object): def setup(self): self.cleanup_tiles = [] def test_full_merge(self): self.cleanup_tiles = [create_tmp_image_file((100, 100)) for _ in range(9)] self.tiles = [ImageSource(tile) for tile in self.cleanup_tiles] m = TileMerger(tile_grid=(3, 3), tile_size=(100, 100)) img_opts = ImageOptions() result = m.merge(self.tiles, img_opts) img = result.as_image() eq_(img.size, (300, 300)) def test_one(self): self.cleanup_tiles = [create_tmp_image_file((100, 100))] self.tiles = [ImageSource(self.cleanup_tiles[0])] m = TileMerger(tile_grid=(1, 1), tile_size=(100, 100)) img_opts = ImageOptions(transparent=True) result = m.merge(self.tiles, img_opts) img = result.as_image() eq_(img.size, (100, 100)) eq_(img.mode, 'RGBA') def test_missing_tiles(self): self.cleanup_tiles = [create_tmp_image_file((100, 100))] self.tiles = [ImageSource(self.cleanup_tiles[0])] self.tiles.extend([None]*8) m = TileMerger(tile_grid=(3, 3), tile_size=(100, 100)) img_opts = ImageOptions() result = m.merge(self.tiles, img_opts) img = result.as_image() eq_(img.size, (300, 300)) eq_(img.getcolors(), [(80000, (255, 255, 255)), (10000, (0, 0, 0)), ]) def test_invalid_tile(self): self.cleanup_tiles = [create_tmp_image_file((100, 100)) for _ in range(9)] self.tiles = [ImageSource(tile) for tile in self.cleanup_tiles] invalid_tile = self.tiles[0].source with open(invalid_tile, 'wb') as tmp: tmp.write(b'invalid') m = TileMerger(tile_grid=(3, 3), tile_size=(100, 100)) img_opts = ImageOptions(bgcolor=(200, 0, 50)) result = m.merge(self.tiles, img_opts) img = result.as_image() eq_(img.size, (300, 300)) eq_(img.getcolors(), [(10000, (200, 0, 50)), (80000, (0, 0, 0))]) assert not os.path.isfile(invalid_tile) def test_none_merge(self): tiles = [None] m = TileMerger(tile_grid=(1, 1), tile_size=(100, 100)) img_opts = ImageOptions(mode='RGBA', bgcolor=(200, 100, 30, 40)) result = m.merge(tiles, img_opts) img = result.as_image() eq_(img.size, (100, 100)) eq_(img.getcolors(), [(100*100, (200, 100, 30, 40))]) def teardown(self): for tile_fname in self.cleanup_tiles: if tile_fname and os.path.isfile(tile_fname): os.remove(tile_fname) class TestGetCrop(object): def setup(self): self.tmp_file = create_tmp_image_file((100, 100), two_colored=True) self.img = ImageSource(self.tmp_file, image_opts=ImageOptions(format='image/png'), size=(100, 100)) def teardown(self): if os.path.exists(self.tmp_file): os.remove(self.tmp_file) def test_perfect_match(self): bbox = (-10, -5, 30, 35) transformer = ImageTransformer(SRS(4326), SRS(4326)) result = transformer.transform(self.img, bbox, (100, 100), bbox, image_opts=None) assert self.img == result def test_simple_resize_nearest(self): bbox = (-10, -5, 30, 35) transformer = ImageTransformer(SRS(4326), SRS(4326)) result = transformer.transform(self.img, bbox, (200, 200), bbox, image_opts=ImageOptions(resampling='nearest')) img = result.as_image() eq_(img.size, (200, 200)) eq_(len(img.getcolors()), 2) def test_simple_resize_bilinear(self): bbox = (-10, -5, 30, 35) transformer = ImageTransformer(SRS(4326), SRS(4326)) result = transformer.transform(self.img, bbox, (200, 200), bbox, image_opts=ImageOptions(resampling='bilinear')) img = result.as_image() eq_(img.size, (200, 200)) # some shades of grey with bilinear assert len(img.getcolors()) >= 4 class TestLayerMerge(object): def test_opacity_merge(self): img1 = ImageSource(Image.new('RGB', (10, 10), (255, 0, 255))) img2 = ImageSource(Image.new('RGB', (10, 10), (0, 255, 255)), image_opts=ImageOptions(opacity=0.5)) result = merge_images([img1, img2], ImageOptions(transparent=False)) img = result.as_image() eq_(img.getpixel((0, 0)), (127, 127, 255)) def test_opacity_merge_mixed_modes(self): img1 = ImageSource(Image.new('RGBA', (10, 10), (255, 0, 255, 255))) img2 = ImageSource(Image.new('RGB', (10, 10), (0, 255, 255)).convert('P'), image_opts=ImageOptions(opacity=0.5)) result = merge_images([img1, img2], ImageOptions(transparent=True)) img = result.as_image() assert_img_colors_eq(img, [ (10*10, (127, 127, 255, 255)), ]) def test_merge_L(self): img1 = ImageSource(Image.new('RGBA', (10, 10), (255, 0, 255, 255))) img2 = ImageSource(Image.new('L', (10, 10), 100)) # img2 overlays img1 result = merge_images([img1, img2], ImageOptions(transparent=True)) img = result.as_image() assert_img_colors_eq(img, [ (10*10, (100, 100, 100, 255)), ]) def test_paletted_merge(self): if not hasattr(Image, 'FASTOCTREE'): raise SkipTest() # generate RGBA images with a transparent rectangle in the lower right img1 = ImageSource(Image.new('RGBA', (50, 50), (0, 255, 0, 255))).as_image() draw = ImageDraw.Draw(img1) draw.rectangle((25, 25, 49, 49), fill=(0, 0, 0, 0)) paletted_img = quantize(img1, alpha=True) assert img_has_transparency(paletted_img) assert paletted_img.mode == 'P' rgba_img = Image.new('RGBA', (50, 50), (255, 0, 0, 255)) draw = ImageDraw.Draw(rgba_img) draw.rectangle((25, 25, 49, 49), fill=(0, 0, 0, 0)) img1 = ImageSource(paletted_img) img2 = ImageSource(rgba_img) # generate base image and merge the others above img3 = ImageSource(Image.new('RGBA', (50, 50), (0, 0, 255, 255))) result = merge_images([img3, img1, img2], ImageOptions(transparent=True)) img = result.as_image() assert img.mode == 'RGBA' eq_(img.getpixel((49, 49)), (0, 0, 255, 255)) eq_(img.getpixel((0, 0)), (255, 0, 0, 255)) def test_solid_merge(self): img1 = ImageSource(Image.new('RGB', (10, 10), (255, 0, 255))) img2 = ImageSource(Image.new('RGB', (10, 10), (0, 255, 255))) result = merge_images([img1, img2], ImageOptions(transparent=False)) img = result.as_image() eq_(img.getpixel((0, 0)), (0, 255, 255)) def test_merge_rgb_with_transp(self): img1 = ImageSource(Image.new('RGB', (10, 10), (255, 0, 255))) raw = Image.new('RGB', (10, 10), (0, 255, 255)) raw.info = {'transparency': (0, 255, 255)} # make full transparent img2 = ImageSource(raw) result = merge_images([img1, img2], ImageOptions(transparent=False)) img = result.as_image() eq_(img.getpixel((0, 0)), (255, 0, 255)) class TestLayerCompositeMerge(object): def test_composite_merge(self): # http://stackoverflow.com/questions/3374878 if not hasattr(Image, 'alpha_composite'): raise SkipTest() img1 = Image.new('RGBA', size=(100, 100), color=(255, 0, 0, 255)) draw = ImageDraw.Draw(img1) draw.rectangle((33, 0, 66, 100), fill=(255, 0, 0, 128)) draw.rectangle((67, 0, 100, 100), fill=(255, 0, 0, 0)) img1 = ImageSource(img1) img2 = Image.new('RGBA', size =(100, 100), color=(0, 255, 0, 255)) draw = ImageDraw.Draw(img2) draw.rectangle((0, 33, 100, 66), fill=(0, 255, 0, 128)) draw.rectangle((0, 67, 100, 100), fill=(0, 255, 0, 0)) img2 = ImageSource(img2) result = merge_images([img2, img1], ImageOptions(transparent=True)) img = result.as_image() eq_(img.mode, 'RGBA') assert_img_colors_eq(img, [ (1089, (0, 255, 0, 255)), (1089, (255, 255, 255, 0)), (1122, (0, 255, 0, 128)), (1122, (128, 126, 0, 255)), (1122, (255, 0, 0, 128)), (1156, (170, 84, 0, 191)), (3300, (255, 0, 0, 255))]) def test_composite_merge_opacity(self): if not hasattr(Image, 'alpha_composite'): raise SkipTest() bg = Image.new('RGBA', size=(100, 100), color=(255, 0, 255, 255)) bg = ImageSource(bg) fg = Image.new('RGBA', size =(100, 100), color=(0, 0, 0, 0)) draw = ImageDraw.Draw(fg) draw.rectangle((10, 10, 89, 89), fill=(0, 255, 255, 255)) fg = ImageSource(fg, image_opts=ImageOptions(opacity=0.5)) result = merge_images([bg, fg], ImageOptions(transparent=True)) img = result.as_image() eq_(img.mode, 'RGBA') assert_img_colors_eq(img, [ (3600, (255, 0, 255, 255)), (6400, (128, 127, 255, 255))]) class TestTransform(object): def setup(self): self.src_img = ImageSource(create_debug_img((200, 200), transparent=False)) self.src_srs = SRS(31467) self.dst_size = (100, 150) self.dst_srs = SRS(4326) self.dst_bbox = (0.2, 45.1, 8.3, 53.2) self.src_bbox = self.dst_srs.transform_bbox_to(self.src_srs, self.dst_bbox) def test_transform(self): transformer = ImageTransformer(self.src_srs, self.dst_srs) result = transformer.transform(self.src_img, self.src_bbox, self.dst_size, self.dst_bbox, image_opts=ImageOptions(resampling='nearest')) assert isinstance(result, ImageSource) assert result.as_image() != self.src_img.as_image() assert result.size == (100, 150) def _test_compare_max_px_err(self): """ Create transformations with different div values. """ for err in [0.2, 0.5, 1, 2, 4, 6, 8, 12, 16]: transformer = ImageTransformer(self.src_srs, self.dst_srs, max_px_err=err) result = transformer.transform(self.src_img, self.src_bbox, self.dst_size, self.dst_bbox, image_opts=ImageOptions(resampling='nearest')) result.as_image().save('/tmp/transform-%03d.png' % (err*10,)) class TestMesh(object): def test_mesh_utm(self): meshes = transform_meshes( src_size=(1335, 1531), src_bbox=(3.65, 39.84, 17.00, 55.15), src_srs=SRS(4326), dst_size=(853, 1683), dst_bbox=(158512, 4428236, 1012321, 6111268), dst_srs=SRS(25832), ) eq_(len(meshes), 40) def test_mesh_none(self): meshes = transform_meshes( src_size=(1000, 1500), src_bbox=(0, 0, 10, 15), src_srs=SRS(4326), dst_size=(1000, 1500), dst_bbox=(0, 0, 10, 15), dst_srs=SRS(4326), ) eq_(meshes, [((0, 0, 1000, 1500), [0.0, 0.0, 0.0, 1500.0, 1000.0, 1500.0, 1000.0, 0.0])]) eq_(len(meshes), 1) def test_mesh(self): # low map scale -> more meshes # print(SRS(4326).transform_bbox_to(SRS(3857), (5, 50, 10, 55))) meshes = transform_meshes( src_size=(1000, 2000), src_bbox=(556597, 6446275, 1113194, 7361866), src_srs=SRS(3857), dst_size=(1000, 1000), dst_bbox=(5, 50, 10, 55), dst_srs=SRS(4326), ) eq_(len(meshes), 16) # large map scale -> one meshes # print(SRS(4326).transform_bbox_to(SRS(3857), (5, 50, 5.1, 50.1))) meshes = transform_meshes( src_size=(1000, 2000), src_bbox=(556597.4539663672, 6446275.841017158, 567729.4030456939, 6463612.124257667), src_srs=SRS(3857), dst_size=(1000, 1000), dst_bbox=(5, 50, 5.1, 50.1), dst_srs=SRS(4326), ) eq_(len(meshes), 1) # quad stretches whole image plus 1 pixel eq_(meshes[0][0], (0, 0, 1000, 1000)) for e, a in zip(meshes[0][1], [0.0, 0.0, 0.0, 2000.0, 1000.0, 2000.0, 1000.0, 0.0]): assert_almost_equal(e, a) class TestSingleColorImage(object): def test_one_point(self): img = Image.new('RGB', (100, 100), color='#ff0000') draw = ImageDraw.Draw(img) draw.point((99, 99)) del draw assert not is_single_color_image(img) def test_solid(self): img = Image.new('RGB', (100, 100), color='#ff0102') eq_(is_single_color_image(img), (255, 1, 2)) def test_solid_w_alpha(self): img = Image.new('RGBA', (100, 100), color='#ff0102') eq_(is_single_color_image(img), (255, 1, 2, 255)) def test_solid_paletted_image(self): img = Image.new('P', (100, 100), color=20) palette = [] for i in range(256): palette.extend((i, i//2, i%3)) img.putpalette(palette) eq_(is_single_color_image(img), (20, 10, 2)) class TestMakeTransparent(object): def _make_test_image(self): img = Image.new('RGB', (50, 50), (130, 140, 120)) draw = ImageDraw.Draw(img) draw.rectangle((10, 10, 39, 39), fill=(130, 150, 120)) return img def _make_transp_test_image(self): img = Image.new('RGBA', (50, 50), (130, 140, 120, 100)) draw = ImageDraw.Draw(img) draw.rectangle((10, 10, 39, 39), fill=(130, 150, 120, 120)) return img def test_result(self): img = self._make_test_image() img = make_transparent(img, (130, 150, 120), tolerance=5) assert img.mode == 'RGBA' assert img.size == (50, 50) colors = img.getcolors() assert colors == [(1600, (130, 140, 120, 255)), (900, (130, 150, 120, 0))] def test_with_color_fuzz(self): img = self._make_test_image() img = make_transparent(img, (128, 154, 121), tolerance=5) assert img.mode == 'RGBA' assert img.size == (50, 50) colors = img.getcolors() assert colors == [(1600, (130, 140, 120, 255)), (900, (130, 150, 120, 0))] def test_no_match(self): img = self._make_test_image() img = make_transparent(img, (130, 160, 120), tolerance=5) assert img.mode == 'RGBA' assert img.size == (50, 50) colors = img.getcolors() assert colors == [(1600, (130, 140, 120, 255)), (900, (130, 150, 120, 255))] def test_from_paletted(self): img = self._make_test_image().quantize(256) img = make_transparent(img, (130, 150, 120), tolerance=5) assert img.mode == 'RGBA' assert img.size == (50, 50) colors = img.getcolors() eq_(colors, [(1600, (130, 140, 120, 255)), (900, (130, 150, 120, 0))]) def test_from_transparent(self): img = self._make_transp_test_image() draw = ImageDraw.Draw(img) draw.rectangle((0, 0, 4, 4), fill=(130, 100, 120, 0)) draw.rectangle((5, 5, 9, 9), fill=(130, 150, 120, 255)) img = make_transparent(img, (130, 150, 120, 120), tolerance=5) assert img.mode == 'RGBA' assert img.size == (50, 50) colors = sorted(img.getcolors(), reverse=True) eq_(colors, [(1550, (130, 140, 120, 100)), (900, (130, 150, 120, 0)), (25, (130, 150, 120, 255)), (25, (130, 100, 120, 0))]) class TestTileSplitter(object): def test_background_larger_crop(self): img = ImageSource(Image.new('RGB', (356, 266), (130, 140, 120))) img_opts = ImageOptions('RGB') splitter = TileSplitter(img, img_opts) tile = splitter.get_tile((0, 0), (256, 256)) eq_(tile.size, (256, 256)) colors = tile.as_image().getcolors() eq_(colors, [(256*256, (130, 140, 120))]) tile = splitter.get_tile((256, 256), (256, 256)) eq_(tile.size, (256, 256)) colors = tile.as_image().getcolors() eq_(sorted(colors), [(10*100, (130, 140, 120)), (256*256-10*100, (255, 255, 255))]) def test_background_larger_crop_with_transparent(self): img = ImageSource(Image.new('RGBA', (356, 266), (130, 140, 120, 255))) img_opts = ImageOptions('RGBA', transparent=True) splitter = TileSplitter(img, img_opts) tile = splitter.get_tile((0, 0), (256, 256)) eq_(tile.size, (256, 256)) colors = tile.as_image().getcolors() eq_(colors, [(256*256, (130, 140, 120, 255))]) tile = splitter.get_tile((256, 256), (256, 256)) eq_(tile.size, (256, 256)) colors = tile.as_image().getcolors() eq_(sorted(colors), [(10*100, (130, 140, 120, 255)), (256*256-10*100, (255, 255, 255, 0))]) class TestHasTransparency(object): def test_rgb(self): if not hasattr(Image, 'FASTOCTREE'): raise SkipTest() img = Image.new('RGB', (10, 10)) assert not img_has_transparency(img) img = quantize(img, alpha=False) assert not img_has_transparency(img) def test_rbga(self): if not hasattr(Image, 'FASTOCTREE'): raise SkipTest() img = Image.new('RGBA', (10, 10), (100, 200, 50, 255)) img.paste((255, 50, 50, 0), (3, 3, 7, 7)) assert img_has_transparency(img) img = quantize(img, alpha=True) assert img_has_transparency(img) class TestPeekImageFormat(object): def test_peek(self): yield self.check, 'png', 'png' yield self.check, 'tiff', 'tiff' yield self.check, 'gif', 'gif' yield self.check, 'jpeg', 'jpeg' yield self.check, 'bmp', None def check(self, format, expected_format): buf = BytesIO() Image.new('RGB', (100, 100)).save(buf, format) eq_(peek_image_format(buf), expected_format) class TestBandMerge(object): def setup(self): self.img0 = ImageSource(Image.new('RGB', (10, 10), (0, 10, 20))) self.img1 = ImageSource(Image.new('RGB', (10, 10), (100, 110, 120))) self.img2 = ImageSource(Image.new('RGB', (10, 10), (200, 210, 220))) self.img3 = ImageSource(Image.new('RGB', (10, 10), (0, 255, 0))) self.blank = BlankImageSource(size=(10, 10), image_opts=ImageOptions()) def test_merge_noops(self): """ Check that black image is returned for no ops. """ merger = BandMerger(mode='RGB') img_opts = ImageOptions('RGB') result = merger.merge([self.img0], img_opts) img = result.as_image() eq_(img.size, (10, 10)) eq_(img.getpixel((0, 0)), (0, 0, 0)) def test_merge_missing_source(self): """ Check that empty source list or source list with missing images returns BlankImageSource. """ merger = BandMerger(mode='RGB') merger.add_ops(dst_band=0, src_img=0, src_band=0) merger.add_ops(dst_band=1, src_img=1, src_band=0) merger.add_ops(dst_band=2, src_img=2, src_band=0) img_opts = ImageOptions('RGBA', transparent=True) result = merger.merge([], img_opts, size=(10, 10)) img = result.as_image() eq_(img.size, (10, 10)) eq_(img.getpixel((0, 0)), (255, 255, 255, 0)) result = merger.merge([self.img0, self.img1], img_opts, size=(10, 10)) img = result.as_image() eq_(img.size, (10, 10)) eq_(img.getpixel((0, 0)), (255, 255, 255, 0)) def test_rgb_merge(self): """ Check merge of RGB bands """ merger = BandMerger(mode='RGB') merger.add_ops(dst_band=1, src_img=0, src_band=0, factor=0.5) merger.add_ops(dst_band=1, src_img=3, src_band=1, factor=0.5) merger.add_ops(dst_band=0, src_img=2, src_band=1) merger.add_ops(dst_band=2, src_img=1, src_band=2) img_opts = ImageOptions('RGB') result = merger.merge([self.img0, self.img1, self.img2, self.img3], img_opts) img = result.as_image() eq_(img.getpixel((0, 0)), (210, 127, 120)) def test_rgb_merge_missing(self): """ Check missing band is set to 0 """ merger = BandMerger(mode='RGB') merger.add_ops(dst_band=0, src_img=2, src_band=1) merger.add_ops(dst_band=2, src_img=1, src_band=2) img_opts = ImageOptions('RGB') result = merger.merge([self.img0, self.img1, self.img2, self.img3], img_opts) img = result.as_image() eq_(img.getpixel((0, 0)), (210, 0, 120)) def test_rgba_merge(self): """ Check merge of RGBA bands """ merger = BandMerger(mode='RGBA') merger.add_ops(dst_band=1, src_img=0, src_band=0, factor=0.5) merger.add_ops(dst_band=1, src_img=3, src_band=1, factor=0.5) merger.add_ops(dst_band=0, src_img=2, src_band=1) merger.add_ops(dst_band=2, src_img=1, src_band=2) merger.add_ops(dst_band=3, src_img=1, src_band=1) img_opts = ImageOptions('RGBA') result = merger.merge([self.img0, self.img1, self.img2, self.img3], img_opts) img = result.as_image() eq_(img.getpixel((0, 0)), (210, 127, 120, 110)) def test_rgba_merge_missing_a(self): """ Check that missing alpha band defaults to opaque """ merger = BandMerger(mode='RGBA') merger.add_ops(dst_band=1, src_img=0, src_band=0, factor=0.5) merger.add_ops(dst_band=1, src_img=3, src_band=1, factor=0.5) merger.add_ops(dst_band=0, src_img=2, src_band=1) merger.add_ops(dst_band=2, src_img=1, src_band=2) img_opts = ImageOptions('RGBA') result = merger.merge([self.img0, self.img1, self.img2, self.img3], img_opts) img = result.as_image() eq_(img.getpixel((0, 0)), (210, 127, 120, 255)) def test_l_merge(self): """ Check merge bands to grayscale image """ merger = BandMerger(mode='L') merger.add_ops(dst_band=0, src_img=0, src_band=2, factor=0.2) merger.add_ops(dst_band=0, src_img=2, src_band=1, factor=0.3) merger.add_ops(dst_band=0, src_img=3, src_band=1, factor=0.5) img_opts = ImageOptions('L') result = merger.merge([self.img0, self.img1, self.img2, self.img3], img_opts) img = result.as_image() eq_(img.getpixel((0, 0)), int(20*0.2) + int(210*0.3) + int(255*0.5)) def test_p_merge(self): """ Check merge bands to paletted image """ merger = BandMerger(mode='RGB') merger.add_ops(dst_band=1, src_img=0, src_band=0, factor=0.5) merger.add_ops(dst_band=1, src_img=3, src_band=1, factor=0.5) merger.add_ops(dst_band=0, src_img=2, src_band=1) merger.add_ops(dst_band=2, src_img=1, src_band=2) img_opts = ImageOptions('P', format='image/png', encoding_options={'quantizer': 'mediancut'}) result = merger.merge([self.img0, self.img1, self.img2, self.img3], img_opts) # need to encode to get conversion to P img = Image.open(result.as_buffer()) eq_(img.mode, 'P') img = img.convert('RGB') eq_(img.getpixel((0, 0)), (210, 127, 120)) def test_from_p_merge(self): """ Check merge bands from paletted image """ merger = BandMerger(mode='RGB') merger.add_ops(dst_band=0, src_img=0, src_band=2) merger.add_ops(dst_band=1, src_img=0, src_band=1) merger.add_ops(dst_band=2, src_img=0, src_band=0) img = Image.new('RGB', (10, 10), (0, 100, 200)).quantize(256) eq_(img.mode, 'P') # src img is P but we can still access RGB bands src_img = ImageSource(img) img_opts = ImageOptions('RGB') result = merger.merge([src_img], img_opts) img = result.as_image() eq_(img.mode, 'RGB') eq_(img.getpixel((0, 0)), (200, 100, 0)) def test_from_mixed_merge(self): """ Check merge RGBA bands from image without alpha (mixed) """ merger = BandMerger(mode='RGBA') merger.add_ops(dst_band=0, src_img=0, src_band=2) merger.add_ops(dst_band=1, src_img=0, src_band=1) merger.add_ops(dst_band=2, src_img=0, src_band=0) merger.add_ops(dst_band=3, src_img=0, src_band=3) img = Image.new('RGB', (10, 10), (0, 100, 200)) src_img = ImageSource(img) img_opts = ImageOptions('RGBA') result = merger.merge([src_img], img_opts) img = result.as_image() eq_(img.mode, 'RGBA') eq_(img.getpixel((0, 0)), (200, 100, 0, 255)) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_image_mask.py�����������������������������������������������0000664�0000000�0000000�00000012674�13204544724�0023013�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of the MapProxy project. # Copyright (C) 2012 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.compat.image import Image, ImageDraw from mapproxy.srs import SRS from mapproxy.image import ImageSource from mapproxy.image.opts import ImageOptions from mapproxy.image.mask import mask_image_source_from_coverage from mapproxy.image.merge import LayerMerger from mapproxy.util.coverage import load_limited_to from mapproxy.test.image import assert_img_colors_eq, create_image from nose.tools import eq_ try: from shapely.geometry import Polygon geom_support = True except ImportError: geom_support = False if not geom_support: from nose.plugins.skip import SkipTest raise SkipTest('requires Shapely') def coverage(geom, srs='EPSG:4326'): return load_limited_to({'srs': srs, 'geometry': geom}) class TestMaskImage(object): def test_mask_outside_of_image_transparent(self): img = ImageSource(Image.new('RGB', (100, 100), color=(100, 0, 200)), image_opts=ImageOptions(transparent=True)) result = mask_image_source_from_coverage(img, [0, 0, 10, 10], SRS(4326), coverage([20, 20, 30, 30])) assert_img_colors_eq(result.as_image().getcolors(), [((100*100), (255, 255, 255, 0))]) def test_mask_outside_of_image_bgcolor(self): img = ImageSource(Image.new('RGB', (100, 100), color=(100, 0, 200)), image_opts=ImageOptions(bgcolor=(200, 30, 120))) result = mask_image_source_from_coverage(img, [0, 0, 10, 10], SRS(4326), coverage([20, 20, 30, 30])) assert_img_colors_eq(result.as_image().getcolors(), [((100*100), (200, 30, 120))]) def test_mask_partial_image_bgcolor(self): img = ImageSource(Image.new('RGB', (100, 100), color=(100, 0, 200)), image_opts=ImageOptions(bgcolor=(200, 30, 120))) result = mask_image_source_from_coverage(img, [0, 0, 10, 10], SRS(4326), coverage([5, 5, 30, 30])) assert_img_colors_eq(result.as_image().getcolors(), [(7500, (200, 30, 120)), (2500, (100, 0, 200))]) def test_mask_partial_image_transparent(self): img = ImageSource(Image.new('RGB', (100, 100), color=(100, 0, 200)), image_opts=ImageOptions(transparent=True)) result = mask_image_source_from_coverage(img, [0, 0, 10, 10], SRS(4326), coverage([5, 5, 30, 30])) assert_img_colors_eq(result.as_image().getcolors(), [(7500, (255, 255, 255, 0)), (2500, (100, 0, 200, 255))]) def test_wkt_mask_partial_image_transparent(self): img = ImageSource(Image.new('RGB', (100, 100), color=(100, 0, 200)), image_opts=ImageOptions(transparent=True)) # polygon with hole geom = 'POLYGON((2 2, 2 8, 8 8, 8 2, 2 2), (4 4, 4 6, 6 6, 6 4, 4 4))' result = mask_image_source_from_coverage(img, [0, 0, 10, 10], SRS(4326), coverage(geom)) # 60*60 - 20*20 = 3200 assert_img_colors_eq(result.as_image().getcolors(), [(10000-3200, (255, 255, 255, 0)), (3200, (100, 0, 200, 255))]) def test_shapely_mask_with_transform_partial_image_transparent(self): img = ImageSource(Image.new('RGB', (100, 100), color=(100, 0, 200)), image_opts=ImageOptions(transparent=True)) p = Polygon([(0, 0), (222000, 0), (222000, 222000), (0, 222000)]) # ~ 2x2 degres result = mask_image_source_from_coverage(img, [0, 0, 10, 10], SRS(4326), coverage(p, 'EPSG:3857')) # 20*20 = 400 assert_img_colors_eq(result.as_image().getcolors(), [(10000-400, (255, 255, 255, 0)), (400, (100, 0, 200, 255))]) class TestLayerCoverageMerge(object): def setup(self): self.coverage1 = coverage(Polygon([(0, 0), (0, 10), (10, 10), (10, 0)]), 3857) self.coverage2 = coverage([2, 2, 8, 8], 3857) def test_merge_single_coverage(self): merger = LayerMerger() merger.add(ImageSource(Image.new('RGB', (10, 10), (255, 255, 255))), self.coverage1) result = merger.merge(image_opts=ImageOptions(transparent=True), bbox=(5, 0, 15, 10), bbox_srs=3857) img = result.as_image() eq_(img.mode, 'RGBA') eq_(img.getpixel((4, 0)), (255, 255, 255, 255)) eq_(img.getpixel((6, 0)), (255, 255, 255, 0)) def test_merge_overlapping_coverage(self): color1 = (255, 255, 0) color2 = (0, 255, 255) merger = LayerMerger() merger.add(ImageSource(Image.new('RGB', (10, 10), color1)), self.coverage1) merger.add(ImageSource(Image.new('RGB', (10, 10), color2)), self.coverage2) result = merger.merge(image_opts=ImageOptions(), bbox=(0, 0, 10, 10), bbox_srs=3857) img = result.as_image() eq_(img.mode, 'RGB') expected = create_image((10, 10), color1, 'RGB') draw = ImageDraw.Draw(expected) draw.polygon([(2, 2), (7, 2), (7, 7), (2, 7)], fill=color2) for x in range(0, 9): for y in range(0, 9): eq_(img.getpixel((x, y)), expected.getpixel((x, y))) ��������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_image_messages.py�������������������������������������������0000664�0000000�0000000�00000015127�13204544724�0023663�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -:- encoding: utf8 -:- # This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function from mapproxy.compat.image import ( Image, ImageDraw, ImageColor, ImageFont, ) from mapproxy.compat import PY3 from mapproxy.cache.tile import Tile from mapproxy.image import ImageSource from mapproxy.image.message import TextDraw, message_image from mapproxy.image.opts import ImageOptions from mapproxy.tilefilter import watermark_filter from nose.tools import eq_ from nose.plugins.skip import SkipTest PNG_FORMAT = ImageOptions(format='image/png') class TestTextDraw(object): def test_ul(self): font = ImageFont.load_default() td = TextDraw('Hello', font) img = Image.new('RGB', (100, 100)) draw = ImageDraw.Draw(img) total_box, boxes = td.text_boxes(draw, (100, 100)) eq_(total_box, boxes[0]) eq_(len(boxes), 1) def test_multiline_ul(self): font = ImageFont.load_default() td = TextDraw('Hello\nWorld', font) img = Image.new('RGB', (100, 100)) draw = ImageDraw.Draw(img) total_box, boxes = td.text_boxes(draw, (100, 100)) eq_(total_box, (5, 5, 35, 30)) eq_(boxes, [(5, 5, 35, 16), (5, 19, 35, 30)]) def test_multiline_lr(self): font = ImageFont.load_default() td = TextDraw('Hello\nWorld', font, placement='lr') img = Image.new('RGB', (100, 100)) draw = ImageDraw.Draw(img) total_box, boxes = td.text_boxes(draw, (100, 100)) eq_(total_box, (65, 70, 95, 95)) eq_(boxes, [(65, 70, 95, 81), (65, 84, 95, 95)]) def test_multiline_center(self): font = ImageFont.load_default() td = TextDraw('Hello\nWorld', font, placement='cc') img = Image.new('RGB', (100, 100)) draw = ImageDraw.Draw(img) total_box, boxes = td.text_boxes(draw, (100, 100)) eq_(total_box, (35, 38, 65, 63)) eq_(boxes, [(35, 38, 65, 49), (35, 52, 65, 63)]) def test_unicode(self): font = ImageFont.load_default() td = TextDraw(u'Héllö\nWørld', font, placement='cc') img = Image.new('RGB', (100, 100)) draw = ImageDraw.Draw(img) total_box, boxes = td.text_boxes(draw, (100, 100)) if PY3: raise SkipTest('unicode handling for default font differs on PY3') eq_(total_box, (35, 38, 65, 63)) eq_(boxes, [(35, 38, 65, 49), (35, 52, 65, 63)]) def _test_all(self): for x in 'c': for y in 'LR': yield self.check_placement, x, y def check_placement(self, x, y): font = ImageFont.load_default() td = TextDraw('Hello\nWorld\n%s %s' % (x, y), font, placement=x+y, padding=5, linespacing=2) img = Image.new('RGB', (100, 100)) draw = ImageDraw.Draw(img) td.draw(draw, img.size) img.show() def test_transparent(self): font = ImageFont.load_default() td = TextDraw('Hello\nWorld', font, placement='cc') img = Image.new('RGBA', (100, 100), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) td.draw(draw, img.size) eq_(len(img.getcolors()), 2) # top color (bg) is transparent eq_(sorted(img.getcolors())[1][1], (0, 0, 0, 0)) class TestMessageImage(object): def test_blank(self): image_opts = PNG_FORMAT.copy() image_opts.bgcolor = '#113399' img = message_image('', size=(100, 150), image_opts=image_opts) assert isinstance(img, ImageSource) eq_(img.size, (100, 150)) pil_img = img.as_image() eq_(pil_img.getpixel((0, 0)), ImageColor.getrgb('#113399')) # 3 values in histogram (RGB) assert [x for x in pil_img.histogram() if x > 0] == [15000, 15000, 15000] def test_message(self): image_opts = PNG_FORMAT.copy() image_opts.bgcolor = '#113399' img = message_image('test', size=(100, 150), image_opts=image_opts) assert isinstance(img, ImageSource) assert img.size == (100, 150) # 6 values in histogram (3xRGB for background, 3xRGB for text message) eq_([x for x in img.as_image().histogram() if x > 10], [14923, 77, 14923, 77, 14923, 77]) def test_transparent(self): image_opts = ImageOptions(transparent=True) print(image_opts) img = message_image('', size=(100, 150), image_opts=image_opts) assert isinstance(img, ImageSource) assert img.size == (100, 150) pil_img = img.as_image() eq_(pil_img.getpixel((0, 0)), (255, 255, 255, 0)) # 6 values in histogram (3xRGB for background, 3xRGB for text message) assert [x for x in pil_img.histogram() if x > 0] == \ [15000, 15000, 15000, 15000] class TestWatermarkTileFilter(object): def setup(self): self.tile = Tile((0, 0, 0)) self.filter = watermark_filter('Test') def test_filter(self): img = Image.new('RGB', (200, 200)) orig_source = ImageSource(img) self.tile.source = orig_source filtered_tile = self.filter(self.tile) assert self.tile is filtered_tile assert orig_source != filtered_tile.source pil_img = filtered_tile.source.as_image() eq_(pil_img.getpixel((0, 0)), (0, 0, 0)) colors = pil_img.getcolors() colors.sort() # most but not all parts are bg color assert 39950 > colors[-1][0] > 39000 assert colors[-1][1] == (0, 0, 0) def test_filter_with_alpha(self): img = Image.new('RGBA', (200, 200), (10, 15, 20, 0)) orig_source = ImageSource(img) self.tile.source = orig_source filtered_tile = self.filter(self.tile) assert self.tile is filtered_tile assert orig_source != filtered_tile.source pil_img = filtered_tile.source.as_image() eq_(pil_img.getpixel((0, 0)), (10, 15, 20, 0)) colors = pil_img.getcolors() colors.sort() # most but not all parts are bg color assert 39950 > colors[-1][0] > 39000 eq_(colors[-1][1], (10, 15, 20, 0))�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_image_options.py��������������������������������������������0000664�0000000�0000000�00000012170�13204544724�0023542�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.image.opts import ImageOptions, create_image, compatible_image_options from nose.tools import eq_ class TestCreateImage(object): def test_default(self): img = create_image((100, 100)) eq_(img.size, (100, 100)) eq_(img.mode, 'RGB') eq_(img.getcolors(), [(100*100, (255, 255, 255))]) def test_transparent(self): img = create_image((100, 100), ImageOptions(transparent=True)) eq_(img.size, (100, 100)) eq_(img.mode, 'RGBA') eq_(img.getcolors(), [(100*100, (255, 255, 255, 0))]) def test_transparent_rgb(self): img = create_image((100, 100), ImageOptions(mode='RGB', transparent=True)) eq_(img.size, (100, 100)) eq_(img.mode, 'RGB') eq_(img.getcolors(), [(100*100, (255, 255, 255))]) def test_bgcolor(self): img = create_image((100, 100), ImageOptions(bgcolor=(200, 100, 0))) eq_(img.size, (100, 100)) eq_(img.mode, 'RGB') eq_(img.getcolors(), [(100*100, (200, 100, 0))]) def test_rgba_bgcolor(self): img = create_image((100, 100), ImageOptions(bgcolor=(200, 100, 0, 30))) eq_(img.size, (100, 100)) eq_(img.mode, 'RGB') eq_(img.getcolors(), [(100*100, (200, 100, 0))]) def test_rgba_bgcolor_transparent(self): img = create_image((100, 100), ImageOptions(bgcolor=(200, 100, 0, 30), transparent=True)) eq_(img.size, (100, 100)) eq_(img.mode, 'RGBA') eq_(img.getcolors(), [(100*100, (200, 100, 0, 30))]) def test_rgba_bgcolor_rgba_mode(self): img = create_image((100, 100), ImageOptions(bgcolor=(200, 100, 0, 30), mode='RGBA')) eq_(img.size, (100, 100)) eq_(img.mode, 'RGBA') eq_(img.getcolors(), [(100*100, (200, 100, 0, 30))]) class TestCompatibleImageOptions(object): def test_formats(self): img_opts = compatible_image_options([ ImageOptions(format='image/png'), ImageOptions(format='image/jpeg'), ]) eq_(img_opts.format, 'image/png') img_opts = compatible_image_options([ ImageOptions(format='image/png'), ImageOptions(format='image/jpeg'), ], ImageOptions(format='image/tiff'), ) eq_(img_opts.format, 'image/tiff') def test_colors(self): img_opts = compatible_image_options([ ImageOptions(colors=None), ImageOptions(colors=16), ]) eq_(img_opts.colors, 16) img_opts = compatible_image_options([ ImageOptions(colors=256), ImageOptions(colors=16), ]) eq_(img_opts.colors, 256) img_opts = compatible_image_options([ ImageOptions(colors=256), ImageOptions(colors=16), ], ImageOptions(colors=4) ) eq_(img_opts.colors, 4) img_opts = compatible_image_options([ ImageOptions(colors=256), ImageOptions(colors=0), ]) eq_(img_opts.colors, 0) def test_transparent(self): img_opts = compatible_image_options([ ImageOptions(transparent=False), ImageOptions(transparent=True), ]) eq_(img_opts.transparent, False) img_opts = compatible_image_options([ ImageOptions(transparent=None), ImageOptions(transparent=True), ]) eq_(img_opts.transparent, True) img_opts = compatible_image_options([ ImageOptions(transparent=None), ImageOptions(transparent=True), ], ImageOptions(transparent=None) ) eq_(img_opts.transparent, True) img_opts = compatible_image_options([ ImageOptions(transparent=True), ImageOptions(transparent=True), ]) eq_(img_opts.transparent, True) def test_mode(self): img_opts = compatible_image_options([ ImageOptions(mode='RGB'), ImageOptions(mode='P'), ]) eq_(img_opts.mode, 'RGB') img_opts = compatible_image_options([ ImageOptions(mode='RGBA'), ImageOptions(mode='P'), ]) eq_(img_opts.mode, 'RGBA') img_opts = compatible_image_options([ ImageOptions(mode='RGB'), ImageOptions(mode='P'), ]) eq_(img_opts.mode, 'RGB') img_opts = compatible_image_options([ ImageOptions(mode='RGB'), ImageOptions(mode='P'), ], ImageOptions(mode='P') ) eq_(img_opts.mode, 'P') ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_multiapp.py�������������������������������������������������0000664�0000000�0000000�00000012250�13204544724�0022537�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -:- encoding: utf-8 -:- # This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import time import tempfile import shutil from mapproxy.multiapp import DirectoryConfLoader, MultiMapProxy from nose.tools import eq_ class TestDirectoryConfLoader(object): def setup(self): self.dir = tempfile.mkdtemp() def teardown(self): shutil.rmtree(self.dir) def make_conf_file(self, name): conf_file_name = os.path.join(self.dir, name) with open(conf_file_name, 'wb'): pass return conf_file_name def test_available_apps_empty(self): loader = DirectoryConfLoader(self.dir) eq_(loader.available_apps(), []) def test_available_apps(self): self.make_conf_file('foo.yaml') self.make_conf_file('bar.yaml') loader = DirectoryConfLoader(self.dir) eq_(set(loader.available_apps()), set(['foo', 'bar'])) self.make_conf_file('bazz.yaml') eq_(set(loader.available_apps()), set(['foo', 'bar', 'bazz'])) def test_app_available(self): self.make_conf_file('foo.yaml') loader = DirectoryConfLoader(self.dir) assert loader.app_available('foo') assert not loader.app_available('bar') def test_app_conf(self): foo_conf_file = self.make_conf_file('foo.yaml') loader = DirectoryConfLoader(self.dir) app_conf = loader.app_conf('foo') eq_(app_conf['mapproxy_conf'], foo_conf_file) def test_app_conf_unknown_app(self): loader = DirectoryConfLoader(self.dir) app_conf = loader.app_conf('foo') assert app_conf is None def test_needs_reload(self): foo_conf_file = self.make_conf_file('foo.yaml') mtime = os.path.getmtime(foo_conf_file) timestamps = {foo_conf_file: mtime} loader = DirectoryConfLoader(self.dir) assert loader.needs_reload('foo', timestamps) == False timestamps[foo_conf_file] -= 10 assert loader.needs_reload('foo', timestamps) == True def test_custom_suffix(self): self.make_conf_file('foo.conf') loader = DirectoryConfLoader(self.dir, suffix='.conf') assert loader.app_available('foo') minimal_mapproxy_conf = b""" services: wms: layers: mylayer: title: My Layer sources: [mysource] sources: mysource: type: wms req: url: http://example.org/service? layers: foo,bar """ class DummyReq(object): script_url = '' class TestMultiMapProxy(object): def setup(self): self.dir = tempfile.mkdtemp() self.loader = DirectoryConfLoader(self.dir) def teardown(self): shutil.rmtree(self.dir) def make_conf_file(self, name): app_conf_file_name = os.path.join(self.dir, name) with open(app_conf_file_name, 'wb') as f: f.write(minimal_mapproxy_conf) return app_conf_file_name def test_listing_with_apps(self): self.make_conf_file('foo.yaml') mmp = MultiMapProxy(self.loader, list_apps=True) resp = mmp.index_list(DummyReq()) assert 'foo' in resp.response def test_listing_without_apps(self): self.make_conf_file('foo.yaml') mmp = MultiMapProxy(self.loader) resp = mmp.index_list(DummyReq()) assert 'foo' not in resp.response assert mmp.proj_app('foo') is not None def test_cached_app_loading(self): self.make_conf_file('foo.yaml') mmp = MultiMapProxy(self.loader) app1 = mmp.proj_app('foo') app2 = mmp.proj_app('foo') # app is cached assert app1 is app2 def test_app_reloading(self): app_conf_file_name = self.make_conf_file('foo.yaml') mmp = MultiMapProxy(self.loader) app = mmp.proj_app('foo') # touch configuration file os.utime(app_conf_file_name, (time.time()+10, time.time()+10)) # app was reloaded assert app is not mmp.proj_app('foo') def test_app_unloading(self): self.make_conf_file('app1.yaml') self.make_conf_file('app2.yaml') self.make_conf_file('app3.yaml') mmp = MultiMapProxy(self.loader, app_cache_size=2) app1 = mmp.proj_app('app1') app2 = mmp.proj_app('app2') # lru cache [app1, app2] assert app1 is mmp.proj_app('app1') assert app2 is mmp.proj_app('app2') # lru cache [app1, app2] app3 = mmp.proj_app('app3') # lru cache [app2, app3] assert app3 is mmp.proj_app('app3') assert app2 is mmp.proj_app('app2') assert app1 is not mmp.proj_app('app1') # lru cache [app2, app1] assert app3 is not mmp.proj_app('app3') ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_ogr_reader.py�����������������������������������������������0000664�0000000�0000000�00000003040�13204544724�0023012�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os from mapproxy.util.ogr import OGRShapeReader, libgdal from nose.tools import eq_ from nose.plugins.skip import SkipTest if not libgdal: raise SkipTest('libgdal not found') polygon_file = os.path.join(os.path.dirname(__file__), 'polygons', 'polygons.shp') class TestOGRShapeReader(object): def setup(self): self.reader = OGRShapeReader(polygon_file) def test_read_all(self): wkts = list(self.reader.wkts()) eq_(len(wkts), 3) for wkt in wkts: assert wkt.startswith(b'POLYGON ('), 'unexpected WKT: %s' % wkt def test_read_filter(self): wkts = list(self.reader.wkts(where='name = "germany"')) eq_(len(wkts), 2) for wkt in wkts: assert wkt.startswith(b'POLYGON ('), 'unexpected WKT: %s' % wkt def test_read_filter_no_match(self): wkts = list(self.reader.wkts(where='name = "foo"')) eq_(len(wkts), 0) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_request.py��������������������������������������������������0000664�0000000�0000000�00000060570�13204544724�0022404�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -:- encoding: UTF8 -:- # This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function from mapproxy.srs import SRS from mapproxy.request.base import url_decode, Request, NoCaseMultiDict, RequestParams from mapproxy.request.tile import TMSRequest, tile_request, TileRequest from mapproxy.request.wms import (wms_request, WMSMapRequest, WMSMapRequestParams, WMS111MapRequest, WMS100MapRequest, WMS130MapRequest, WMS111FeatureInfoRequest) from mapproxy.request.arcgis import ArcGISRequest, ArcGISIdentifyRequest from mapproxy.exception import RequestError from mapproxy.request.wms.exception import (WMS111ExceptionHandler, WMSImageExceptionHandler, WMSBlankExceptionHandler) from mapproxy.test.http import make_wsgi_env, assert_url_eq, assert_query_eq import pickle from nose.tools import eq_ class TestNoCaseMultiDict(object): def test_from_iterable(self): data = (('layers', 'foo,bar'), ('laYERs', 'baz'), ('crs', 'EPSG:4326')) nc_dict = NoCaseMultiDict(data) print(nc_dict) for name in ('layers', 'LAYERS', 'lAYeRS'): assert name in nc_dict, name + ' not found' assert nc_dict.get_all('layers') == ['foo,bar', 'baz'] assert nc_dict.get_all('crs') == ['EPSG:4326'] def test_from_dict(self): data = [('layers', 'foo,bar'), ('laYERs', 'baz'), ('crs', 'EPSG:4326')] nc_dict = NoCaseMultiDict(data) print(nc_dict) for name in ('layers', 'LAYERS', 'lAYeRS'): assert name in nc_dict, name + ' not found' assert nc_dict.get_all('layers') == ['foo,bar', 'baz'] assert nc_dict.get_all('crs') == ['EPSG:4326'] def test_iteritems(self): data = [('LAYERS', 'foo,bar'), ('laYERs', 'baz'), ('crs', 'EPSG:4326')] nc_dict = NoCaseMultiDict(data) for key, values in nc_dict.iteritems(): if key in ('LAYERS', 'laYERs'): assert values == ['foo,bar', 'baz'] elif key == 'crs': assert values == ['EPSG:4326'] else: assert False, 'unexpected key ' + key def test_multiple_sets(self): nc_dict = NoCaseMultiDict() nc_dict['foo'] = 'bar' assert nc_dict['FOO'] == 'bar' nc_dict['foo'] = 'baz' assert nc_dict['FOO'] == 'baz' def test_missing_key(self): nc_dict = NoCaseMultiDict([('foo', 'bar')]) try: nc_dict['bar'] assert False, 'Did not throw KeyError exception.' except KeyError: pass def test_get(self): nc_dict = NoCaseMultiDict([('foo', 'bar'), ('num', '42')]) assert nc_dict.get('bar') == None assert nc_dict.get('bar', 'default_bar') == 'default_bar' assert nc_dict.get('num') == '42' assert nc_dict.get('num', type_func=int) == 42 assert nc_dict.get('foo') == 'bar' def test_get_all(self): nc_dict = NoCaseMultiDict([('foo', 'bar'), ('num', '42'), ('foo', 'biz')]) assert nc_dict.get_all('bar') == [] assert nc_dict.get_all('foo') == ['bar', 'biz'] assert nc_dict.get_all('num') == ['42'] def test_set(self): nc_dict = NoCaseMultiDict() nc_dict.set('foo', 'bar') assert nc_dict.get_all('fOO') == ['bar'] nc_dict.set('fOo', 'buzz', append=True) assert nc_dict.get_all('FOO') == ['bar', 'buzz'] nc_dict.set('foO', 'bizz') assert nc_dict.get_all('FOO') == ['bizz'] nc_dict.set('foO', ['ham', 'spam'], unpack=True) assert nc_dict.get_all('FOO') == ['ham', 'spam'] nc_dict.set('FoO', ['egg', 'bacon'], append=True, unpack=True) assert nc_dict.get_all('FOo') == ['ham', 'spam', 'egg', 'bacon'] def test_setitem(self): nc_dict = NoCaseMultiDict() nc_dict['foo'] = 'bar' assert nc_dict['foo'] == 'bar' nc_dict['foo'] = 'buz' assert nc_dict['foo'] == 'buz' nc_dict['bar'] = nc_dict['foo'] assert nc_dict['bar'] == 'buz' nc_dict['bing'] = '1' nc_dict['bong'] = '2' nc_dict['bing'] = nc_dict['bong'] assert nc_dict['bing'] == '2' assert nc_dict['bong'] == '2' def test_del(self): nc_dict = NoCaseMultiDict([('foo', 'bar'), ('num', '42')]) assert nc_dict['fOO'] == 'bar' del nc_dict['FOO'] assert nc_dict.get('foo') == None class DummyRequest(object): def __init__(self, args, url=''): self.args = args self.base_url = url class TestWMSMapRequest(object): def setup(self): self.base_req = url_decode('''SERVICE=WMS&format=image%2Fpng&layers=foo&styles=& REQUEST=GetMap&height=300&srs=EPSG%3A4326&VERSION=1.1.1& bbox=7,50,8,51&width=400'''.replace('\n','')) class TestWMS100MapRequest(TestWMSMapRequest): def setup(self): TestWMSMapRequest.setup(self) del self.base_req['service'] del self.base_req['version'] self.base_req['wmtver'] = '1.0.0' self.base_req['request'] = 'Map' def test_basic_request(self): req = wms_request(DummyRequest(self.base_req), validate=False) assert isinstance(req, WMS100MapRequest) eq_(req.params.request, 'GetMap') class TestWMS111MapRequest(TestWMSMapRequest): def test_basic_request(self): req = wms_request(DummyRequest(self.base_req), validate=False) assert isinstance(req, WMS111MapRequest) eq_(req.params.request, 'GetMap') class TestWMS130MapRequest(TestWMSMapRequest): def setup(self): TestWMSMapRequest.setup(self) self.base_req['version'] = '1.3.0' self.base_req['crs'] = self.base_req['srs'] del self.base_req['srs'] def test_basic_request(self): req = wms_request(DummyRequest(self.base_req), validate=False) assert isinstance(req, WMS130MapRequest) eq_(req.params.request, 'GetMap') eq_(req.params.bbox, (50.0, 7.0, 51.0, 8.0)) def test_copy_with_request_params(self): # check that we allways have our internal axis order req1 = WMS130MapRequest(param=dict(bbox="10,0,20,40", crs='EPSG:4326')) eq_(req1.params.bbox, (0.0, 10.0, 40.0, 20.0)) req2 = WMS111MapRequest(param=dict(bbox="0,10,40,20", srs='EPSG:4326')) eq_(req2.params.bbox, (0.0, 10.0, 40.0, 20.0)) # 130 <- 111 req3 = req1.copy_with_request_params(req2) eq_(req3.params.bbox, (0.0, 10.0, 40.0, 20.0)) assert isinstance(req3, WMS130MapRequest) # 130 <- 130 req4 = req1.copy_with_request_params(req3) eq_(req4.params.bbox, (0.0, 10.0, 40.0, 20.0)) assert isinstance(req4, WMS130MapRequest) # 111 <- 130 req5 = req2.copy_with_request_params(req3) eq_(req5.params.bbox, (0.0, 10.0, 40.0, 20.0)) assert isinstance(req5, WMS111MapRequest) class TestWMS111FeatureInfoRequest(TestWMSMapRequest): def setup(self): TestWMSMapRequest.setup(self) self.base_req['request'] = 'GetFeatureInfo' self.base_req['x'] = '100' self.base_req['y'] = '150' self.base_req['query_layers'] = 'foo' def test_basic_request(self): req = wms_request(DummyRequest(self.base_req))#, validate=False) assert isinstance(req, WMS111FeatureInfoRequest) def test_pos(self): req = wms_request(DummyRequest(self.base_req)) eq_(req.params.pos, (100, 150)) def test_pos_coords(self): req = wms_request(DummyRequest(self.base_req)) eq_(req.params.pos_coords, (7.25, 50.5)) class TestArcGISRequest(object): def test_base_request(self): req = ArcGISRequest(url="http://example.com/ArcGIS/rest/MapServer/") eq_("http://example.com/ArcGIS/rest/MapServer/export", req.url) req.params.bbox = [-180.0, -90.0, 180.0, 90.0] eq_((-180.0, -90.0, 180.0, 90.0), req.params.bbox) eq_("-180.0,-90.0,180.0,90.0", req.params["bbox"]) req.params.size = [256, 256] eq_((256, 256), req.params.size) eq_("256,256", req.params["size"]) req.params.imageSR = "EPSG:4326" eq_("4326", req.params.imageSR) eq_("4326", req.params["imageSR"]) req.params.bboxSR = SRS("EPSG:4326") eq_("4326", req.params.bboxSR) eq_("4326", req.params["bboxSR"]) def check_endpoint(self, url, expected): req = ArcGISRequest(url=url) eq_(req.url, expected) def test_endpoint_urls(self): yield self.check_endpoint, 'http://example.com/ArcGIS/rest/MapServer/', 'http://example.com/ArcGIS/rest/MapServer/export' yield self.check_endpoint, 'http://example.com/ArcGIS/rest/MapServer', 'http://example.com/ArcGIS/rest/MapServer/export' yield self.check_endpoint, 'http://example.com/ArcGIS/rest/MapServer/export', 'http://example.com/ArcGIS/rest/MapServer/export' yield self.check_endpoint, 'http://example.com/ArcGIS/rest/ImageServer/', 'http://example.com/ArcGIS/rest/ImageServer/exportImage' yield self.check_endpoint, 'http://example.com/ArcGIS/rest/ImageServer', 'http://example.com/ArcGIS/rest/ImageServer/exportImage' yield self.check_endpoint, 'http://example.com/ArcGIS/rest/ImageServer/export', 'http://example.com/ArcGIS/rest/ImageServer/exportImage' yield self.check_endpoint, 'http://example.com/ArcGIS/rest/ImageServer/exportImage', 'http://example.com/ArcGIS/rest/ImageServer/exportImage' yield self.check_endpoint, 'http://example.com/ArcGIS/rest/MapServer/export?param=foo', 'http://example.com/ArcGIS/rest/MapServer/export?param=foo' yield self.check_endpoint, 'http://example.com/ArcGIS/rest/ImageServer/export?param=foo', 'http://example.com/ArcGIS/rest/ImageServer/exportImage?param=foo' class TestArcGISIndentifyRequest(object): def test_base_request(self): req = ArcGISIdentifyRequest(url="http://example.com/ArcGIS/rest/MapServer/") eq_("http://example.com/ArcGIS/rest/MapServer/identify", req.url) req.params.bbox = [-180.0, -90.0, 180.0, 90.0] eq_((-180.0, -90.0, 180.0, 90.0), req.params.bbox) eq_("-180.0,-90.0,180.0,90.0", req.params["mapExtent"]) req.params.size = [256, 256] eq_((256, 256), req.params.size) eq_("256,256,96", req.params["imageDisplay"]) req.params.srs = "EPSG:4326" eq_("EPSG:4326", req.params.srs) eq_("4326", req.params["sr"]) def check_endpoint(self, url, expected): req = ArcGISIdentifyRequest(url=url) eq_(req.url, expected) def test_endpoint_urls(self): yield self.check_endpoint, 'http://example.com/ArcGIS/rest/MapServer/', 'http://example.com/ArcGIS/rest/MapServer/identify' yield self.check_endpoint, 'http://example.com/ArcGIS/rest/MapServer', 'http://example.com/ArcGIS/rest/MapServer/identify' yield self.check_endpoint, 'http://example.com/ArcGIS/rest/MapServer/export', 'http://example.com/ArcGIS/rest/MapServer/identify' yield self.check_endpoint, 'http://example.com/ArcGIS/rest/ImageServer/', 'http://example.com/ArcGIS/rest/ImageServer/identify' yield self.check_endpoint, 'http://example.com/ArcGIS/rest/ImageServer', 'http://example.com/ArcGIS/rest/ImageServer/identify' yield self.check_endpoint, 'http://example.com/ArcGIS/rest/ImageServer/export', 'http://example.com/ArcGIS/rest/ImageServer/identify' yield self.check_endpoint, 'http://example.com/ArcGIS/rest/ImageServer/exportImage', 'http://example.com/ArcGIS/rest/ImageServer/identify' yield self.check_endpoint, 'http://example.com/ArcGIS/rest/MapServer/export?param=foo', 'http://example.com/ArcGIS/rest/MapServer/identify?param=foo' yield self.check_endpoint, 'http://example.com/ArcGIS/rest/ImageServer/export?param=foo', 'http://example.com/ArcGIS/rest/ImageServer/identify?param=foo' class TestRequest(object): def setup(self): self.env = { 'HTTP_HOST': 'localhost:5050', 'PATH_INFO': '/service', 'QUERY_STRING': 'LAYERS=osm_mapnik&FORMAT=image%2Fpng&SPHERICALMERCATOR=true&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&STYLES=&EXCEPTIONS=application%2Fvnd.ogc.se_inimage&SRS=EPSG%3A900913&bbox=1013566.9382067363,7051939.297837454,1030918.1436243634,7069577.142111099&WIDTH=908&HEIGHT=923', 'REMOTE_ADDR': '127.0.0.1', 'REQUEST_METHOD': 'GET', 'SCRIPT_NAME': '', 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '5050', 'SERVER_PROTOCOL': 'HTTP/1.1', 'wsgi.url_scheme': 'http', } def test_path(self): req = Request(self.env) assert req.path == '/service' def test_host_url(self): req = Request(self.env) assert req.host_url == 'http://localhost:5050/' def test_base_url(self): req = Request(self.env) assert req.base_url == 'http://localhost:5050/service' del self.env['HTTP_HOST'] req = Request(self.env) assert req.base_url == 'http://127.0.0.1:5050/service' self.env['SERVER_PORT'] = '80' req = Request(self.env) assert req.base_url == 'http://127.0.0.1/service' def test_query_string(self): self.env['QUERY_STRING'] = 'Foo=boo&baz=baa&fOO=bizz' req = Request(self.env) print(req.args['foo']) assert req.args.get_all('foo') == ['boo', 'bizz'] def test_query_string_encoding(self): env = { 'QUERY_STRING': 'foo=some%20special%20chars%20%26%20%3D' } req = Request(env) print(req.args['foo']) assert req.args['foo'] == u'some special chars & =' def test_script_url(self): req = Request(self.env) eq_(req.script_url, 'http://localhost:5050') self.env['SCRIPT_NAME'] = '/' req = Request(self.env) eq_(req.script_url, 'http://localhost:5050') self.env['SCRIPT_NAME'] = '/proxy' req = Request(self.env) eq_(req.script_url, 'http://localhost:5050/proxy') self.env['SCRIPT_NAME'] = '/proxy/' req = Request(self.env) eq_(req.script_url, 'http://localhost:5050/proxy') def test_pop_path(self): self.env['PATH_INFO'] = '/foo/service' req = Request(self.env) part = req.pop_path() eq_(part, 'foo') eq_(self.env['PATH_INFO'], '/service') eq_(self.env['SCRIPT_NAME'], '/foo') part = req.pop_path() eq_(part, 'service') eq_(self.env['PATH_INFO'], '') eq_(self.env['SCRIPT_NAME'], '/foo/service') part = req.pop_path() eq_(part, '') eq_(self.env['PATH_INFO'], '') eq_(self.env['SCRIPT_NAME'], '/foo/service') def test_maprequest_from_request(): env = { 'QUERY_STRING': 'layers=bar&bBOx=-90,-80,70.0,+80&format=image/png&'\ 'WIdth=100&heIGHT=200&LAyerS=foo' } req = WMSMapRequest(param=Request(env).args) assert req.params.bbox == (-90.0, -80.0, 70.0, 80.0) assert req.params.layers == ['bar', 'foo'] assert req.params.size == (100, 200) class TestWMSMapRequestParams(object): def setup(self): self.m = WMSMapRequestParams(url_decode('layers=bar&bBOx=-90,-80,70.0, 80&format=image/png' '&WIdth=100&heIGHT=200&LAyerS=foo&srs=EPSG%3A0815')) def test_empty(self): m = WMSMapRequestParams() assert m.query_string == '' def test_size(self): assert self.m.size == (100, 200) self.m.size = (250, 350) assert self.m.size == (250, 350) assert self.m['width'] == '250' assert self.m['height'] == '350' del self.m['width'] assert self.m.size == None def test_format(self): assert self.m.format == 'png' assert self.m.format_mime_type == 'image/png' self.m['transparent'] = 'True' assert self.m.format == 'png' def test_bbox(self): assert self.m.bbox == (-90.0, -80.0, 70.0, 80.0) del self.m['bbox'] assert self.m.bbox is None self.m.bbox = (-90.0, -80.0, 70.0, 80.0) assert self.m.bbox == (-90.0, -80.0, 70.0, 80.0) self.m.bbox = '0.0, -40.0, 70.0, 80.0' assert self.m.bbox == (0.0, -40.0, 70.0, 80.0) self.m.bbox = None assert self.m.bbox is None def test_transparent(self): assert self.m.transparent == False self.m['transparent'] = 'trUe' assert self.m.transparent == True def test_transparent_bool(self): self.m['transparent'] = True assert self.m['transparent'] == 'True' def test_bgcolor(self): assert self.m.bgcolor == '#ffffff' self.m['bgcolor'] = '0x42cafe' assert self.m.bgcolor == '#42cafe' def test_srs(self): print(self.m.srs) assert self.m.srs == 'EPSG:0815' del self.m['srs'] assert self.m.srs is None self.m.srs = SRS('EPSG:4326') assert self.m.srs == 'EPSG:4326' def test_layers(self): assert list(self.m.layers) == ['bar', 'foo'] def test_query_string(self): assert_query_eq(self.m.query_string, 'layers=bar,foo&WIdth=100&bBOx=-90,-80,70.0,+80' '&format=image%2Fpng&srs=EPSG%3A0815&heIGHT=200') def test_query_string_encoding(self): m = WMSMapRequestParams() m.layers = ["layer with whitespace", u"layer with ümlauts"] eq_(m.query_string, 'layers=layer%20with%20whitespace,layer%20with%20%C3%BCmlauts') def test_get(self): assert self.m.get('LAYERS') == 'bar' assert self.m.get('width', type_func=int) == 100 def test_set(self): self.m.set('Layers', 'baz', append=True) assert self.m.get('LAYERS') == 'bar' self.m.set('Layers', 'baz') assert self.m.get('LAYERS') == 'baz' def test_attr_access(self): assert self.m['width'] == '100' assert self.m['height'] == '200' try: self.m.invalid except AttributeError: pass else: assert False def test_with_defaults(self): orig_req = WMSMapRequestParams(param=dict(layers='baz')) new_req = self.m.with_defaults(orig_req) assert new_req is not self.m assert self.m.get('LayErs') == 'bar' assert new_req.get('LAyers') == 'baz' assert new_req.size == (100, 200) class TestURLDecode(object): def test_key_decode(self): d = url_decode('white+space=in+key&foo=bar', decode_keys=True) assert d['white space'] == 'in key' assert d['foo'] == 'bar' def test_include_empty(self): d = url_decode('bar&foo=baz&bing', include_empty=True) assert d['bar'] == '' assert d['foo'] == 'baz' assert d['bing'] == '' def test_non_mime_format(): m = WMSMapRequest(param={'format': 'jpeg'}) assert m.params.format == 'jpeg' def test_request_w_url(): url = WMSMapRequest(url='http://localhost:8000/service?', param={'layers': 'foo,bar'}).complete_url assert_url_eq(url, 'http://localhost:8000/service?layers=foo,bar&styles=&request=GetMap&service=WMS') url = WMSMapRequest(url='http://localhost:8000/service', param={'layers': 'foo,bar'}).complete_url assert_url_eq(url, 'http://localhost:8000/service?layers=foo,bar&styles=&request=GetMap&service=WMS') url = WMSMapRequest(url='http://localhost:8000/service?map=foo', param={'layers': 'foo,bar'}).complete_url assert_url_eq(url, 'http://localhost:8000/service?map=foo&layers=foo,bar&styles=&request=GetMap&service=WMS') class TestWMSRequest(object): env = make_wsgi_env("""LAYERS=foo&FORMAT=image%2Fjpeg&SERVICE=WMS&VERSION=1.1.1& REQUEST=GetMap&STYLES=&EXCEPTIONS=application%2Fvnd.ogc.se_xml&SRS=EPSG%3A900913& BBOX=8,4,9,5&WIDTH=984&HEIGHT=708""".replace('\n', '')) def setup(self): self.req = Request(self.env) def test_valid_request(self): map_req = wms_request(self.req) # constructor validates assert map_req.params.size == (984, 708) def test_invalid_request(self): del self.req.args['request'] try: wms_request(self.req) except RequestError as e: assert 'request' in e.msg else: assert False, 'RequestError expected' def test_exception_handler(self): map_req = wms_request(self.req) assert isinstance(map_req.exception_handler, WMS111ExceptionHandler) def test_image_exception_handler(self): self.req.args['exceptions'] = 'application/vnd.ogc.se_inimage' map_req = wms_request(self.req) assert isinstance(map_req.exception_handler, WMSImageExceptionHandler) def test_blank_exception_handler(self): self.req.args['exceptions'] = 'blank' map_req = wms_request(self.req) assert isinstance(map_req.exception_handler, WMSBlankExceptionHandler) class TestSRSAxisOrder(object): def setup(self): params111 = url_decode("""LAYERS=foo&FORMAT=image%2Fjpeg&SERVICE=WMS& VERSION=1.1.1&REQUEST=GetMap&STYLES=&EXCEPTIONS=application%2Fvnd.ogc.se_xml& SRS=EPSG%3A4326&BBOX=8,4,9,5&WIDTH=984&HEIGHT=708""".replace('\n', '')) self.req111 = WMS111MapRequest(params111) self.params130 = params111.copy() self.params130['version'] = '1.3.0' self.params130['crs'] = self.params130['srs'] del self.params130['srs'] def test_111_order(self): eq_(self.req111.params.bbox, (8, 4, 9, 5)) def test_130_order_geog(self): req130 = WMS130MapRequest(self.params130) eq_(req130.params.bbox, (4, 8, 5, 9)) self.params130['crs'] = 'EPSG:4258' req130 = WMS130MapRequest(self.params130) eq_(req130.params.bbox, (4, 8, 5, 9)) def test_130_order_geog_old(self): self.params130['crs'] = 'CRS:84' req130 = WMS130MapRequest(self.params130) eq_(req130.params.bbox, (8, 4, 9, 5)) def test_130_order_proj_north_east(self): self.params130['crs'] = 'EPSG:31466' req130 = WMS130MapRequest(self.params130) eq_(req130.params.bbox, (4, 8, 5, 9)) def test_130_order_proj(self): self.params130['crs'] = 'EPSG:31463' req130 = WMS130MapRequest(self.params130) eq_(req130.params.bbox, (8, 4, 9, 5)) class TestTileRequest(object): def test_tms_request(self): env = { 'PATH_INFO': '/tms/1.0.0/osm/5/2/3.png', 'QUERY_STRING': '', } req = Request(env) tms = tile_request(req) assert isinstance(tms, TMSRequest) eq_(tms.tile, (2, 3, 5)) eq_(tms.format, 'png') eq_(tms.layer, 'osm') eq_(tms.dimensions, {}) def test_tile_request(self): env = { 'PATH_INFO': '/tiles/1.0.0/osm/5/2/3.png', 'QUERY_STRING': '', } req = Request(env) tile_req = tile_request(req) assert isinstance(tile_req, TileRequest) eq_(tile_req.tile, (2, 3, 5)) eq_(tile_req.origin, None) eq_(tile_req.format, 'png') eq_(tile_req.layer, 'osm') eq_(tile_req.dimensions, {}) def test_tile_request_flipped_y(self): env = { 'PATH_INFO': '/tiles/1.0.0/osm/5/2/3.png', 'QUERY_STRING': 'origin=nw', } req = Request(env) tile_req = tile_request(req) assert isinstance(tile_req, TileRequest) eq_(tile_req.tile, (2, 3, 5)) # not jet flipped eq_(tile_req.origin, 'nw') eq_(tile_req.format, 'png') eq_(tile_req.layer, 'osm') eq_(tile_req.dimensions, {}) def test_tile_request_w_epsg(self): env = { 'PATH_INFO': '/tiles/1.0.0/osm/EPSG4326/5/2/3.png', 'QUERY_STRING': '', } req = Request(env) tile_req = tile_request(req) assert isinstance(tile_req, TileRequest) eq_(tile_req.tile, (2, 3, 5)) eq_(tile_req.format, 'png') eq_(tile_req.layer, 'osm') eq_(tile_req.dimensions, {'_layer_spec': 'EPSG4326'}) def test_request_params_pickle(): params = RequestParams(dict(foo='bar', zing='zong')) params2 = pickle.loads(pickle.dumps(params, 2)) assert params.params == params2.params ����������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_request_wmts.py���������������������������������������������0000664�0000000�0000000�00000006133�13204544724�0023451�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.request.wmts import wmts_request, WMTS100CapabilitiesRequest from mapproxy.request.wmts import URLTemplateConverter, InvalidWMTSTemplate from mapproxy.request.base import url_decode from nose.tools import eq_, raises def dummy_req(url): return DummyRequest(url_decode(url.replace('\n', ''))) class DummyRequest(object): def __init__(self, args, url=''): self.args = args self.base_url = url def test_tile_request(): url = '''requeST=GetTile&service=wmts&tileMatrixset=EPSG900913& tilematrix=2&tileROW=4&TILECOL=2&FORMAT=image/png&Style=&layer=Foo&version=1.0.0''' req = wmts_request(dummy_req(url)) eq_(req.params.coord, (2, 4, '2')) eq_(req.params.layer, 'Foo') eq_(req.params.format, 'png') eq_(req.params.tilematrixset, 'EPSG900913') def test_capabilities_request(): url = '''requeST=GetCapabilities&service=wmts''' req = wmts_request(dummy_req(url)) assert isinstance(req, WMTS100CapabilitiesRequest) def test_template_converter(): regexp = URLTemplateConverter('/{Layer}/{Style}/{TileMatrixSet}-{TileMatrix}-{TileCol}-{TileRow}/tile').regexp() match = regexp.match('/test/bar/foo-EPSG4326-4-12-99/tile') assert match assert match.groupdict()['Layer'] == 'test' assert match.groupdict()['TileMatrixSet'] == 'foo-EPSG4326' assert match.groupdict()['TileMatrix'] == '4' assert match.groupdict()['TileCol'] == '12' assert match.groupdict()['TileRow'] == '99' assert match.groupdict()['Style'] == 'bar' def test_template_converter_deprecated_format(): # old format that doesn't match the WMTS spec, now deprecated regexp = URLTemplateConverter('/{{Layer}}/{{Style}}/{{TileMatrixSet}}-{{TileMatrix}}-{{TileCol}}-{{TileRow}}/tile').regexp() match = regexp.match('/test/bar/foo-EPSG4326-4-12-99/tile') assert match assert match.groupdict()['Layer'] == 'test' assert match.groupdict()['TileMatrixSet'] == 'foo-EPSG4326' assert match.groupdict()['TileMatrix'] == '4' assert match.groupdict()['TileCol'] == '12' assert match.groupdict()['TileRow'] == '99' assert match.groupdict()['Style'] == 'bar' @raises(InvalidWMTSTemplate) def test_template_converter_missing_vars(): URLTemplateConverter('/wmts/{Style}/{TileMatrixSet}/{TileCol}.png').regexp() def test_template_converter_dimensions(): converter = URLTemplateConverter('/{Layer}/{Dim1}/{Dim2}/{TileMatrixSet}-{TileMatrix}-{TileCol}-{TileRow}/tile') assert converter.dimensions == ['Dim1', 'Dim2'] �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_response.py�������������������������������������������������0000664�0000000�0000000�00000005517�13204544724�0022552�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from io import BytesIO from mapproxy.test.helper import Mocker from mapproxy.test.mocker import ANY from mapproxy.response import Response from mapproxy.compat import string_type class TestResponse(Mocker): def test_str_response(self): resp = Response('string content') assert isinstance(resp.response, string_type) start_response = self.mock() self.expect(start_response('200 OK', ANY)) self.replay() result = resp({'REQUEST_METHOD': 'GET'}, start_response) assert next(result) == b'string content' def test_itr_response(self): resp = Response(iter(['string content', 'as iterable'])) assert hasattr(resp.response, 'next') or hasattr(resp.response, '__next__') start_response = self.mock() self.expect(start_response('200 OK', ANY)) self.replay() result = resp({'REQUEST_METHOD': 'GET'}, start_response) assert next(result) == 'string content' assert next(result) == 'as iterable' def test_file_response(self): data = BytesIO(b'foobar') resp = Response(data) assert resp.response == data start_response = self.mock() self.expect(start_response('200 OK', ANY)) self.replay() result = resp({'REQUEST_METHOD': 'GET'}, start_response) assert next(result) == b'foobar' def test_file_response_w_file_wrapper(self): data = BytesIO(b'foobar') resp = Response(data) assert resp.response == data start_response = self.mock() self.expect(start_response('200 OK', ANY)) file_wrapper = self.mock() self.expect(file_wrapper(data, resp.block_size)).result('DUMMY') self.replay() result = resp({'REQUEST_METHOD': 'GET', 'wsgi.file_wrapper': file_wrapper}, start_response) assert result == 'DUMMY' def test_file_response_content_length(self): data = BytesIO(b'*' * 342) resp = Response(data) assert resp.response == data start_response = self.mock() self.expect(start_response('200 OK', ANY)) self.replay() resp({'REQUEST_METHOD': 'GET'}, start_response) assert resp.content_length == 342 ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_seed.py�����������������������������������������������������0000664�0000000�0000000�00000031275�13204544724�0021634�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of the MapProxy project. # Copyright (C) 2010-2012 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import os import time try: import cPickle as pickle except ImportError: import pickle from mapproxy.seed.seeder import TileWalker, SeedTask, SeedProgress from mapproxy.cache.dummy import DummyLocker from mapproxy.cache.tile import TileManager from mapproxy.source.tile import TiledSource from mapproxy.grid import tile_grid_for_epsg from mapproxy.grid import TileGrid from mapproxy.srs import SRS from mapproxy.util.coverage import BBOXCoverage, GeomCoverage from mapproxy.seed.config import before_timestamp_from_options, SeedConfigurationError from mapproxy.seed.config import LevelsList, LevelsRange, LevelsResolutionList, LevelsResolutionRange from mapproxy.seed.util import ProgressStore from mapproxy.test.helper import TempFile from collections import defaultdict from nose.tools import eq_, assert_almost_equal, raises from nose.plugins.skip import SkipTest try: from shapely.wkt import loads as load_wkt load_wkt # prevent lint warning except ImportError: load_wkt = None class MockSeedPool(object): def __init__(self): self.seeded_tiles = defaultdict(set) def process(self, tiles, progess): for x, y, level in tiles: self.seeded_tiles[level].add((x, y)) class MockCache(object): def is_cached(self, tile): return False class TestSeeder(object): def setup(self): self.grid = TileGrid(SRS(4326), bbox=[-180, -90, 180, 90]) self.source = TiledSource(self.grid, None) self.tile_mgr = TileManager(self.grid, MockCache(), [self.source], 'png', locker=DummyLocker()) self.seed_pool = MockSeedPool() def make_bbox_task(self, bbox, srs, levels): md = dict(name='', cache_name='', grid_name='') coverage = BBOXCoverage(bbox, srs) return SeedTask(md, self.tile_mgr, levels, refresh_timestamp=None, coverage=coverage) def make_geom_task(self, geom, srs, levels): md = dict(name='', cache_name='', grid_name='') coverage = GeomCoverage(geom, srs) return SeedTask(md, self.tile_mgr, levels, refresh_timestamp=None, coverage=coverage) def test_seed_full_bbox(self): task = self.make_bbox_task([-180, -90, 180, 90], SRS(4326), [0, 1, 2]) seeder = TileWalker(task, self.seed_pool, handle_uncached=True) seeder.walk() eq_(len(self.seed_pool.seeded_tiles), 3) eq_(self.seed_pool.seeded_tiles[0], set([(0, 0)])) eq_(self.seed_pool.seeded_tiles[1], set([(0, 0), (1, 0)])) eq_(self.seed_pool.seeded_tiles[2], set([(0, 0), (1, 0), (2, 0), (3, 0), (0, 1), (1, 1), (2, 1), (3, 1)])) def test_seed_small_bbox(self): task = self.make_bbox_task([-45, 0, 180, 90], SRS(4326), [0, 1, 2]) seeder = TileWalker(task, self.seed_pool, handle_uncached=True) seeder.walk() eq_(len(self.seed_pool.seeded_tiles), 3) eq_(self.seed_pool.seeded_tiles[0], set([(0, 0)])) eq_(self.seed_pool.seeded_tiles[1], set([(0, 0), (1, 0)])) eq_(self.seed_pool.seeded_tiles[2], set([(1, 1), (2, 1), (3, 1)])) def test_seed_small_bbox_iregular_levels(self): task = self.make_bbox_task([-45, 0, 180, 90], SRS(4326), [0, 2]) seeder = TileWalker(task, self.seed_pool, handle_uncached=True) seeder.walk() eq_(len(self.seed_pool.seeded_tiles), 2) eq_(self.seed_pool.seeded_tiles[0], set([(0, 0)])) eq_(self.seed_pool.seeded_tiles[2], set([(1, 1), (2, 1), (3, 1)])) def test_seed_small_bbox_transformed(self): bbox = SRS(4326).transform_bbox_to(SRS(900913), [-45, 0, 179, 80]) task = self.make_bbox_task(bbox, SRS(900913), [0, 1, 2]) seeder = TileWalker(task, self.seed_pool, handle_uncached=True) seeder.walk() eq_(len(self.seed_pool.seeded_tiles), 3) eq_(self.seed_pool.seeded_tiles[0], set([(0, 0)])) eq_(self.seed_pool.seeded_tiles[1], set([(0, 0), (1, 0)])) eq_(self.seed_pool.seeded_tiles[2], set([(1, 1), (2, 1), (3, 1)])) def test_seed_with_geom(self): if not load_wkt: raise SkipTest('no shapely installed') # box from 10 10 to 80 80 with small spike/corner to -10 60 (upper left) geom = load_wkt("POLYGON((10 10, 10 50, -10 60, 10 80, 80 80, 80 10, 10 10))") task = self.make_geom_task(geom, SRS(4326), [0, 1, 2, 3, 4]) seeder = TileWalker(task, self.seed_pool, handle_uncached=True) seeder.walk() eq_(len(self.seed_pool.seeded_tiles), 5) eq_(self.seed_pool.seeded_tiles[0], set([(0, 0)])) eq_(self.seed_pool.seeded_tiles[1], set([(0, 0), (1, 0)])) eq_(self.seed_pool.seeded_tiles[2], set([(1, 1), (2, 1)])) eq_(self.seed_pool.seeded_tiles[3], set([(4, 2), (5, 2), (4, 3), (5, 3), (3, 3)])) eq_(len(self.seed_pool.seeded_tiles[4]), 4*4+2) def test_seed_with_res_list(self): if not load_wkt: raise SkipTest('no shapely installed') # box from 10 10 to 80 80 with small spike/corner to -10 60 (upper left) geom = load_wkt("POLYGON((10 10, 10 50, -10 60, 10 80, 80 80, 80 10, 10 10))") self.grid = TileGrid(SRS(4326), bbox=[-180, -90, 180, 90], res=[360/256, 360/720, 360/2000, 360/5000, 360/8000]) self.tile_mgr = TileManager(self.grid, MockCache(), [self.source], 'png', locker=DummyLocker()) task = self.make_geom_task(geom, SRS(4326), [0, 1, 2, 3, 4]) seeder = TileWalker(task, self.seed_pool, handle_uncached=True) seeder.walk() eq_(len(self.seed_pool.seeded_tiles), 5) eq_(self.seed_pool.seeded_tiles[0], set([(0, 0)])) eq_(self.grid.grid_sizes[1], (3, 2)) eq_(self.seed_pool.seeded_tiles[1], set([(1, 0), (1, 1), (2, 0), (2, 1)])) eq_(self.grid.grid_sizes[2], (8, 4)) eq_(self.seed_pool.seeded_tiles[2], set([(4, 2), (5, 2), (4, 3), (5, 3), (3, 3)])) eq_(self.grid.grid_sizes[3], (20, 10)) eq_(len(self.seed_pool.seeded_tiles[3]), 5*5+2) def test_seed_full_bbox_continue(self): task = self.make_bbox_task([-180, -90, 180, 90], SRS(4326), [0, 1, 2]) seed_progress = SeedProgress([(0, 1), (1, 2)]) seeder = TileWalker(task, self.seed_pool, handle_uncached=True, seed_progress=seed_progress) seeder.walk() eq_(len(self.seed_pool.seeded_tiles), 3) eq_(self.seed_pool.seeded_tiles[0], set([(0, 0)])) eq_(self.seed_pool.seeded_tiles[1], set([(0, 0), (1, 0)])) eq_(self.seed_pool.seeded_tiles[2], set([(2, 0), (3, 0), (2, 1), (3, 1)])) class TestLevels(object): def test_level_list(self): levels = LevelsList([-10, 3, 1, 3, 5, 7, 50]) eq_(levels.for_grid(tile_grid_for_epsg(4326)), [1, 3, 5, 7]) def test_level_range(self): levels = LevelsRange([1, 5]) eq_(levels.for_grid(tile_grid_for_epsg(4326)), [1, 2, 3, 4, 5]) def test_level_range_open_from(self): levels = LevelsRange([None, 5]) eq_(levels.for_grid(tile_grid_for_epsg(4326)), [0, 1, 2, 3, 4, 5]) def test_level_range_open_to(self): levels = LevelsRange([13, None]) eq_(levels.for_grid(tile_grid_for_epsg(4326)), [13, 14, 15, 16, 17, 18, 19]) def test_level_range_open_tos_range(self): levels = LevelsResolutionRange([1000, 100]) eq_(levels.for_grid(tile_grid_for_epsg(900913)), [8, 9, 10, 11]) def test_res_range_open_from(self): levels = LevelsResolutionRange([None, 100]) eq_(levels.for_grid(tile_grid_for_epsg(900913)), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) def test_res_range_open_to(self): levels = LevelsResolutionRange([1000, None]) eq_(levels.for_grid(tile_grid_for_epsg(900913)), [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) def test_resolution_list(self): levels = LevelsResolutionList([1000, 100, 500]) eq_(levels.for_grid(tile_grid_for_epsg(900913)), [8, 9, 11]) class TestProgressStore(object): def test_load_empty(self): store = ProgressStore('doesnotexist.no_realy.txt') store.load() assert store.get(('foo', 'bar', 'baz')) == None def test_load_store(self): with TempFile(no_create=True) as tmp: with open(tmp, 'wb') as f: f.write(pickle.dumps({("view", "cache", "grid"): [(0, 1), (2, 4)]})) store = ProgressStore(tmp) assert store.get(('view', 'cache', 'grid')) == [(0, 1), (2, 4)] assert store.get(('view', 'cache', 'grid2')) == None store.add(('view', 'cache', 'grid'), []) store.add(('view', 'cache', 'grid2'), [(0, 1)]) store.write() store = ProgressStore(tmp) assert store.get(('view', 'cache', 'grid')) == [] assert store.get(('view', 'cache', 'grid2')) == [(0, 1)] def test_load_broken(self): with TempFile(no_create=True) as tmp: with open(tmp, 'wb') as f: f.write(b'##invaliddata') f.write(pickle.dumps({("view", "cache", "grid"): [(0, 1), (2, 4)]})) store = ProgressStore(tmp) assert store.status == {} class TestRemovebreforeTimetamp(object): def test_from_time(self): ts = before_timestamp_from_options({'time': '2010-12-01T20:12:00'}) # we don't know the timezone this test will run assert (1291230720.0 - 14 * 3600) < ts < (1291230720.0 + 14 * 3600) def test_from_mtime(self): with TempFile() as tmp: os.utime(tmp, (12376512, 12376512)) eq_(before_timestamp_from_options({'mtime': tmp}), 12376512) @raises(SeedConfigurationError) def test_from_mtime_missing_file(self): before_timestamp_from_options({'mtime': '/tmp/does-not-exist-at-all,really'}) def test_from_empty(self): assert_almost_equal( before_timestamp_from_options({}), time.time(), -1 ) def test_from_delta(self): assert_almost_equal( before_timestamp_from_options({'minutes': 15}) + 60 * 15, time.time(), -1 ) class TestSeedProgress(object): def test_progress_identifier(self): old = SeedProgress() with old.step_down(0, 2): with old.step_down(0, 4): eq_(old.current_progress_identifier(), [(0, 2), (0, 4)]) # previous leafs are still present eq_(old.current_progress_identifier(), [(0, 2), (0, 4)]) with old.step_down(1, 4): eq_(old.current_progress_identifier(), [(0, 2), (1, 4)]) eq_(old.current_progress_identifier(), [(0, 2), (1, 4)]) eq_(old.current_progress_identifier(), []) # empty list after seed with old.step_down(1, 2): eq_(old.current_progress_identifier(), [(1, 2)]) with old.step_down(0, 4): with old.step_down(1, 4): eq_(old.current_progress_identifier(), [(1, 2), (0, 4), (1, 4)]) def test_already_processed(self): new = SeedProgress([(0, 2)]) with new.step_down(0, 2): assert not new.already_processed() with new.step_down(0, 2): assert not new.already_processed() new = SeedProgress([(1, 2)]) with new.step_down(0, 2): assert new.already_processed() with new.step_down(0, 2): assert new.already_processed() new = SeedProgress([(0, 2), (1, 4), (2, 4)]) with new.step_down(0, 2): assert not new.already_processed() with new.step_down(0, 4): assert new.already_processed() with new.step_down(1, 4): assert not new.already_processed() with new.step_down(1, 4): assert new.already_processed() with new.step_down(2, 4): assert not new.already_processed() with new.step_down(3, 4): assert not new.already_processed() with new.step_down(2, 4): assert not new.already_processed() �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_seed_cachelock.py�������������������������������������������0000664�0000000�0000000�00000005221�13204544724�0023620�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of the MapProxy project. # Copyright (C) 2012 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import multiprocessing import os import shutil import tempfile import time from mapproxy.seed.cachelock import CacheLocker, CacheLockedError class TestCacheLock(object): def setup(self): self.tmp_dir = tempfile.mkdtemp() self.lock_file = os.path.join(self.tmp_dir, 'lock') def teardown(self): shutil.rmtree(self.tmp_dir) def test_free_lock(self): locker = CacheLocker(self.lock_file) with locker.lock('foo'): assert True def test_locked_by_process_no_block(self): proc_is_locked = multiprocessing.Event() def lock(): locker = CacheLocker(self.lock_file) with locker.lock('foo'): proc_is_locked.set() time.sleep(10) p = multiprocessing.Process(target=lock) p.start() # wait for process to start proc_is_locked.wait() locker = CacheLocker(self.lock_file) # test unlocked bar with locker.lock('bar', no_block=True): assert True # test locked foo try: with locker.lock('foo', no_block=True): assert False except CacheLockedError: pass finally: p.terminate() p.join() def test_locked_by_process_waiting(self): proc_is_locked = multiprocessing.Event() def lock(): locker = CacheLocker(self.lock_file) with locker.lock('foo'): proc_is_locked.set() time.sleep(.1) p = multiprocessing.Process(target=lock) start_time = time.time() p.start() # wait for process to start proc_is_locked.wait() locker = CacheLocker(self.lock_file, polltime=0.02) try: with locker.lock('foo', no_block=False): diff = time.time() - start_time assert diff > 0.1 finally: p.terminate() p.join()�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_srs.py������������������������������������������������������0000664�0000000�0000000�00000004735�13204544724�0021524�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os from mapproxy.config import base_config from mapproxy import srs from mapproxy.srs import SRS class Test_0_ProjDefaultDataPath(object): def test_known_srs(self): srs.SRS(4326) def test_unknown_srs(self): try: srs.SRS(1234) except RuntimeError: pass else: assert False, 'RuntimeError expected' class Test_1_ProjDataPath(object): def setup(self): srs._proj_initalized = False srs._srs_cache = {} base_config().srs.proj_data_dir = os.path.dirname(__file__) def test_dummy_srs(self): srs.SRS(1234) def test_unknown_srs(self): try: srs.SRS(2339) except RuntimeError: pass else: assert False, 'RuntimeError expected' def teardown(self): srs._proj_initalized = False srs._srs_cache = {} base_config().srs.proj_data_dir = None class TestSRS(object): def test_epsg4326(self): srs = SRS(4326) assert srs.is_latlong assert not srs.is_axis_order_en assert srs.is_axis_order_ne def test_crs84(self): srs = SRS('CRS:84') assert srs.is_latlong assert srs.is_axis_order_en assert not srs.is_axis_order_ne assert srs == SRS('EPSG:4326') def test_epsg31467(self): srs = SRS('EPSG:31467') assert not srs.is_latlong assert not srs.is_axis_order_en assert srs.is_axis_order_ne def test_epsg900913(self): srs = SRS('epsg:900913') assert not srs.is_latlong assert srs.is_axis_order_en assert not srs.is_axis_order_ne def test_from_srs(self): srs1 = SRS('epgs:4326') srs2 = SRS(srs1) assert srs1 == srs2 �����������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_tiled_source.py���������������������������������������������0000664�0000000�0000000�00000007010�13204544724�0023363�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of the MapProxy project. # Copyright (C) 2012 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.client.tile import TMSClient from mapproxy.grid import TileGrid from mapproxy.srs import SRS from mapproxy.source.tile import TiledSource from mapproxy.source.error import HTTPSourceErrorHandler from mapproxy.layer import MapQuery from mapproxy.test.http import mock_httpd from nose.tools import eq_ TEST_SERVER_ADDRESS = ('127.0.0.1', 56413) TESTSERVER_URL = 'http://%s:%d' % TEST_SERVER_ADDRESS class TestTileClientOnError(object): def setup(self): self.grid = TileGrid(SRS(4326), bbox=[-180, -90, 180, 90]) self.client = TMSClient(TESTSERVER_URL) def test_cacheable_response(self): error_handler = HTTPSourceErrorHandler() error_handler.add_handler(500, (255, 0, 0), cacheable=True) self.source = TiledSource(self.grid, self.client, error_handler=error_handler) with mock_httpd(TEST_SERVER_ADDRESS, [({'path': '/1/0/0.png'}, {'body': b'error', 'status': 500, 'headers':{'content-type': 'text/plain'}})]): resp = self.source.get_map(MapQuery([-180, -90, 0, 90], (256, 256), SRS(4326), format='png')) assert resp.cacheable eq_(resp.as_image().getcolors(), [((256*256), (255, 0, 0))]) def test_image_response(self): error_handler = HTTPSourceErrorHandler() error_handler.add_handler(500, (255, 0, 0), cacheable=False) self.source = TiledSource(self.grid, self.client, error_handler=error_handler) with mock_httpd(TEST_SERVER_ADDRESS, [({'path': '/1/0/0.png'}, {'body': b'error', 'status': 500, 'headers':{'content-type': 'text/plain'}})]): resp = self.source.get_map(MapQuery([-180, -90, 0, 90], (256, 256), SRS(4326), format='png')) assert not resp.cacheable eq_(resp.as_image().getcolors(), [((256*256), (255, 0, 0))]) def test_multiple_image_responses(self): error_handler = HTTPSourceErrorHandler() error_handler.add_handler(500, (255, 0, 0), cacheable=False) error_handler.add_handler(204, (255, 0, 127, 200), cacheable=True) self.source = TiledSource(self.grid, self.client, error_handler=error_handler) with mock_httpd(TEST_SERVER_ADDRESS, [ ({'path': '/1/0/0.png'}, {'body': b'error', 'status': 500, 'headers':{'content-type': 'text/plain'}}), ({'path': '/1/0/0.png'}, {'body': b'error', 'status': 204, 'headers':{'content-type': 'text/plain'}})]): resp = self.source.get_map(MapQuery([-180, -90, 0, 90], (256, 256), SRS(4326), format='png')) assert not resp.cacheable eq_(resp.as_image().getcolors(), [((256*256), (255, 0, 0))]) resp = self.source.get_map(MapQuery([-180, -90, 0, 90], (256, 256), SRS(4326), format='png')) assert resp.cacheable eq_(resp.as_image().getcolors(), [((256*256), (255, 0, 127, 200))]) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_tilefilter.py�����������������������������������������������0000664�0000000�0000000�00000002453�13204544724�0023053�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of the MapProxy project. # Copyright (C) 2010, 2011 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.tilefilter import tile_watermark_placement def test_tile_watermark_placement(): from nose.tools import eq_ eq_(tile_watermark_placement((0, 0, 0)), 'c') eq_(tile_watermark_placement((1, 0, 0)), 'c') eq_(tile_watermark_placement((0, 1, 0)), 'b') eq_(tile_watermark_placement((1, 1, 0)), 'b') eq_(tile_watermark_placement((0, 0, 0), True), None) eq_(tile_watermark_placement((1, 0, 0), True), 'c') eq_(tile_watermark_placement((2, 0, 0), True), None) eq_(tile_watermark_placement((0, 1, 0), True), 'c') eq_(tile_watermark_placement((1, 1, 0), True), None) eq_(tile_watermark_placement((2, 1, 0), True), 'c') ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_times.py����������������������������������������������������0000664�0000000�0000000�00000000562�13204544724�0022030�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from mapproxy.util.times import timestamp_from_isodate def test_timestamp_from_isodate(): ts = timestamp_from_isodate('2009-06-09T10:57:00') assert (1244537820.0 - 14 * 3600) < ts < (1244537820.0 + 14 * 3600) try: timestamp_from_isodate('2009-06-09T10:57') except ValueError: pass else: assert False, 'expected ValueError' ����������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_timeutils.py������������������������������������������������0000664�0000000�0000000�00000003334�13204544724�0022726�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from datetime import datetime from mapproxy.util.times import parse_httpdate, format_httpdate, timestamp from nose.tools import eq_, raises class TestHTTPDate(object): def test_parse_httpdate(self): for date in ( 'Fri, 13 Feb 2009 23:31:30 GMT', 'Friday, 13-Feb-09 23:31:30 GMT', 'Fri Feb 13 23:31:30 2009', ): eq_(parse_httpdate(date), 1234567890) def test_parse_invalid(self): for date in ( None, 'foobar', '4823764923', 'Fri, 13 Foo 2009 23:31:30 GMT' ): eq_(parse_httpdate(date), None) def test_format_httpdate(self): eq_(format_httpdate(datetime.fromtimestamp(1234567890)), 'Fri, 13 Feb 2009 23:31:30 GMT') eq_(format_httpdate(1234567890), 'Fri, 13 Feb 2009 23:31:30 GMT') @raises(AssertionError) def test_format_invalid(self): format_httpdate('foobar') def test_timestamp(): eq_(timestamp(1234567890), 1234567890) eq_(timestamp(datetime.fromtimestamp(1234567890)), 1234567890) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_util_conf_utils.py������������������������������������������0000664�0000000�0000000�00000004662�13204544724�0024116�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -:- encoding: utf-8 -:- # This file is part of the MapProxy project. # Copyright (C) 2013 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.script.conf.utils import update_config from copy import deepcopy from nose.tools import eq_ class TestUpdateConfig(object): def test_empty(self): a = {'a': 'foo', 'b': 42} b = {} eq_(update_config(deepcopy(a), b), a) def test_add(self): a = {'a': 'foo', 'b': 42} b = {'c': [1, 2, 3]} eq_(update_config(a, b), {'a': 'foo', 'b': 42, 'c': [1, 2, 3]}) def test_mod(self): a = {'a': 'foo', 'b': 42, 'c': {}} b = {'a': [1, 2, 3], 'c': 1} eq_(update_config(a, b), {'b': 42, 'a': [1, 2, 3], 'c': 1}) def test_nested_add_mod(self): a = {'a': 'foo', 'b': {'ba': 42, 'bb': {}}} b = {'b': {'bb': {'bba': 1}, 'bc': [1, 2, 3]}} eq_(update_config(a, b), {'a': 'foo', 'b': {'ba': 42, 'bb': {'bba': 1}, 'bc': [1, 2, 3]}}) def test_add_all(self): a = {'a': 'foo', 'b': {'ba': 42, 'bb': {}}} b = {'__all__': {'ba': 1}} eq_(update_config(a, b), {'a': {'ba': 1}, 'b': {'ba': 1, 'bb': {}}}) def test_extend(self): a = {'a': 'foo', 'b': ['ba']} b = {'b__extend__': ['bb', 'bc']} eq_(update_config(a, b), {'a': 'foo', 'b': ['ba', 'bb', 'bc']}) def test_prefix_wildcard(self): a = {'test_foo': 'foo', 'test_bar': 'ba', 'test2_foo': 'test2', 'nounderfoo': 1} b = {'____foo': 42} eq_(update_config(a, b), {'test_foo': 42, 'test_bar': 'ba', 'test2_foo': 42, 'nounderfoo': 1}) def test_suffix_wildcard(self): a = {'test_foo': 'foo', 'test_bar': 'ba', 'test2_foo': 'test2', 'nounderfoo': 1} b = {'test____': 42} eq_(update_config(a, b), {'test_foo': 42, 'test_bar': 42, 'test2_foo': 'test2', 'nounderfoo': 1}) ������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_utils.py����������������������������������������������������0000664�0000000�0000000�00000033617�13204544724�0022056�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of the MapProxy project. # Copyright (C) 2010-2013 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import glob import sys import shutil import tempfile import threading import multiprocessing import random import time from mapproxy.util.lock import ( FileLock, SemLock, cleanup_lockdir, LockTimeout, ) from mapproxy.util.fs import ( _force_rename_dir, swap_dir, cleanup_directory, write_atomic, ) from mapproxy.util.py import reraise_exception from mapproxy.util.times import timestamp_before from mapproxy.test.helper import Mocker from nose.tools import eq_ is_win = sys.platform == 'win32' class TestFileLock(Mocker): def setup(self): Mocker.setup(self) self.lock_dir = tempfile.mkdtemp() self.lock_file = os.path.join(self.lock_dir, 'lock.lck') def teardown(self): shutil.rmtree(self.lock_dir) Mocker.teardown(self) def test_file_lock_timeout(self): lock = self._create_lock() assert_locked(self.lock_file) lock # prevent lint warnings def test_file_lock(self): # Test a lock that becomes free during a waiting lock() call. class Lock(threading.Thread): def __init__(self, lock_file): threading.Thread.__init__(self) self.lock_file = lock_file self.lock = FileLock(self.lock_file) def run(self): self.lock.lock() time.sleep(0.2) self.lock.unlock() lock_thread = Lock(self.lock_file) start_time = time.time() lock_thread.start() # wait until thread got the locked while not lock_thread.lock._locked: time.sleep(0.001) # one lock that times out assert_locked(self.lock_file) # one lock that will get it after some time l = FileLock(self.lock_file, timeout=0.3, step=0.001) l.lock() locked_for = time.time() - start_time assert locked_for - 0.2 <=0.1, 'locking took to long?! (rerun if not sure)' #cleanup l.unlock() lock_thread.join() def test_lock_cleanup(self): old_lock_file = os.path.join(self.lock_dir, 'lock_old.lck') l = FileLock(old_lock_file) l.lock() l.unlock() mtime = os.stat(old_lock_file).st_mtime mtime -= 7*60 os.utime(old_lock_file, (mtime, mtime)) l = self._create_lock() l.unlock() assert os.path.exists(old_lock_file) assert os.path.exists(self.lock_file) cleanup_lockdir(self.lock_dir) assert not os.path.exists(old_lock_file) assert os.path.exists(self.lock_file) def test_concurrent_access(self): count_file = os.path.join(self.lock_dir, 'count.txt') with open(count_file, 'wb') as f: f.write(b'0') def count_up(): with FileLock(self.lock_file, timeout=60): with open(count_file, 'r+b') as f: counter = int(f.read().strip()) f.seek(0) f.write(str(counter+1).encode('utf-8')) def do_it(): for x in range(20): time.sleep(0.002) count_up() threads = [threading.Thread(target=do_it) for _ in range(20)] [t.start() for t in threads] [t.join() for t in threads] with open(count_file, 'r+b') as f: counter = int(f.read().strip()) assert counter == 400, counter def test_remove_on_unlock(self): l = FileLock(self.lock_file, remove_on_unlock=True) l.lock() assert os.path.exists(self.lock_file) l.unlock() assert not os.path.exists(self.lock_file) l.lock() assert os.path.exists(self.lock_file) os.remove(self.lock_file) assert not os.path.exists(self.lock_file) # ignore removed lock l.unlock() assert not os.path.exists(self.lock_file) def _create_lock(self): lock = FileLock(self.lock_file) lock.lock() return lock def assert_locked(lock_file, timeout=0.02, step=0.001): assert os.path.exists(lock_file) l = FileLock(lock_file, timeout=timeout, step=step) try: l.lock() assert False, 'file was not locked' except LockTimeout: pass class TestSemLock(object): def setup(self): self.lock_dir = tempfile.mkdtemp() self.lock_file = os.path.join(self.lock_dir, 'lock.lck') def teardown(self): shutil.rmtree(self.lock_dir) def count_lockfiles(self): return len(glob.glob(self.lock_file + '*')) def test_single(self): locks = [SemLock(self.lock_file, 1, timeout=0.01) for _ in range(2)] locks[0].lock() try: locks[1].lock() except LockTimeout: pass else: assert False, 'expected LockTimeout' def test_creating(self): locks = [SemLock(self.lock_file, 2) for _ in range(3)] eq_(self.count_lockfiles(), 0) locks[0].lock() eq_(self.count_lockfiles(), 1) locks[1].lock() eq_(self.count_lockfiles(), 2) assert os.path.exists(locks[0]._lock._path) assert os.path.exists(locks[1]._lock._path) locks[0].unlock() locks[2].lock() locks[2].unlock() locks[1].unlock() def test_timeout(self): locks = [SemLock(self.lock_file, 2, timeout=0.1) for _ in range(3)] eq_(self.count_lockfiles(), 0) locks[0].lock() eq_(self.count_lockfiles(), 1) locks[1].lock() eq_(self.count_lockfiles(), 2) try: locks[2].lock() except LockTimeout: pass else: assert False, 'expected LockTimeout' locks[0].unlock() locks[2].unlock() def test_load(self): locks = [SemLock(self.lock_file, 8, timeout=1) for _ in range(20)] new_locks = random.sample([l for l in locks if not l._locked], 5) for l in new_locks: l.lock() for _ in range(20): old_locks = random.sample([l for l in locks if l._locked], 3) for l in old_locks: l.unlock() eq_(len([l for l in locks if l._locked]), 2) eq_(len([l for l in locks if not l._locked]), 18) new_locks = random.sample([l for l in locks if not l._locked], 3) for l in new_locks: l.lock() eq_(len([l for l in locks if l._locked]), 5) eq_(len([l for l in locks if not l._locked]), 15) assert self.count_lockfiles() == 8 class DirTest(object): def setup(self): self.tmpdir = tempfile.mkdtemp() def teardown(self): if os.path.exists(self.tmpdir): shutil.rmtree(self.tmpdir) def mkdir(self, name): dirname = os.path.join(self.tmpdir, name) os.mkdir(dirname) self.mkfile(name, dirname=dirname) return dirname def mkfile(self, name, dirname=None): if dirname is None: dirname = self.mkdir(name) filename = os.path.join(dirname, name + '.txt') open(filename, 'wb').close() return filename class TestForceRenameDir(DirTest): def test_rename(self): src_dir = self.mkdir('bar') dst_dir = os.path.join(self.tmpdir, 'baz') _force_rename_dir(src_dir, dst_dir) assert os.path.exists(dst_dir) assert os.path.exists(os.path.join(dst_dir, 'bar.txt')) assert not os.path.exists(src_dir) def test_rename_overwrite(self): src_dir = self.mkdir('bar') dst_dir = self.mkdir('baz') _force_rename_dir(src_dir, dst_dir) assert os.path.exists(dst_dir) assert os.path.exists(os.path.join(dst_dir, 'bar.txt')) assert not os.path.exists(src_dir) class TestSwapDir(DirTest): def test_swap_dir(self): src_dir = self.mkdir('bar') dst_dir = os.path.join(self.tmpdir, 'baz') swap_dir(src_dir, dst_dir) assert os.path.exists(dst_dir) assert os.path.exists(os.path.join(dst_dir, 'bar.txt')) assert not os.path.exists(src_dir) def test_swap_dir_w_old(self): src_dir = self.mkdir('bar') dst_dir = self.mkdir('baz') swap_dir(src_dir, dst_dir) assert os.path.exists(dst_dir) assert os.path.exists(os.path.join(dst_dir, 'bar.txt')) assert not os.path.exists(src_dir) def test_swap_dir_keep_old(self): src_dir = self.mkdir('bar') dst_dir = self.mkdir('baz') swap_dir(src_dir, dst_dir, keep_old=True, backup_ext='.bak') assert os.path.exists(dst_dir) assert os.path.exists(os.path.join(dst_dir, 'bar.txt')) assert os.path.exists(dst_dir + '.bak') assert os.path.exists(os.path.join(dst_dir + '.bak', 'baz.txt')) class TestCleanupDirectory(DirTest): def test_no_remove(self): dirs = [self.mkdir('dir'+str(n)) for n in range(10)] for d in dirs: assert os.path.exists(d), d cleanup_directory(self.tmpdir, timestamp_before(minutes=1)) for d in dirs: assert os.path.exists(d), d def test_file_handler(self): files = [] file_handler_calls = [] def file_handler(filename): file_handler_calls.append(filename) new_date = timestamp_before(weeks=1) for n in range(10): fname = 'foo'+str(n) filename = self.mkfile(fname) os.utime(filename, (new_date, new_date)) files.append(filename) for filename in files: assert os.path.exists(filename), filename cleanup_directory(self.tmpdir, timestamp_before(), file_handler=file_handler) for filename in files: assert os.path.exists(filename), filename assert set(files) == set(file_handler_calls) def test_no_directory(self): cleanup_directory(os.path.join(self.tmpdir, 'invalid'), timestamp_before()) # nothing should happen def test_remove_all(self): files = [] new_date = timestamp_before(weeks=1) for n in range(10): fname = 'foo'+str(n) filename = self.mkfile(fname) os.utime(filename, (new_date, new_date)) files.append(filename) for filename in files: assert os.path.exists(filename), filename cleanup_directory(self.tmpdir, timestamp_before()) for filename in files: assert not os.path.exists(filename), filename assert not os.path.exists(os.path.dirname(filename)), filename def test_remove_empty_dirs(self): os.makedirs(os.path.join(self.tmpdir, 'foo', 'bar', 'baz')) cleanup_directory(self.tmpdir, timestamp_before(minutes=-1)) assert not os.path.exists(os.path.join(self.tmpdir, 'foo')) def test_remove_some(self): files = [] # create a few files, every other file is one week old new_date = timestamp_before(weeks=1) for n in range(10): fname = 'foo'+str(n) filename = self.mkfile(fname) if n % 2 == 0: os.utime(filename, (new_date, new_date)) files.append(filename) # check all files are present for filename in files: assert os.path.exists(filename), filename # cleanup_directory for all files older then one minute cleanup_directory(self.tmpdir, timestamp_before(minutes=1)) # check old files and dirs are removed for filename in files[::2]: assert not os.path.exists(filename), filename assert not os.path.exists(os.path.dirname(filename)), filename # check new files are still present for filename in files[1::2]: assert os.path.exists(filename), filename def _write_atomic_data(i_filename): (i, filename) = i_filename data = str(i) + '\n' + 'x' * 10000 write_atomic(filename, data.encode('utf-8')) time.sleep(0.001) class TestWriteAtomic(object): def setup(self): self.dirname = tempfile.mkdtemp() def teardown(self): if self.dirname: shutil.rmtree(self.dirname) def test_concurrent_write(self): filename = os.path.join(self.dirname, 'tmpfile') num_writes = 800 concurrent_writes = 8 p = multiprocessing.Pool(concurrent_writes) p.map(_write_atomic_data, ((i, filename) for i in range(num_writes))) p.close() p.join() assert os.path.exists(filename) last_i = int(open(filename).readline()) assert last_i > (num_writes / 2), ("file should contain content from " "later writes, got content from write %d" % (last_i + 1) ) os.unlink(filename) assert os.listdir(self.dirname) == [] def test_not_a_file(self): # check that expected errors are not hidden filename = os.path.join(self.dirname, 'tmpfile') os.mkdir(filename) try: write_atomic(filename, b'12345') except OSError: pass else: assert False, 'expected exception' def test_reraise_exception(): def valueerror_raiser(): raise ValueError() def reraiser(): try: valueerror_raiser() except ValueError: reraise_exception(TypeError(), sys.exc_info()) try: reraiser() except TypeError as ex: assert ex else: assert False, 'expected exception'�����������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_wms_capabilities.py�����������������������������������������0000664�0000000�0000000�00000003021�13204544724�0024217�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of the MapProxy project. # Copyright (C) 2014 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mapproxy.service.wms import limit_srs_extents from mapproxy.layer import DefaultMapExtent, MapExtent from mapproxy.srs import SRS from nose.tools import eq_ class TestLimitSRSExtents(object): def test_defaults(self): eq_( limit_srs_extents({}, ['EPSG:4326', 'EPSG:3857']), { 'EPSG:4326': DefaultMapExtent(), 'EPSG:3857': DefaultMapExtent(), } ) def test_unsupported(self): eq_( limit_srs_extents({'EPSG:9999': DefaultMapExtent()}, ['EPSG:4326', 'EPSG:3857']), {} ) def test_limited_unsupported(self): eq_( limit_srs_extents({'EPSG:9999': DefaultMapExtent(), 'EPSG:4326': MapExtent([0, 0, 10, 10], SRS(4326))}, ['EPSG:4326', 'EPSG:3857']), {'EPSG:4326': MapExtent([0, 0, 10, 10], SRS(4326)),} ) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_wms_layer.py������������������������������������������������0000664�0000000�0000000�00000007336�13204544724�0022717�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of the MapProxy project. # Copyright (C) 2013 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division from mapproxy.layer import MapQuery, InfoQuery from mapproxy.srs import SRS from mapproxy.service.wms import combined_layers from nose.tools import eq_ from mapproxy.source.wms import WMSSource from mapproxy.client.wms import WMSClient from mapproxy.request.wms import create_request class TestCombinedLayers(object): q = MapQuery((0, 0, 10000, 10000), (100, 100), SRS(3857)) def test_empty(self): eq_(combined_layers([], self.q), []) def test_same_source(self): layers = [ WMSSource(WMSClient(create_request({'url': 'http://foo/', 'layers': 'a'}, {}))), WMSSource(WMSClient(create_request({'url': 'http://foo/', 'layers': 'b'}, {}))), ] combined = combined_layers(layers, self.q) eq_(len(combined), 1) eq_(combined[0].client.request_template.params.layers, ['a', 'b']) def test_mixed_hosts(self): layers = [ WMSSource(WMSClient(create_request({'url': 'http://foo/', 'layers': 'a'}, {}))), WMSSource(WMSClient(create_request({'url': 'http://foo/', 'layers': 'b'}, {}))), WMSSource(WMSClient(create_request({'url': 'http://bar/', 'layers': 'c'}, {}))), WMSSource(WMSClient(create_request({'url': 'http://bar/', 'layers': 'd'}, {}))), WMSSource(WMSClient(create_request({'url': 'http://foo/', 'layers': 'e'}, {}))), WMSSource(WMSClient(create_request({'url': 'http://foo/', 'layers': 'f'}, {}))), ] combined = combined_layers(layers, self.q) eq_(len(combined), 3) eq_(combined[0].client.request_template.params.layers, ['a', 'b']) eq_(combined[1].client.request_template.params.layers, ['c', 'd']) eq_(combined[2].client.request_template.params.layers, ['e', 'f']) def test_mixed_params(self): layers = [ WMSSource(WMSClient(create_request({'url': 'http://foo/', 'layers': 'a'}, {}))), WMSSource(WMSClient(create_request({'url': 'http://foo/', 'layers': 'b'}, {}))), WMSSource(WMSClient(create_request({'url': 'http://foo/', 'layers': 'c'}, {}))), WMSSource(WMSClient(create_request({'url': 'http://foo/', 'layers': 'd'}, {}))), WMSSource(WMSClient(create_request({'url': 'http://foo/', 'layers': 'e'}, {}))), WMSSource(WMSClient(create_request({'url': 'http://foo/', 'layers': 'f'}, {}))), ] layers[0].supported_srs = ["EPSG:4326"] layers[1].supported_srs = ["EPSG:4326"] layers[2].supported_formats = ["image/png"] layers[3].supported_formats = ["image/png"] combined = combined_layers(layers, self.q) eq_(len(combined), 3) eq_(combined[0].client.request_template.params.layers, ['a', 'b']) eq_(combined[1].client.request_template.params.layers, ['c', 'd']) eq_(combined[2].client.request_template.params.layers, ['e', 'f']) class TestInfoQuery(object): def test_coord(self): query = InfoQuery((8, 50, 9, 51), (400, 1000), SRS(4326), (100, 600), 'text/plain') eq_(query.coord, (8.25, 50.4)) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/test/unit/test_yaml.py�����������������������������������������������������0000664�0000000�0000000�00000003560�13204544724�0021652�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import tempfile from mapproxy.util.yaml import load_yaml, load_yaml_file, YAMLError from mapproxy.compat import string_type from nose.tools import eq_ class TestLoadYAMLFile(object): def setup(self): self.tmp_files = [] def teardown(self): for f in self.tmp_files: os.unlink(f) def yaml_file(self, content): fd, fname = tempfile.mkstemp() f = os.fdopen(fd, 'w') f.write(content) self.tmp_files.append(fname) return fname def test_load_yaml_file(self): f = self.yaml_file("hello:\n - 1\n - 2") doc = load_yaml_file(open(f)) eq_(doc, {'hello': [1, 2]}) def test_load_yaml_file_filename(self): f = self.yaml_file("hello:\n - 1\n - 2") assert isinstance(f, string_type) doc = load_yaml_file(f) eq_(doc, {'hello': [1, 2]}) def test_load_yaml(self): doc = load_yaml("hello:\n - 1\n - 2") eq_(doc, {'hello': [1, 2]}) def test_load_yaml_with_tabs(self): try: f = self.yaml_file("hello:\n\t- world") load_yaml_file(f) except YAMLError as ex: assert 'line 2' in str(ex) else: assert False, 'expected YAMLError' ������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/tilefilter.py��������������������������������������������������������������0000664�0000000�0000000�00000004171�13204544724�0020055�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of the MapProxy project. # Copyright (C) 2010, 2011 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Filter for tiles (watermark, etc.) """ from mapproxy.image.message import WatermarkImage def create_watermark_filter(conf, context, **kw): text = conf['watermark'].get('text', '') opacity = conf['watermark'].get('opacity') font_size = conf['watermark'].get('font_size') spacing = conf['watermark'].get('spacing') font_color = conf['watermark'].get('color') if spacing not in ('wide', None): raise ValueError('unsupported watermark spacing: %r' % spacing) if text != '': return watermark_filter(text, opacity=opacity, font_size=font_size, spacing=spacing, font_color=font_color) def watermark_filter(text, opacity=None, spacing=None, font_size=None, font_color=None): """ Returns a tile filter that adds a watermark to the tiles. :param text: watermark text """ def _watermark_filter(tile): placement = tile_watermark_placement(tile.coord, spacing == 'wide') wimg = WatermarkImage(text, image_opts=tile.source.image_opts, placement=placement, opacity=opacity, font_size=font_size, font_color=font_color) tile.source = wimg.draw(img=tile.source, in_place=False) return tile return _watermark_filter def tile_watermark_placement(coord, double_spacing=False): if not double_spacing: if coord[1] % 2 == 0: return 'c' else: return 'b' if coord[1] % 2 != coord[0] % 2: return 'c' return None �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/����������������������������������������������������������������������0000775�0000000�0000000�00000000000�13204544724�0016312�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/__init__.py�����������������������������������������������������������0000664�0000000�0000000�00000000000�13204544724�0020411�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/async.py��������������������������������������������������������������0000664�0000000�0000000�00000025615�13204544724�0020012�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. MAX_MAP_ASYNC_THREADS = 20 try: import Queue except ImportError: import queue as Queue import sys import threading try: import eventlet import eventlet.greenpool import eventlet.tpool import eventlet.patcher _has_eventlet = True import eventlet.debug eventlet.debug.hub_exceptions(False) except ImportError: _has_eventlet = False from mapproxy.config import base_config from mapproxy.config import local_base_config from mapproxy.compat import PY2 import logging log_system = logging.getLogger('mapproxy.system') class AsyncResult(object): def __init__(self, result=None, exception=None): self.result = result self.exception = exception def __repr__(self): return "<AsyncResult result='%s' exception='%s'>" % ( self.result, self.exception) def _result_iter(results, use_result_objects=False): for result in results: if use_result_objects: exception = None if (isinstance(result, tuple) and len(result) == 3 and isinstance(result[1], Exception)): exception = result result = None yield AsyncResult(result, exception) else: yield result class EventletPool(object): def __init__(self, size=100): self.size = size self.base_config = base_config() def shutdown(self, force=False): # there is not way to stop a GreenPool pass def map(self, func, *args, **kw): return list(self.imap(func, *args, **kw)) def imap(self, func, *args, **kw): use_result_objects = kw.get('use_result_objects', False) def call(*args): with local_base_config(self.base_config): try: return func(*args) except Exception: if use_result_objects: return sys.exc_info() else: raise if len(args[0]) == 1: eventlet.sleep() return _result_iter([call(*list(zip(*args))[0])], use_result_objects) pool = eventlet.greenpool.GreenPool(self.size) return _result_iter(pool.imap(call, *args), use_result_objects) def starmap(self, func, args, **kw): use_result_objects = kw.get('use_result_objects', False) def call(*args): with local_base_config(self.base_config): try: return func(*args) except Exception: if use_result_objects: return sys.exc_info() else: raise if len(args) == 1: eventlet.sleep() return _result_iter([call(*args[0])], use_result_objects) pool = eventlet.greenpool.GreenPool(self.size) return _result_iter(pool.starmap(call, args), use_result_objects) def starcall(self, args, **kw): use_result_objects = kw.get('use_result_objects', False) def call(func, *args): with local_base_config(self.base_config): try: return func(*args) except Exception: if use_result_objects: return sys.exc_info() else: raise if len(args) == 1: eventlet.sleep() return _result_iter([call(args[0][0], *args[0][1:])], use_result_objects) pool = eventlet.greenpool.GreenPool(self.size) return _result_iter(pool.starmap(call, args), use_result_objects) class ThreadWorker(threading.Thread): def __init__(self, task_queue, result_queue): threading.Thread.__init__(self) self.task_queue = task_queue self.result_queue = result_queue self.base_config = base_config() def run(self): with local_base_config(self.base_config): while True: task = self.task_queue.get() if task is None: self.task_queue.task_done() break exec_id, func, args = task try: result = func(*args) except Exception: result = sys.exc_info() self.result_queue.put((exec_id, result)) self.task_queue.task_done() def _consume_queue(queue): """ Get all items from queue. """ while not queue.empty(): try: queue.get(block=False) queue.task_done() except Queue.Empty: pass class ThreadPool(object): def __init__(self, size=4): self.pool_size = size self.task_queue = Queue.Queue() self.result_queue = Queue.Queue() self.pool = None def map_each(self, func_args, raise_exceptions): """ args should be a list of function arg tuples. map_each calls each function with the given arg. """ if self.pool_size < 2: for func, arg in func_args: try: yield func(*arg) except Exception: yield sys.exc_info() raise StopIteration() self.pool = self._init_pool() i = 0 for i, (func, arg) in enumerate(func_args): self.task_queue.put((i, func, arg)) results = {} next_result = 0 for value in self._get_results(next_result, results, raise_exceptions): yield value next_result += 1 self.task_queue.join() for value in self._get_results(next_result, results, raise_exceptions): yield value next_result += 1 self.shutdown() def _single_call(self, func, args, use_result_objects): try: result = func(*args) except Exception: if not use_result_objects: raise result = sys.exc_info() return _result_iter([result], use_result_objects) def map(self, func, *args, **kw): return list(self.imap(func, *args, **kw)) def imap(self, func, *args, **kw): use_result_objects = kw.get('use_result_objects', False) if len(args[0]) == 1: return self._single_call(func, next(iter(zip(*args))), use_result_objects) return _result_iter(self.map_each([(func, arg) for arg in zip(*args)], raise_exceptions=not use_result_objects), use_result_objects) def starmap(self, func, args, **kw): use_result_objects = kw.get('use_result_objects', False) if len(args[0]) == 1: return self._single_call(func, args[0], use_result_objects) return _result_iter(self.map_each([(func, arg) for arg in args], raise_exceptions=not use_result_objects), use_result_objects) def starcall(self, args, **kw): def call(func, *args): return func(*args) return self.starmap(call, args, **kw) def _get_results(self, next_result, results, raise_exceptions): for i, value in self._fetch_results(raise_exceptions): if i == next_result: yield value next_result += 1 while next_result in results: yield results.pop(next_result) next_result += 1 else: results[i] = value def _fetch_results(self, raise_exceptions): while not self.task_queue.empty() or not self.result_queue.empty(): task_result = self.result_queue.get() if (raise_exceptions and isinstance(task_result[1], tuple) and len(task_result[1]) == 3 and isinstance(task_result[1][1], Exception)): self.shutdown(force=True) exc_class, exc, tb = task_result[1] if PY2: exec('raise exc_class, exc, tb') else: raise exc.with_traceback(tb) yield task_result def shutdown(self, force=False): """ Send shutdown sentinel to all executor threads. If `force` is True, clean task_queue and result_queue. """ if force: _consume_queue(self.task_queue) _consume_queue(self.result_queue) for _ in range(self.pool_size): self.task_queue.put(None) def _init_pool(self): if self.pool_size < 2: return [] pool = [] for _ in range(self.pool_size): t = ThreadWorker(self.task_queue, self.result_queue) t.daemon = True t.start() pool.append(t) return pool def imap_async_eventlet(func, *args): pool = EventletPool() return pool.imap(func, *args) def imap_async_threaded(func, *args): pool = ThreadPool(min(len(args[0]), MAX_MAP_ASYNC_THREADS)) return pool.imap(func, *args) def starmap_async_eventlet(func, args): pool = EventletPool() return pool.starmap(func, args) def starmap_async_threaded(func, args): pool = ThreadPool(min(len(args[0]), MAX_MAP_ASYNC_THREADS)) return pool.starmap(func, args) def starcall_async_eventlet(args): pool = EventletPool() return pool.starcall(args) def starcall_async_threaded(args): pool = ThreadPool(min(len(args[0]), MAX_MAP_ASYNC_THREADS)) return pool.starcall(args) def run_non_blocking_eventlet(func, args, kw={}): return eventlet.tpool.execute(func, *args, **kw) def run_non_blocking_threaded(func, args, kw={}): return func(*args, **kw) def import_module(module): """ Import ``module``. Import patched version if eventlet is used. """ if uses_eventlet: return eventlet.import_patched(module) else: return __import__(module) uses_eventlet = False # socket should be monkey patched when MapProxy runs inside eventlet if _has_eventlet and eventlet.patcher.is_monkey_patched('socket'): uses_eventlet = True log_system.info('using eventlet for asynchronous operations') imap = imap_async_eventlet starmap = starmap_async_eventlet starcall = starcall_async_eventlet Pool = EventletPool run_non_blocking = run_non_blocking_eventlet else: imap = imap_async_threaded starmap = starmap_async_threaded starcall = starcall_async_threaded Pool = ThreadPool run_non_blocking = run_non_blocking_threaded �������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/collections.py��������������������������������������������������������0000664�0000000�0000000�00000007141�13204544724�0021205�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import from collections import deque from mapproxy.compat.itertools import islice from mapproxy.compat import string_type class LRU(object): """ Least Recently Used dictionary. Stores `size` key-value pairs. Removes last used key-value when dict is full. This LRU was developed for sizes <1000. Set new: O(1) Get/Set existing: O(1) newest to O(n) for oldest entry Contains: O(1) """ def __init__(self, size=100): self.size = size self.values = {} self.last_used = deque() def get(self, key, default=None): if key not in self.values: return default else: return self[key] def __repr__(self): last_values = [] for k in islice(self.last_used, 10): last_values.append((k, self.values[k])) return '<LRU size=%d values=%s%s>' % ( self.size, repr(last_values)[:-1], ', ...]' if len(self)>10 else ']') def __getitem__(self, key): result = self.values[key] try: self.last_used.remove(key) except ValueError: pass self.last_used.appendleft(key) return result def __setitem__(self, key, value): if key in self.values: try: self.last_used.remove(key) except ValueError: pass self.last_used.appendleft(key) self.values[key] = value while len(self.values) > self.size: del self.values[self.last_used.pop()] def __len__(self): return len(self.values) def __delitem__(self, key): if key in self.values: try: self.last_used.remove(key) except ValueError: pass del self.values[key] def __contains__(self, key): return key in self.values class ImmutableDictList(object): """ A dictionary where each item can also be accessed by the integer index of the initial position. >>> d = ImmutableDictList([('foo', 23), ('bar', 24)]) >>> d['bar'] 24 >>> d[0], d[1] (23, 24) """ def __init__(self, items): self._names = [] self._values = {} for name, value in items: self._values[name] = value self._names.append(name) def __getitem__(self, name): if isinstance(name, string_type): return self._values[name] else: return self._values[self._names[name]] def __contains__(self, name): try: self[name] return True except KeyError: return False def __len__(self): return len(self._values) def __str__(self): values = [] for name in self._names: values.append('%s: %s' % (name, self._values[name])) return '[%s]' % (', '.join(values),) def iteritems(self): for idx in self._names: yield idx, self._values[idx] �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/coverage.py�����������������������������������������������������������0000664�0000000�0000000�00000023051�13204544724�0020460�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import operator import threading from mapproxy.grid import bbox_intersects, bbox_contains from mapproxy.util.py import cached_property from mapproxy.util.geom import ( require_geom_support, load_polygon_lines, transform_geometry, bbox_polygon, EmptyGeometryError, ) from mapproxy.srs import SRS import logging from functools import reduce log_config = logging.getLogger('mapproxy.config.coverage') try: import shapely.geometry import shapely.prepared except ImportError: # missing Shapely is handled by require_geom_support pass def coverage(geom, srs, clip=False): if isinstance(geom, (list, tuple)): return BBOXCoverage(geom, srs, clip=clip) else: return GeomCoverage(geom, srs, clip=clip) def load_limited_to(limited_to): require_geom_support() srs = SRS(limited_to['srs']) geom = limited_to['geometry'] if not hasattr(geom, 'type'): # not a Shapely geometry if isinstance(geom, (list, tuple)): geom = bbox_polygon(geom) else: polygons = load_polygon_lines(geom.split('\n')) if len(polygons) == 1: geom = polygons[0] else: geom = shapely.geometry.MultiPolygon(polygons) return GeomCoverage(geom, srs, clip=True) class MultiCoverage(object): clip = False """Aggregates multiple coverages""" def __init__(self, coverages): self.coverages = coverages self.bbox = self.extent.bbox @cached_property def extent(self): return reduce(operator.add, [c.extent for c in self.coverages]) def intersects(self, bbox, srs): return any(c.intersects(bbox, srs) for c in self.coverages) def contains(self, bbox, srs): return any(c.contains(bbox, srs) for c in self.coverages) def transform_to(self, srs): return MultiCoverage([c.transform_to(srs) for c in self.coverages]) def __eq__(self, other): if not isinstance(other, MultiCoverage): return NotImplemented if self.bbox != other.bbox: return False if len(self.coverages) != len(other.coverages): return False for a, b in zip(self.coverages, other.coverages): if a != b: return False return True def __ne__(self, other): if not isinstance(other, MultiCoverage): return NotImplemented return not self.__eq__(other) def __repr__(self): return '<MultiCoverage %r: %r>' % (self.extent.llbbox, self.coverages) class BBOXCoverage(object): def __init__(self, bbox, srs, clip=False): self.bbox = bbox self.srs = srs self.geom = None self.clip = clip @property def extent(self): from mapproxy.layer import MapExtent return MapExtent(self.bbox, self.srs) def _bbox_in_coverage_srs(self, bbox, srs): if srs != self.srs: bbox = srs.transform_bbox_to(self.srs, bbox) return bbox def intersects(self, bbox, srs): bbox = self._bbox_in_coverage_srs(bbox, srs) return bbox_intersects(self.bbox, bbox) def intersection(self, bbox, srs): bbox = self._bbox_in_coverage_srs(bbox, srs) intersection = ( max(self.bbox[0], bbox[0]), max(self.bbox[1], bbox[1]), min(self.bbox[2], bbox[2]), min(self.bbox[3], bbox[3]), ) if intersection[0] >= intersection[2] or intersection[1] >= intersection[3]: return None return BBOXCoverage(intersection, self.srs, clip=self.clip) def contains(self, bbox, srs): bbox = self._bbox_in_coverage_srs(bbox, srs) return bbox_contains(self.bbox, bbox) def transform_to(self, srs): if srs == self.srs: return self bbox = self.srs.transform_bbox_to(srs, self.bbox) return BBOXCoverage(bbox, srs, clip=self.clip) def __eq__(self, other): if not isinstance(other, BBOXCoverage): return NotImplemented if self.srs != other.srs: return False if self.bbox != other.bbox: return False return True def __ne__(self, other): if not isinstance(other, BBOXCoverage): return NotImplemented return not self.__eq__(other) def __repr__(self): return '<BBOXCoverage %r/%r>' % (self.extent.llbbox, self.bbox) class GeomCoverage(object): def __init__(self, geom, srs, clip=False): self.geom = geom self.bbox = geom.bounds self.srs = srs self.clip = clip self._prep_lock = threading.Lock() self._prepared_geom = None self._prepared_counter = 0 self._prepared_max = 10000 @property def extent(self): from mapproxy.layer import MapExtent return MapExtent(self.bbox, self.srs) @property def prepared_geom(self): # GEOS internal data structure for prepared geometries grows over time, # recreate to limit memory consumption if not self._prepared_geom or self._prepared_counter > self._prepared_max: self._prepared_geom = shapely.prepared.prep(self.geom) self._prepared_counter = 0 self._prepared_counter += 1 return self._prepared_geom def _geom_in_coverage_srs(self, geom, srs): if isinstance(geom, shapely.geometry.base.BaseGeometry): if srs != self.srs: geom = transform_geometry(srs, self.srs, geom) elif len(geom) == 2: if srs != self.srs: geom = srs.transform_to(self.srs, geom) geom = shapely.geometry.Point(geom) else: if srs != self.srs: geom = srs.transform_bbox_to(self.srs, geom) geom = bbox_polygon(geom) return geom def transform_to(self, srs): if srs == self.srs: return self geom = transform_geometry(self.srs, srs, self.geom) return GeomCoverage(geom, srs, clip=self.clip) def intersects(self, bbox, srs): bbox = self._geom_in_coverage_srs(bbox, srs) with self._prep_lock: return self.prepared_geom.intersects(bbox) def intersection(self, bbox, srs): bbox = self._geom_in_coverage_srs(bbox, srs) return GeomCoverage(self.geom.intersection(bbox), self.srs, clip=self.clip) def contains(self, bbox, srs): bbox = self._geom_in_coverage_srs(bbox, srs) with self._prep_lock: return self.prepared_geom.contains(bbox) def __eq__(self, other): if not isinstance(other, GeomCoverage): return NotImplemented if self.srs != other.srs: return False if self.bbox != other.bbox: return False if not self.geom.equals(other.geom): return False return True def __ne__(self, other): if not isinstance(other, GeomCoverage): return NotImplemented return not self.__eq__(other) def __repr__(self): return '<GeomCoverage %r: %r>' % (self.extent.llbbox, self.geom) def union_coverage(coverages, clip=None): """ Create a coverage that is the union of all `coverages`. Resulting coverage is in the SRS of the first coverage. """ srs = coverages[0].srs coverages = [c.transform_to(srs) for c in coverages] geoms = [] for c in coverages: if isinstance(c, BBOXCoverage): geoms.append(bbox_polygon(c.bbox)) else: geoms.append(c.geom) import shapely.ops union = shapely.ops.cascaded_union(geoms) return GeomCoverage(union, srs=srs, clip=clip) def diff_coverage(coverages, clip=None): """ Create a coverage by subtracting all `coverages` from the first one. Resulting coverage is in the SRS of the first coverage. """ srs = coverages[0].srs coverages = [c.transform_to(srs) for c in coverages] geoms = [] for c in coverages: if isinstance(c, BBOXCoverage): geoms.append(bbox_polygon(c.bbox)) else: geoms.append(c.geom) sub = shapely.ops.cascaded_union(geoms[1:]) diff = geoms[0].difference(sub) if diff.is_empty: raise EmptyGeometryError("diff did not return any geometry") return GeomCoverage(diff, srs=srs, clip=clip) def intersection_coverage(coverages, clip=None): """ Create a coverage by creating the intersection of all `coverages`. Resulting coverage is in the SRS of the first coverage. """ srs = coverages[0].srs coverages = [c.transform_to(srs) for c in coverages] geoms = [] for c in coverages: if isinstance(c, BBOXCoverage): geoms.append(bbox_polygon(c.bbox)) else: geoms.append(c.geom) intersection = reduce(lambda a, b: a.intersection(b), geoms) if intersection.is_empty: raise EmptyGeometryError("intersection did not return any geometry") return GeomCoverage(intersection, srs=srs, clip=clip)���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/������������������������������������������������������������������0000775�0000000�0000000�00000000000�13204544724�0017112�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/__init__.py�������������������������������������������������������0000664�0000000�0000000�00000001206�13204544724�0021222�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale <http://omniscale.de> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/dictspec/���������������������������������������������������������0000775�0000000�0000000�00000000000�13204544724�0020710�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/dictspec/__init__.py����������������������������������������������0000664�0000000�0000000�00000000023�13204544724�0023014�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������__version__ = '0.1'�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/dictspec/spec.py��������������������������������������������������0000664�0000000�0000000�00000007153�13204544724�0022222�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2011, Oliver Tonnhofer <olt@omniscale.de> # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. from mapproxy.compat import itervalues import sys if sys.version_info[0] == 2: number_types = (float, int, long) else: number_types = (float, int) class required(str): """ Mark a dictionary key as required. """ pass class anything(object): """ Wildcard key or value for dictionaries. >>> from .validator import validate >>> validate({anything(): 1}, {'foo': 2, 'bar': 49}) """ def compare_type(self, data): return True class recursive(object): """ Recursive types. >>> from .validator import validate >>> spec = recursive({'foo': recursive()}) >>> validate(spec, {'foo': {'foo': {'foo':{}}}}) """ def __init__(self, spec=None): self.spec = spec def compare_type(self, data): return isinstance(data, type(self.spec)) class one_of(object): """ One of the given types. >>> from .validator import validate >>> validate(one_of(str(), number()), 'foo') >>> validate(one_of(str(), number()), 32) """ def __init__(self, *specs): self.specs = specs # typo, backwards compatibility one_off = one_of def combined(*dicts): """ Combine multiple dicts. >>> (combined({'a': 'foo'}, {'b': 'bar'}) ... == {'a': 'foo', 'b': 'bar'}) True """ result = {} for d in dicts: result.update(d) return result class number(object): """ Any number. >>> from .validator import validate >>> validate(number(), 1) >>> validate(number(), -32.0) >>> validate(number(), 99999999999999) """ def compare_type(self, data): # True/False are also instances of int, exclude them return isinstance(data, number_types) and not isinstance(data, bool) class type_spec(object): def __init__(self, type_key, specs): self.type_key = type_key self.specs = specs for v in itervalues(specs): if not isinstance(v, dict): raise ValueError('%s requires dict subspecs', self.__class__) if self.type_key not in v: v[self.type_key] = str() def subspec(self, data, context): if not data: raise ValueError("%s is empty" % (context.current_pos, )) if self.type_key not in data: raise ValueError("'%s' not in %s" % (self.type_key, context.current_pos)) key = data[self.type_key] if key not in self.specs: raise ValueError("unknown %s value '%s' in %s" % (self.type_key, key, context.current_pos)) return self.specs[key] ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/dictspec/test/����������������������������������������������������0000775�0000000�0000000�00000000000�13204544724�0021667�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/dictspec/test/__init__.py�����������������������������������������0000664�0000000�0000000�00000000000�13204544724�0023766�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/dictspec/test/test_validator.py�����������������������������������0000664�0000000�0000000�00000022734�13204544724�0025275�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -:- encoding: utf8 -:- # Copyright (c) 2011, Oliver Tonnhofer <olt@omniscale.de> # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. from __future__ import absolute_import import unittest from ..validator import validate, ValidationError, SpecError from ..spec import required, one_of, number, recursive, type_spec, anything from mapproxy.compat import string_type def raises(exception): def wrapper(f): def _wrapper(self): try: f(self) except exception: pass else: raise AssertionError('expected exception %s', exception) return wrapper class TestSimpleDict(unittest.TestCase): def test_validate_simple_dict(self): spec = {'hello': 1, 'world': True} validate(spec, {'hello': 34, 'world': False}) @raises(ValidationError) def test_invalid_key(self): spec = {'world': True} validate(spec, {'world_foo': False}) def test_empty_data(self): spec = {'world': 1} validate(spec, {}) @raises(ValidationError) def test_invalid_value(self): spec = {'world': 1} validate(spec, {'world_foo': False}) @raises(ValidationError) def test_missing_required_key(self): spec = {required('world'): 1} validate(spec, {}) def test_valid_one_of(self): spec = {'hello': one_of(1, bool())} validate(spec, {'hello': 129}) validate(spec, {'hello': True}) @raises(ValidationError) def test_invalid_one_of(self): spec = {'hello': one_of(1, False)} validate(spec, {'hello': []}) def test_instances_and_types(self): spec = {'str()': str(), 'string_type': string_type, 'int': int, 'int()': int()} validate(spec, {'str()': 'str', 'string_type': u'☃', 'int': 1, 'int()': 1}) class TestLists(unittest.TestCase): def test_list(self): spec = [1] validate(spec, [1, 2, 3, 4, -9]) def test_empty_list(self): spec = [1] validate(spec, []) @raises(ValidationError) def test_invalid_item(self): spec = [1] validate(spec, [1, 'hello']) class TestNumber(unittest.TestCase): def check_valid(self, spec, data): validate(spec, data) def test_numbers(self): spec = number() for i in (0, 1, 23e999, int(10e20), 23.1, -0.0000000001): self.check_valid(spec, i) class TestNested(unittest.TestCase): def check_valid(self, spec, data): validate(spec, data) def check_invalid(self, spec, data): try: validate(spec, data) except ValidationError: pass else: assert False, "expected ValidationError" def test_dict(self): spec = { 'globals': { 'image': { 'format': { 'png': { 'mode': 'RGB', } }, }, 'cache': { 'base_dir': '/path/to/foo' } } } self.check_valid(spec, {'globals': {'image': {'format': {'png': {'mode': 'P'}}}}}) self.check_valid(spec, {'globals': {'image': {'format': {'png': {'mode': 'P'}}}, 'cache': {'base_dir': '/somewhere'}}}) self.check_invalid(spec, {'globals': {'image': {'foo': {'png': {'mode': 'P'}}}}}) self.check_invalid(spec, {'globals': {'image': {'png': {'png': {'mode': 1}}}}}) def test_errors_in_unicode_keys(self): # should not raise UnicodeEncodeError spec = { anything(): str(), } self.check_invalid(spec, {u'globalü': 12}) class TestRecursive(unittest.TestCase): def test(self): spec = recursive({'hello': str(), 'more': recursive()}) validate(spec, {'hello': 'world', 'more': {'hello': 'foo', 'more': {'more': {}}}}) def test_multiple(self): spec = {'a': recursive({'hello': str(), 'more': recursive()}), 'b': recursive({'foo': recursive()})} validate(spec, {'b': {'foo': {'foo': {}}}}) validate(spec, {'a': {'hello': 'world', 'more': {'hello': 'foo', 'more': {'more': {}}}}}) validate(spec, {'b': {'foo': {'foo': {}}}, 'a': {'hello': 'world', 'more': {'hello': 'foo', 'more': {'more': {}}}}}) @raises(SpecError) def test_without_spec(self): spec = {'a': recursive()} validate(spec, {'a': {'a': {}}}) class TestTypeSpec(unittest.TestCase): def test(self): spec = type_spec('type', {'foo': {'alpha': str()}, 'bar': {'one': 1, 'two': str()}}) validate(spec, {'type': 'foo', 'alpha': 'yes'}) validate(spec, {'type': 'bar', 'one': 2}) def test_missing_type(self): spec = type_spec('type', {'foo': {'alpha': str()}, 'bar': {'one': 1, 'two': str()}}) try: validate(spec, {'alpha': 'yes'}) except ValidationError as ex: assert "'type' not in ." in ex.errors[0] else: assert False def test_unknown_type(self): spec = type_spec('type', {'foo': {'alpha': str()}, 'bar': {'one': 1, 'two': str()}}) try: validate(spec, {'type': 'baz', 'alpha': 'yes'}) except ValidationError as ex: assert "unknown type value 'baz' in ." in ex.errors[0], ex else: assert False def test_no_type_dict(self): spec = {'dict': type_spec('type', {'foo': {'alpha': str()}, 'bar': {'one': 1, 'two': str()}})} try: validate(spec, {'dict': None}) except ValidationError as ex: assert "dict is empty" in ex.errors[0], ex else: assert False class TestErrors(unittest.TestCase): def test_invalid_types(self): spec = {'str': str, 'str()': str(), 'string_type': string_type, '1': 1, 'int': int} try: validate(spec, {'str': 1, 'str()': 1, 'string_type': 1, '1': 'a', 'int': 'int'}) except ValidationError as ex: ex.errors.sort() assert ex.errors[0] == "'a' in 1 not of type int" assert ex.errors[1] == "'int' in int not of type int" assert ex.errors[2] == '1 in str not of type str' assert ex.errors[3] == '1 in str() not of type str' assert ex.errors[4] in ( '1 in string_type not of type basestring', #PY2 '1 in string_type not of type str') #PY3 else: assert False def test_invalid_key(self): spec = {'world': {'europe': {}}} try: validate(spec, {'world': {'europe': {'germany': 1}}}) except ValidationError as ex: assert 'world.europe' in str(ex) else: assert False def test_invalid_list_item(self): spec = {'numbers': [number()]} try: validate(spec, {'numbers': [1, 2, 3, 'foo']}) except ValidationError as ex: assert 'numbers[3] not of type number' in str(ex), str(ex) else: assert False def test_multiple_invalid_list_items(self): spec = {'numbers': [number()]} try: validate(spec, {'numbers': [1, True, 3, 'foo']}) except ValidationError as ex: assert '2 validation errors' in str(ex), str(ex) assert 'numbers[1] not of type number' in ex.errors[0] assert 'numbers[3] not of type number' in ex.errors[1] else: assert False def test_error_in_non_string_key(self): spec = {1: bool()} try: validate(spec, {1: 'not a bool'}) except ValidationError as ex: assert "'not a bool' in 1 not of type bool" in ex.errors[0] else: assert False def test_error_in_non_string_key_with_anything_key_spec(self): spec = {anything(): bool()} try: validate(spec, {1: 'not a bool'}) except ValidationError as ex: assert "'not a bool' in 1 not of type bool" in ex.errors[0] else: assert False def test_one_of_with_custom_types(): # test for fixed validation of one_of specs with values that are # not lists or dicts (e.g. recursive) spec = one_of([str], recursive({required('foo'): string_type})) validate(spec, ['foo', 'bar']) validate(spec, {'foo': 'bar'}) try: validate(spec, {'nofoo': 'bar'}) except ValidationError as ex: assert "missing 'foo'" in ex.errors[0] else: assert False if __name__ == '__main__': unittest.main() ������������������������������������mapproxy-1.11.0/mapproxy/util/ext/dictspec/validator.py���������������������������������������������0000664�0000000�0000000�00000014477�13204544724�0023264�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2011, Oliver Tonnhofer <olt@omniscale.de> # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. import re from contextlib import contextmanager from .spec import required, one_of, anything, recursive from mapproxy.compat import iteritems, iterkeys, text_type class Context(object): def __init__(self): self.recurse_spec = None self.obj_pos = [] def push(self, spec): self.obj_pos.append(spec) def pop(self): return self.obj_pos.pop() @contextmanager def pos(self, spec): self.push(spec) yield self.pop() @property def current_pos(self): return ''.join(self.obj_pos).lstrip('.') or '.' def validate(spec, data): """ Validate `data` against `spec`. """ return Validator(spec).validate(data) class ValidationError(TypeError): def __init__(self, msg, errors=None, informal_only=False): TypeError.__init__(self, msg) self.informal_only = informal_only self.errors = errors or [] class SpecError(TypeError): pass class Validator(object): def __init__(self, spec, fail_fast=False): """ :params fail_fast: True if it should raise on the first error """ self.context = Context() self.complete_spec = spec self.raise_first_error = fail_fast self.errors = False self.messages = [] def validate(self, data): self._validate_part(self.complete_spec, data) if self.messages: if len(self.messages) == 1: raise ValidationError(self.messages[0], self.messages, informal_only=not self.errors) else: raise ValidationError('found %d validation errors.' % len(self.messages), self.messages, informal_only=not self.errors) def _validate_part(self, spec, data): if hasattr(spec, 'subspec'): try: spec = spec.subspec(data, self.context) except ValueError as ex: return self._handle_error(str(ex)) if isinstance(spec, recursive): if spec.spec: self.context.recurse_spec = spec.spec self._validate_part(spec.spec, data) self.context.recurse_spec = None return else: spec = self.context.recurse_spec if spec is None: raise SpecError('found recursive() outside recursive spec') if isinstance(spec, anything): return if data is None: data = {} if isinstance(spec, one_of): # check if at least one spec type matches for subspec in spec.specs: if type_matches(subspec, data): self._validate_part(subspec, data) return else: return self._handle_error("%r in %s not of any type %s" % (data, self.context.current_pos, ', '.join(map(type_str, spec.specs)))) elif not type_matches(spec, data): return self._handle_error("%r in %s not of type %s" % (data, self.context.current_pos, type_str(spec))) # recurse in dicts and lists if isinstance(spec, dict): self._validate_dict(spec, data) elif isinstance(spec, list): self._validate_list(spec, data) def _validate_dict(self, spec, data): accept_any_key = False any_key_spec = None for k in iterkeys(spec): if isinstance(k, required): if k not in data: self._handle_error("missing '%s', not in %s" % (k, self.context.current_pos)) if isinstance(k, anything): accept_any_key = True any_key_spec = spec[k] for k, v in iteritems(data): if accept_any_key: with self.context.pos('.' + text_type(k)): self._validate_part(any_key_spec, v) else: if k not in spec: self._handle_error("unknown '%s' in %s" % (k, self.context.current_pos), info_only=True) continue with self.context.pos('.' + text_type(k)): self._validate_part(spec[k], v) def _validate_list(self, spec, data): if not len(spec) == 1: raise SpecError('lists support only one type, got: %s' % spec) for i, v in enumerate(data): with self.context.pos('[%d]' % i): self._validate_part(spec[0], v) def _handle_error(self, msg, info_only=False): if not info_only: self.errors = True if self.raise_first_error and not info_only: raise ValidationError(msg) self.messages.append(msg) def type_str(spec): if not isinstance(spec, type): spec = type(spec) match = re.match("<type '(\w+)'>", str(spec)) if match: return match.group(1) match = re.match("<class '([\w._]+)'>", str(spec)) if match: return match.group(1).split('.')[-1] return str(type) def type_matches(spec, data): if hasattr(spec, 'compare_type'): return spec.compare_type(data) if isinstance(spec, type): spec_type = spec else: spec_type = type(spec) return isinstance(data, spec_type) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/local.py����������������������������������������������������������0000664�0000000�0000000�00000013445�13204544724�0020565�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -*- coding: utf-8 -*- """ This module implements context-local objects. This is a partial version of werkzeug/local.py containing only Local and StackLocal. Last update: 2011-03-15 9ada59c958b2edbb9739fb55a6b32ef4a97dac07 :copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ try: from greenlet import getcurrent as get_current_greenlet except ImportError: # pragma: no cover try: from py.magic import greenlet get_current_greenlet = greenlet.getcurrent del greenlet except Exception: # catch all, py.* fails with so many different errors. get_current_greenlet = int try: from _thread import get_ident as get_current_thread, allocate_lock except ImportError: # pragma: no cover try: from thread import get_ident as get_current_thread, allocate_lock except ImportError: # pragma: no cover from dummy_thread import get_ident as get_current_thread, allocate_lock # get the best ident function. if greenlets are not installed we can # safely just use the builtin thread function and save a python methodcall # and the cost of calculating a hash. if get_current_greenlet is int: # pragma: no cover get_ident = get_current_thread else: get_ident = lambda: (get_current_thread(), get_current_greenlet()) def release_local(local): """Releases the contents of the local for the current context. This makes it possible to use locals without a manager. Example:: >>> loc = Local() >>> loc.foo = 42 >>> release_local(loc) >>> hasattr(loc, 'foo') False With this function one can release :class:`Local` objects as well as :class:`StackLocal` objects. However it is not possible to release data held by proxies that way, one always has to retain a reference to the underlying local object in order to be able to release it. .. versionadded:: 0.6.1 """ local.__release_local__() class Local(object): __slots__ = ('__storage__', '__lock__', '__ident_func__') def __init__(self): object.__setattr__(self, '__storage__', {}) object.__setattr__(self, '__lock__', allocate_lock()) object.__setattr__(self, '__ident_func__', get_ident) def __iter__(self): return self.__storage__.iteritems() def __call__(self, proxy): """Create a proxy for a name.""" return LocalProxy(self, proxy) def __release_local__(self): self.__storage__.pop(self.__ident_func__(), None) def __getattr__(self, name): try: return self.__storage__[self.__ident_func__()][name] except KeyError: raise AttributeError(name) def __setattr__(self, name, value): ident = self.__ident_func__() self.__lock__.acquire() try: storage = self.__storage__ if ident in storage: storage[ident][name] = value else: storage[ident] = {name: value} finally: self.__lock__.release() def __delattr__(self, name): try: del self.__storage__[self.__ident_func__()][name] except KeyError: raise AttributeError(name) class LocalStack(object): """This class works similar to a :class:`Local` but keeps a stack of objects instead. This is best explained with an example:: >>> ls = LocalStack() >>> ls.push(42) [42] >>> ls.top 42 >>> ls.push(23) [42, 23] >>> ls.top 23 >>> ls.pop() 23 >>> ls.top 42 They can be force released by using a :class:`LocalManager` or with the :func:`release_local` function but the correct way is to pop the item from the stack after using. When the stack is empty it will no longer be bound to the current context (and as such released). By calling the stack without arguments it returns a proxy that resolves to the topmost item on the stack. .. versionadded:: 0.6.1 """ def __init__(self): self._local = Local() self._lock = allocate_lock() def __release_local__(self): self._local.__release_local__() def _get__ident_func__(self): return self._local.__ident_func__ def _set__ident_func__(self, value): object.__setattr__(self._local, '__ident_func__', value) __ident_func__ = property(_get__ident_func__, _set__ident_func__) del _get__ident_func__, _set__ident_func__ def __call__(self): def _lookup(): rv = self.top if rv is None: raise RuntimeError('object unbound') return rv return LocalProxy(_lookup) def push(self, obj): """Pushes a new item to the stack""" self._lock.acquire() try: rv = getattr(self._local, 'stack', None) if rv is None: self._local.stack = rv = [] rv.append(obj) return rv finally: self._lock.release() def pop(self): """Removes the topmost item from the stack, will return the old value or `None` if the stack was already empty. """ self._lock.acquire() try: stack = getattr(self._local, 'stack', None) if stack is None: return None elif len(stack) == 1: release_local(self._local) return stack[-1] else: return stack.pop() finally: self._lock.release() @property def top(self): """The topmost item on the stack. If the stack is empty, `None` is returned. """ try: return self._local.stack[-1] except (AttributeError, IndexError): return None ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/lockfile.py�������������������������������������������������������0000664�0000000�0000000�00000010700�13204544724�0021252�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������############################################################################## # # This is a modified version of zc.lockfile 1.0.0 # (http://pypi.python.org/pypi/zc.lockfile/1.0.0) # # Copyright (c) 2001, 2002 Zope Corporation and Contributors. # All Rights Reserved. # # ==== Changelog ==== # 2010-04-01 - Commented out logging. <olt@omniscale.de> # # ==== License ==== # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). # # Zope Public License (ZPL) Version 2.1 # # A copyright notice accompanies this license document that identifies the # copyright holders. # # This license has been certified as open source. It has also been designated as # GPL compatible by the Free Software Foundation (FSF). # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # - Redistributions in source code must retain the accompanying copyright # notice, this list of conditions, and the following disclaimer. # # - Redistributions in binary form must reproduce the accompanying copyright # notice, this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # - Names of the copyright holders must not be used to endorse or promote # products derived from this software without prior written permission from # the copyright holders. # # - The right to distribute this software or to use it for any purpose does not # give you the right to use Servicemarks (sm) or Trademarks (tm) of the # copyright holders. Use of them is covered by separate agreement with the # copyright holders. # # - If any files are modified, you must cause the modified files to carry # prominent notices stating that you changed the files and the date of any # change. # # Disclaimer # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO # EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE ############################################################################## import os # import logging # logger = logging.getLogger("zc.lockfile") class LockError(Exception): """Couldn't get a lock """ try: import fcntl except ImportError: try: import msvcrt except ImportError: def _lock_file(file): raise TypeError('No file-locking support on this platform') def _unlock_file(file): raise TypeError('No file-locking support on this platform') else: # Windows def _lock_file(file): # Lock just the first byte try: msvcrt.locking(file.fileno(), msvcrt.LK_NBLCK, 1) except IOError: raise LockError("Couldn't lock %r" % file.name) def _unlock_file(file): try: file.seek(0) msvcrt.locking(file.fileno(), msvcrt.LK_UNLCK, 1) except IOError: raise LockError("Couldn't unlock %r" % file.name) else: # Unix _flags = fcntl.LOCK_EX | fcntl.LOCK_NB def _lock_file(file): try: fcntl.flock(file.fileno(), _flags) except IOError: raise LockError("Couldn't lock %r" % file.name) def _unlock_file(file): # File is automatically unlocked on close pass class LockFile: _fp = None def __init__(self, path): self._path = path fp = open(path, 'w+') try: _lock_file(fp) except Exception as ex: try: fp.close() except Exception: pass raise ex self._fp = fp fp.write(" %s\n" % os.getpid()) fp.truncate() fp.flush() def close(self): if self._fp is not None: _unlock_file(self._fp) self._fp.close() self._fp = None ����������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/odict.py����������������������������������������������������������0000664�0000000�0000000�00000025130�13204544724�0020567�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -*- coding: utf-8 -*- """ odict ~~~~~ This module is an example implementation of an ordered dict for the collections module. It's not written for performance (it actually performs pretty bad) but to show how the API works. Questions and Answers ===================== Why would anyone need ordered dicts? Dicts in python are unordered which means that the order of items when iterating over dicts is undefined. As a matter of fact it is most of the time useless and differs from implementation to implementation. Many developers stumble upon that problem sooner or later when comparing the output of doctests which often does not match the order the developer thought it would. Also XML systems such as Genshi have their problems with unordered dicts as the input and output ordering of tag attributes is often mixed up because the ordering is lost when converting the data into a dict. Switching to lists is often not possible because the complexity of a lookup is too high. Another very common case is metaprogramming. The default namespace of a class in python is a dict. With Python 3 it becomes possible to replace it with a different object which could be an ordered dict. Django is already doing something similar with a hack that assigns numbers to some descriptors initialized in the class body of a specific subclass to restore the ordering after class creation. When porting code from programming languages such as PHP and Ruby where the item-order in a dict is guaranteed it's also a great help to have an equivalent data structure in Python to ease the transition. Where are new keys added? At the end. This behavior is consistent with Ruby 1.9 Hashmaps and PHP Arrays. It also matches what common ordered dict implementations do currently. What happens if an existing key is reassigned? The key is *not* moved. This is consitent with existing implementations and can be changed by a subclass very easily:: class movingodict(odict): def __setitem__(self, key, value): self.pop(key, None) odict.__setitem__(self, key, value) Moving keys to the end of a ordered dict on reassignment is not very useful for most applications. Does it mean the dict keys are sorted by a sort expression? That's not the case. The odict only guarantees that there is an order and that newly inserted keys are inserted at the end of the dict. If you want to sort it you can do so, but newly added keys are again added at the end of the dict. I initializes the odict with a dict literal but the keys are not ordered like they should! Dict literals in Python generate dict objects and as such the order of their items is not guaranteed. Before they are passed to the odict constructor they are already unordered. What happens if keys appear multiple times in the list passed to the constructor? The same as for the dict. The latter item overrides the former. This has the side-effect that the position of the first key is used because the key is actually overwritten: >>> odict([('a', 1), ('b', 2), ('a', 3)]) odict.odict([('a', 3), ('b', 2)]) This behavor is consistent with existing implementation in Python and the PHP array and the hashmap in Ruby 1.9. This odict doesn't scale! Yes it doesn't. The delitem operation is O(n). This is file is a mockup of a real odict that could be implemented for collections based on an linked list. Why is there no .insert()? There are few situations where you really want to insert a key at an specified index. To now make the API too complex the proposed solution for this situation is creating a list of items, manipulating that and converting it back into an odict: >>> d = odict([('a', 42), ('b', 23), ('c', 19)]) >>> l = d.items() >>> l.insert(1, ('x', 0)) >>> odict(l) odict.odict([('a', 42), ('x', 0), ('b', 23), ('c', 19)]) :copyright: (c) 2008 by Armin Ronacher and PEP 273 authors. :license: modified BSD license. """ from __future__ import absolute_import from mapproxy.compat import iteritems from mapproxy.compat.itertools import izip, imap from copy import deepcopy missing = object() class odict(dict): """ Ordered dict example implementation. This is the proposed interface for a an ordered dict as proposed on the Python mailinglist (proposal_). It's a dict subclass and provides some list functions. The implementation of this class is inspired by the implementation of Babel but incorporates some ideas from the `ordereddict`_ and Django's ordered dict. The constructor and `update()` both accept iterables of tuples as well as mappings: >>> d = odict([('a', 'b'), ('c', 'd')]) >>> d.update({'foo': 'bar'}) >>> d odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar')]) Keep in mind that when updating from dict-literals the order is not preserved as these dicts are unsorted! You can copy an odict like a dict by using the constructor, `copy.copy` or the `copy` method and make deep copies with `copy.deepcopy`: >>> from copy import copy, deepcopy >>> copy(d) odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar')]) >>> d.copy() odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar')]) >>> odict(d) odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar')]) >>> d['spam'] = [] >>> d2 = deepcopy(d) >>> d2['spam'].append('eggs') >>> d odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar'), ('spam', [])]) >>> d2 odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar'), ('spam', ['eggs'])]) All iteration methods as well as `keys`, `values` and `items` return the values ordered by the the time the key-value pair is inserted: >>> d.keys() ['a', 'c', 'foo', 'spam'] >>> list(d.values()) ['b', 'd', 'bar', []] >>> list(d.items()) [('a', 'b'), ('c', 'd'), ('foo', 'bar'), ('spam', [])] >>> list(d.iterkeys()) ['a', 'c', 'foo', 'spam'] >>> list(d.itervalues()) ['b', 'd', 'bar', []] >>> list(d.iteritems()) [('a', 'b'), ('c', 'd'), ('foo', 'bar'), ('spam', [])] Index based lookup is supported too by `byindex` which returns the key/value pair for an index: >>> d.byindex(2) ('foo', 'bar') You can reverse the odict as well: >>> d.reverse() >>> d odict.odict([('spam', []), ('foo', 'bar'), ('c', 'd'), ('a', 'b')]) And sort it like a list: >>> d.sort(key=lambda x: x[0].lower()) >>> d odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar'), ('spam', [])]) .. _proposal: http://thread.gmane.org/gmane.comp.python.devel/95316 .. _ordereddict: http://www.xs4all.nl/~anthon/Python/ordereddict/ """ def __init__(self, *args, **kwargs): dict.__init__(self) self._keys = [] self.update(*args, **kwargs) def __delitem__(self, key): dict.__delitem__(self, key) self._keys.remove(key) def __setitem__(self, key, item): if key not in self: self._keys.append(key) dict.__setitem__(self, key, item) def __deepcopy__(self, memo=None): if memo is None: memo = {} d = memo.get(id(self), missing) if d is not missing: return d memo[id(self)] = d = self.__class__() dict.__init__(d, deepcopy(self.items(), memo)) d._keys = self._keys[:] return d def __getstate__(self): return {'items': dict(self), 'keys': self._keys} def __setstate__(self, d): self._keys = d['keys'] dict.update(d['items']) def __reversed__(self): return reversed(self._keys) def __eq__(self, other): if isinstance(other, odict): if not dict.__eq__(self, other): return False return self.items() == other.items() return dict.__eq__(self, other) def __ne__(self, other): return not self.__eq__(other) def __cmp__(self, other): if isinstance(other, odict): return cmp(self.items(), other.items()) elif isinstance(other, dict): return dict.__cmp__(self, other) return NotImplemented @classmethod def fromkeys(cls, iterable, default=None): return cls((key, default) for key in iterable) def clear(self): del self._keys[:] dict.clear(self) def copy(self): return self.__class__(self) def items(self): return list(zip(self._keys, self.values())) def iteritems(self): return izip(self._keys, self.itervalues()) def keys(self): return self._keys[:] def iterkeys(self): return iter(self._keys) def pop(self, key, default=missing): if default is missing: return dict.pop(self, key) elif key not in self: return default self._keys.remove(key) return dict.pop(self, key, default) def popitem(self, key): self._keys.remove(key) return dict.popitem(key) def setdefault(self, key, default=None): if key not in self: self._keys.append(key) return dict.setdefault(self, key, default) def update(self, *args, **kwargs): sources = [] if len(args) == 1: if hasattr(args[0], 'iteritems') or hasattr(args[0], 'items'): sources.append(iteritems(args[0])) else: sources.append(iter(args[0])) elif args: raise TypeError('expected at most one positional argument') if kwargs: sources.append(kwargs.iteritems()) for iterable in sources: for key, val in iterable: self[key] = val def values(self): return map(self.get, self._keys) def itervalues(self): return imap(self.get, self._keys) def index(self, item): return self._keys.index(item) def byindex(self, item): key = self._keys[item] return (key, dict.__getitem__(self, key)) def reverse(self): self._keys.reverse() def sort(self, *args, **kwargs): self._keys.sort(*args, **kwargs) def __repr__(self): return 'odict.odict(%r)' % self.items() __copy__ = copy __iter__ = iterkeys if __name__ == '__main__': import doctest doctest.testmod()����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/serving.py��������������������������������������������������������0000664�0000000�0000000�00000045357�13204544724�0021157�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -*- coding: utf-8 -*- """ WSGI server code for `mapproxy-util serve-develop`. Supports automatic reloading and proper ^C handling. Stripped down version of werkzeug.serving. :copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ import os import socket import sys import time import signal import subprocess try: import thread except ImportError: import _thread as thread try: from SocketServer import ThreadingMixIn from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler except ImportError: from socketserver import ThreadingMixIn from http.server import HTTPServer, BaseHTTPRequestHandler from mapproxy.compat import iteritems, PY2, text_type from mapproxy.compat.itertools import chain from mapproxy.util.py import reraise if PY2: def wsgi_encoding_dance(s, charset='utf-8', errors='replace'): if isinstance(s, bytes): return s return s.encode(charset, errors) else: def wsgi_encoding_dance(s, charset='utf-8', errors='replace'): if isinstance(s, text_type): s = s.encode(charset) return s.decode('latin1', errors) try: from urllib.parse import urlparse as url_parse, unquote as url_unquote except ImportError: from urlparse import urlparse as url_parse, unquote as url_unquote # from werkzeug.urls import url_parse, url_unquote # from werkzeug.exceptions import InternalServerError, BadRequest import mapproxy.version def _log(type, message, *args): if args: message = message % args sys.stderr.write('[%s] %s\n' % (type, message.rstrip())) sys.stderr.flush() class WSGIRequestHandler(BaseHTTPRequestHandler, object): """A request handler that implements WSGI dispatching.""" @property def server_version(self): return 'MapProxy/' + mapproxy.version.__version__ + ' (Werkzeug based)' def make_environ(self): request_url = url_parse(self.path) def shutdown_server(): self.server.shutdown_signal = True url_scheme = 'http' path_info = url_unquote(request_url.path) environ = { 'wsgi.version': (1, 0), 'wsgi.url_scheme': url_scheme, 'wsgi.input': self.rfile, 'wsgi.errors': sys.stderr, 'wsgi.multithread': self.server.multithread, 'wsgi.multiprocess': self.server.multiprocess, 'wsgi.run_once': False, 'werkzeug.server.shutdown': shutdown_server, 'SERVER_SOFTWARE': self.server_version, 'REQUEST_METHOD': self.command, 'SCRIPT_NAME': '', 'PATH_INFO': wsgi_encoding_dance(path_info), 'QUERY_STRING': wsgi_encoding_dance(request_url.query), 'CONTENT_TYPE': self.headers.get('Content-Type', ''), 'CONTENT_LENGTH': self.headers.get('Content-Length', ''), 'REMOTE_ADDR': self.client_address[0], 'REMOTE_PORT': self.client_address[1], 'SERVER_NAME': self.server.server_address[0], 'SERVER_PORT': str(self.server.server_address[1]), 'SERVER_PROTOCOL': self.request_version } for key, value in self.headers.items(): key = 'HTTP_' + key.upper().replace('-', '_') if key not in ('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH'): environ[key] = value if request_url.netloc: environ['HTTP_HOST'] = request_url.netloc return environ def run_wsgi(self): if self.headers.get('Expect', '').lower().strip() == '100-continue': self.wfile.write(b'HTTP/1.1 100 Continue\r\n\r\n') environ = self.make_environ() headers_set = [] headers_sent = [] def write(data): assert headers_set, 'write() before start_response' if not headers_sent: status, response_headers = headers_sent[:] = headers_set try: code, msg = status.split(None, 1) except ValueError: code, msg = status, "" self.send_response(int(code), msg) header_keys = set() for key, value in response_headers: self.send_header(key, value) key = key.lower() header_keys.add(key) if 'content-length' not in header_keys: self.close_connection = True self.send_header('Connection', 'close') if 'server' not in header_keys: self.send_header('Server', self.version_string()) if 'date' not in header_keys: self.send_header('Date', self.date_time_string()) self.end_headers() assert type(data) is bytes, 'applications must write bytes' self.wfile.write(data) self.wfile.flush() def start_response(status, response_headers, exc_info=None): if exc_info: try: if headers_sent: reraise(*exc_info) finally: exc_info = None elif headers_set: raise AssertionError('Headers already set') headers_set[:] = [status, response_headers] return write def execute(app): application_iter = app(environ, start_response) try: for data in application_iter: write(data) if not headers_sent: write(b'') finally: if hasattr(application_iter, 'close'): application_iter.close() application_iter = None try: execute(self.server.app) except (socket.error, socket.timeout) as e: self.connection_dropped(e, environ) except Exception: if self.server.passthrough_errors: raise from werkzeug.debug.tbtools import get_current_traceback traceback = get_current_traceback(ignore_system_exceptions=True) try: # if we haven't yet sent the headers but they are set # we roll back to be able to set them again. if not headers_sent: del headers_set[:] execute(InternalServerError()) except Exception: pass self.server.log('error', 'Error on request:\n%s', traceback.plaintext) def handle(self): """Handles a request ignoring dropped connections.""" rv = None try: rv = BaseHTTPRequestHandler.handle(self) except (socket.error, socket.timeout) as e: self.connection_dropped(e) except Exception: raise if self.server.shutdown_signal: self.initiate_shutdown() return rv def initiate_shutdown(self): """A horrible, horrible way to kill the server for Python 2.6 and later. It's the best we can do. """ # Windows does not provide SIGKILL, go with SIGTERM then. sig = getattr(signal, 'SIGKILL', signal.SIGTERM) # reloader active if os.environ.get('WERKZEUG_RUN_MAIN') == 'true': os.kill(os.getpid(), sig) # python 2.7 self.server._BaseServer__shutdown_request = True # python 2.6 self.server._BaseServer__serving = False def connection_dropped(self, error, environ=None): """Called if the connection was closed by the client. By default nothing happens. """ def handle_one_request(self): """Handle a single HTTP request.""" self.raw_requestline = self.rfile.readline() if not self.raw_requestline: self.close_connection = 1 elif self.parse_request(): return self.run_wsgi() def send_response(self, code, message=None): """Send the response header and log the response code.""" self.log_request(code) if message is None: message = code in self.responses and self.responses[code][0] or '' if self.request_version != 'HTTP/0.9': hdr = "%s %d %s\r\n" % (self.protocol_version, code, message) self.wfile.write(hdr.encode('ascii')) def version_string(self): return BaseHTTPRequestHandler.version_string(self).strip() def address_string(self): return self.client_address[0] def log_request(self, code='-', size='-'): self.log('info', '"%s" %s %s', self.requestline, code, size) def log_error(self, *args): self.log('error', *args) def log_message(self, format, *args): self.log('info', format, *args) def log(self, type, message, *args): _log(type, '%s - - [%s] %s\n' % (self.address_string(), self.log_date_time_string(), message % args)) #: backwards compatible name if someone is subclassing it BaseRequestHandler = WSGIRequestHandler def select_ip_version(host, port): """Returns AF_INET4 or AF_INET6 depending on where to connect to.""" # disabled due to problems with current ipv6 implementations # and various operating systems. Probably this code also is # not supposed to work, but I can't come up with any other # ways to implement this. ##try: ## info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, ## socket.SOCK_STREAM, 0, ## socket.AI_PASSIVE) ## if info: ## return info[0][0] ##except socket.gaierror: ## pass if ':' in host and hasattr(socket, 'AF_INET6'): return socket.AF_INET6 return socket.AF_INET class BaseWSGIServer(HTTPServer, object): """Simple single-threaded, single-process WSGI server.""" multithread = False multiprocess = False request_queue_size = 128 def __init__(self, host, port, app, handler=None, passthrough_errors=False): if handler is None: handler = WSGIRequestHandler self.address_family = select_ip_version(host, port) HTTPServer.__init__(self, (host, int(port)), handler) self.app = app self.passthrough_errors = passthrough_errors self.shutdown_signal = False def log(self, type, message, *args): _log(type, message, *args) def serve_forever(self): self.shutdown_signal = False try: HTTPServer.serve_forever(self) except KeyboardInterrupt: pass def handle_error(self, request, client_address): if self.passthrough_errors: raise else: return HTTPServer.handle_error(self, request, client_address) def get_request(self): con, info = self.socket.accept() return con, info class ThreadedWSGIServer(ThreadingMixIn, BaseWSGIServer): """A WSGI server that does threading.""" multithread = True def _iter_module_files(): # The list call is necessary on Python 3 in case the module # dictionary modifies during iteration. for module in list(sys.modules.values()): filename = getattr(module, '__file__', None) if filename: old = None while not os.path.isfile(filename): old = filename filename = os.path.dirname(filename) if filename == old: break else: if filename[-4:] in ('.pyc', '.pyo'): filename = filename[:-1] yield filename def _reloader_stat_loop(extra_files=None, interval=1): """When this function is run from the main thread, it will force other threads to exit when any modules currently loaded change. Copyright notice. This function is based on the autoreload.py from the CherryPy trac which originated from WSGIKit which is now dead. :param extra_files: a list of additional files it should watch. """ mtimes = {} while 1: for filename in chain(_iter_module_files(), extra_files or ()): try: mtime = os.stat(filename).st_mtime except OSError: continue old_time = mtimes.get(filename) if old_time is None: mtimes[filename] = mtime continue elif mtime > old_time: _log('info', ' * Detected change in %r, reloading' % filename) sys.exit(3) time.sleep(interval) # currently we always use the stat loop reloader for the simple reason # that the inotify one does not respond to added files properly. Also # it's quite buggy and the API is a mess. reloader_loop = _reloader_stat_loop def restart_with_reloader(): """Spawn a new Python interpreter with the same arguments as this one, but running the reloader thread. """ while 1: _log('info', ' * Restarting with reloader') args = [sys.executable] + sys.argv if os.name == 'nt': # pip installs commands as .exe, but sys.argv[0] # can miss the prefix. # Add .exe to avoid file-not-found in subprocess call. # Also, recent pip versions create .exe commands that are not # executable by Python, but there is a -script.py which # we need to call in this case. Check for this first. if os.path.exists(args[1] + '-script.py'): args[1] = args[1] + '-script.py' elif not args[1].endswith('.exe'): args[1] = args[1] + '.exe' new_environ = os.environ.copy() new_environ['WERKZEUG_RUN_MAIN'] = 'true' # a weird bug on windows. sometimes unicode strings end up in the # environment and subprocess.call does not like this, encode them # to latin1 and continue. if os.name == 'nt' and PY2: for key, value in iteritems(new_environ): if isinstance(value, text_type): new_environ[key] = value.encode('iso-8859-1') exit_code = subprocess.call(args, env=new_environ) if exit_code != 3: return exit_code def run_with_reloader(main_func, extra_files=None, interval=1): """Run the given function in an independent python interpreter.""" import signal signal.signal(signal.SIGTERM, lambda *args: sys.exit(0)) if os.environ.get('WERKZEUG_RUN_MAIN') == 'true': thread.start_new_thread(main_func, ()) try: reloader_loop(extra_files, interval) except KeyboardInterrupt: return try: sys.exit(restart_with_reloader()) except KeyboardInterrupt: pass def run_simple(hostname, port, application, use_reloader=False, use_debugger=False, use_evalex=True, extra_files=None, reloader_interval=1, threaded=False, processes=1, request_handler=None, static_files=None, passthrough_errors=False): """Start an application using wsgiref and with an optional reloader. This wraps `wsgiref` to fix the wrong default reporting of the multithreaded WSGI variable and adds optional multithreading and fork support. This function has a command-line interface too:: python -m werkzeug.serving --help .. versionadded:: 0.5 `static_files` was added to simplify serving of static files as well as `passthrough_errors`. .. versionadded:: 0.6 support for SSL was added. .. versionadded:: 0.8 Added support for automatically loading a SSL context from certificate file and private key. .. versionadded:: 0.9 Added command-line interface. :param hostname: The host for the application. eg: ``'localhost'`` :param port: The port for the server. eg: ``8080`` :param application: the WSGI application to execute :param use_reloader: should the server automatically restart the python process if modules were changed? :param use_debugger: should the werkzeug debugging system be used? :param use_evalex: should the exception evaluation feature be enabled? :param extra_files: a list of files the reloader should watch additionally to the modules. For example configuration files. :param reloader_interval: the interval for the reloader in seconds. :param threaded: should the process handle each request in a separate thread? :param processes: if greater than 1 then handle each request in a new process up to this maximum number of concurrent processes. :param request_handler: optional parameter that can be used to replace the default one. You can use this to replace it with a different :class:`~BaseHTTPServer.BaseHTTPRequestHandler` subclass. :param static_files: a dict of paths for static files. This works exactly like :class:`SharedDataMiddleware`, it's actually just wrapping the application in that middleware before serving. :param passthrough_errors: set this to `True` to disable the error catching. This means that the server will die on errors but it can be useful to hook debuggers in (pdb etc.) """ if use_debugger: from werkzeug.debug import DebuggedApplication application = DebuggedApplication(application, use_evalex) def inner(): ThreadedWSGIServer(hostname, port, application, request_handler, passthrough_errors).serve_forever() if os.environ.get('WERKZEUG_RUN_MAIN') != 'true': display_hostname = hostname != '*' and hostname or 'localhost' if ':' in display_hostname: display_hostname = '[%s]' % display_hostname quit_msg = '(Press CTRL+C to quit)' _log('info', ' * Running on http://%s:%d/ %s', display_hostname, port, quit_msg) if use_reloader: # Create and destroy a socket so that any exceptions are raised before # we spawn a separate Python interpreter and lose this ability. address_family = select_ip_version(hostname, port) test_socket = socket.socket(address_family, socket.SOCK_STREAM) test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) test_socket.bind((hostname, port)) test_socket.close() run_with_reloader(inner, extra_files, reloader_interval) else: inner() ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/tempita/����������������������������������������������������������0000775�0000000�0000000�00000000000�13204544724�0020555�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/tempita/__init__.py�����������������������������������������������0000664�0000000�0000000�00000114265�13204544724�0022677�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������""" A small templating language This implements a small templating language. This language implements if/elif/else, for/continue/break, expressions, and blocks of Python code. The syntax is:: {{any expression (function calls etc)}} {{any expression | filter}} {{for x in y}}...{{endfor}} {{if x}}x{{elif y}}y{{else}}z{{endif}} {{py:x=1}} {{py: def foo(bar): return 'baz' }} {{default var = default_value}} {{# comment}} You use this with the ``Template`` class or the ``sub`` shortcut. The ``Template`` class takes the template string and the name of the template (for errors) and a default namespace. Then (like ``string.Template``) you can call the ``tmpl.substitute(**kw)`` method to make a substitution (or ``tmpl.substitute(a_dict)``). ``sub(content, **kw)`` substitutes the template immediately. You can use ``__name='tmpl.html'`` to set the name of the template. If there are syntax errors ``TemplateError`` will be raised. """ from __future__ import print_function import re import sys import cgi import os import tokenize from io import StringIO, BytesIO from mapproxy.compat import iteritems, PY2, text_type from mapproxy.util.py import reraise from mapproxy.util.ext.tempita._looper import looper from mapproxy.util.ext.tempita.compat3 import bytes, basestring_, next, is_unicode, coerce_text if PY2: from urllib import quote as url_quote else: from urllib.parse import quote as url_quote __all__ = ['TemplateError', 'Template', 'sub', 'HTMLTemplate', 'sub_html', 'html', 'bunch'] token_re = re.compile(r'\{\{|\}\}') in_re = re.compile(r'\s+in\s+') var_re = re.compile(r'^[a-z_][a-z0-9_]*$', re.I) class TemplateError(Exception): """Exception raised while parsing a template """ def __init__(self, message, position, name=None): Exception.__init__(self, message) self.position = position self.name = name def __str__(self): msg = ' '.join(self.args) if self.position: msg = '%s at line %s column %s' % ( msg, self.position[0], self.position[1]) if self.name: msg += ' in %s' % self.name return msg class _TemplateContinue(Exception): pass class _TemplateBreak(Exception): pass def get_file_template(name, from_template): path = os.path.join(os.path.dirname(from_template.name), name) return from_template.__class__.from_filename( path, namespace=from_template.namespace, get_template=from_template.get_template) class Template(object): default_namespace = { 'start_braces': '{{', 'end_braces': '}}', 'looper': looper, } default_encoding = 'utf8' default_inherit = None def __init__(self, content, name=None, namespace=None, stacklevel=None, get_template=None, default_inherit=None, line_offset=0): self.content = content self._unicode = is_unicode(content) if name is None and stacklevel is not None: try: caller = sys._getframe(stacklevel) except ValueError: pass else: globals = caller.f_globals lineno = caller.f_lineno if '__file__' in globals: name = globals['__file__'] if name.endswith('.pyc') or name.endswith('.pyo'): name = name[:-1] elif '__name__' in globals: name = globals['__name__'] else: name = '<string>' if lineno: name += ':%s' % lineno self.name = name self._parsed = parse(content, name=name, line_offset=line_offset) if namespace is None: namespace = {} self.namespace = namespace self.get_template = get_template if default_inherit is not None: self.default_inherit = default_inherit def from_filename(cls, filename, namespace=None, encoding=None, default_inherit=None, get_template=get_file_template): f = open(filename, 'rb') c = f.read() f.close() if encoding: c = c.decode(encoding) return cls(content=c, name=filename, namespace=namespace, default_inherit=default_inherit, get_template=get_template) from_filename = classmethod(from_filename) def __repr__(self): return '<%s %s name=%r>' % ( self.__class__.__name__, hex(id(self))[2:], self.name) def substitute(self, *args, **kw): if args: if kw: raise TypeError( "You can only give positional *or* keyword arguments") if len(args) > 1: raise TypeError( "You can only give one positional argument") if not hasattr(args[0], 'items'): raise TypeError( "If you pass in a single argument, you must pass in a dictionary-like object (with a .items() method); you gave %r" % (args[0],)) kw = args[0] ns = kw ns['__template_name__'] = self.name if self.namespace: ns.update(self.namespace) result, defs, inherit = self._interpret(ns) if not inherit: inherit = self.default_inherit if inherit: result = self._interpret_inherit(result, defs, inherit, ns) return result def _interpret(self, ns): __traceback_hide__ = True parts = [] defs = {} self._interpret_codes(self._parsed, ns, out=parts, defs=defs) if '__inherit__' in defs: inherit = defs.pop('__inherit__') else: inherit = None return ''.join(parts), defs, inherit def _interpret_inherit(self, body, defs, inherit_template, ns): __traceback_hide__ = True if not self.get_template: raise TemplateError( 'You cannot use inheritance without passing in get_template', position=None, name=self.name) templ = self.get_template(inherit_template, self) self_ = TemplateObject(self.name) for name, value in iteritems(defs): setattr(self_, name, value) self_.body = body ns = ns.copy() ns['self'] = self_ return templ.substitute(ns) def _interpret_codes(self, codes, ns, out, defs): __traceback_hide__ = True for item in codes: if isinstance(item, basestring_): out.append(item) else: self._interpret_code(item, ns, out, defs) def _interpret_code(self, code, ns, out, defs): __traceback_hide__ = True name, pos = code[0], code[1] if name == 'py': self._exec(code[2], ns, pos) elif name == 'continue': raise _TemplateContinue() elif name == 'break': raise _TemplateBreak() elif name == 'for': vars, expr, content = code[2], code[3], code[4] expr = self._eval(expr, ns, pos) self._interpret_for(vars, expr, content, ns, out, defs) elif name == 'cond': parts = code[2:] self._interpret_if(parts, ns, out, defs) elif name == 'expr': parts = code[2].split('|') base = self._eval(parts[0], ns, pos) for part in parts[1:]: func = self._eval(part, ns, pos) base = func(base) out.append(self._repr(base, pos)) elif name == 'default': var, expr = code[2], code[3] if var not in ns: result = self._eval(expr, ns, pos) ns[var] = result elif name == 'inherit': expr = code[2] value = self._eval(expr, ns, pos) defs['__inherit__'] = value elif name == 'def': name = code[2] signature = code[3] parts = code[4] ns[name] = defs[name] = TemplateDef(self, name, signature, body=parts, ns=ns, pos=pos) elif name == 'comment': return else: assert 0, "Unknown code: %r" % name def _interpret_for(self, vars, expr, content, ns, out, defs): __traceback_hide__ = True for item in expr: if len(vars) == 1: ns[vars[0]] = item else: if len(vars) != len(item): raise ValueError( 'Need %i items to unpack (got %i items)' % (len(vars), len(item))) for name, value in zip(vars, item): ns[name] = value try: self._interpret_codes(content, ns, out, defs) except _TemplateContinue: continue except _TemplateBreak: break def _interpret_if(self, parts, ns, out, defs): __traceback_hide__ = True # @@: if/else/else gets through for part in parts: assert not isinstance(part, basestring_) name, pos = part[0], part[1] if name == 'else': result = True else: result = self._eval(part[2], ns, pos) if result: self._interpret_codes(part[3], ns, out, defs) break def _eval(self, code, ns, pos): __traceback_hide__ = True try: try: value = eval(code, self.default_namespace, ns) except SyntaxError as e: raise SyntaxError( 'invalid syntax in expression: %s' % code) return value except: exc_info = sys.exc_info() e = exc_info[1] if getattr(e, 'args', None): arg0 = e.args[0] else: arg0 = coerce_text(e) e.args = (self._add_line_info(arg0, pos),) reraise((exc_info[0], e, exc_info[2])) def _exec(self, code, ns, pos): __traceback_hide__ = True try: exec(code, self.default_namespace, ns) except: exc_info = sys.exc_info() e = exc_info[1] if e.args: e.args = (self._add_line_info(e.args[0], pos),) else: e.args = (self._add_line_info(None, pos),) reraise((exc_info[0], e, exc_info[2])) def _repr(self, value, pos): __traceback_hide__ = True try: if value is None: return '' if self._unicode: try: value = text_type(value) except UnicodeDecodeError: value = bytes(value) else: if not isinstance(value, basestring_): value = coerce_text(value) if (is_unicode(value) and self.default_encoding): value = value.encode(self.default_encoding) except: exc_info = sys.exc_info() e = exc_info[1] e.args = (self._add_line_info(e.args[0], pos),) reraise((exc_info[0], e, exc_info[2])) else: if self._unicode and isinstance(value, bytes): if not self.default_encoding: raise UnicodeDecodeError( 'Cannot decode bytes value %r into unicode ' '(no default_encoding provided)' % value) try: value = value.decode(self.default_encoding) except UnicodeDecodeError as e: raise UnicodeDecodeError( e.encoding, e.object, e.start, e.end, e.reason + ' in string %r' % value) elif not self._unicode and is_unicode(value): if not self.default_encoding: raise UnicodeEncodeError( 'Cannot encode unicode value %r into bytes ' '(no default_encoding provided)' % value) value = value.encode(self.default_encoding) return value def _add_line_info(self, msg, pos): msg = "%s at line %s column %s" % ( msg, pos[0], pos[1]) if self.name: msg += " in file %s" % self.name return msg def sub(content, **kw): name = kw.get('__name') tmpl = Template(content, name=name) return tmpl.substitute(kw) def paste_script_template_renderer(content, vars, filename=None): tmpl = Template(content, name=filename) return tmpl.substitute(vars) class bunch(dict): def __init__(self, **kw): for name, value in iteritems(kw): setattr(self, name, value) def __setattr__(self, name, value): self[name] = value def __getattr__(self, name): try: return self[name] except KeyError: raise AttributeError(name) def __getitem__(self, key): if 'default' in self: try: return dict.__getitem__(self, key) except KeyError: return dict.__getitem__(self, 'default') else: return dict.__getitem__(self, key) def __repr__(self): items = [ (k, v) for k, v in iteritems(self)] items.sort() return '<%s %s>' % ( self.__class__.__name__, ' '.join(['%s=%r' % (k, v) for k, v in items])) ############################################################ ## HTML Templating ############################################################ class html(object): def __init__(self, value): self.value = value def __str__(self): return self.value def __html__(self): return self.value def __repr__(self): return '<%s %r>' % ( self.__class__.__name__, self.value) def html_quote(value, force=True): if not force and hasattr(value, '__html__'): return value.__html__() if value is None: return '' if not isinstance(value, basestring_): value = coerce_text(value) if sys.version >= "3" and isinstance(value, bytes): value = cgi.escape(value.decode('latin1'), 1) value = value.encode('latin1') else: value = cgi.escape(value, 1) if sys.version < "3": if is_unicode(value): value = value.encode('ascii', 'xmlcharrefreplace') return value def url(v): v = coerce_text(v) if is_unicode(v): v = v.encode('utf8') return url_quote(v) def attr(**kw): kw = list(kw.iteritems()) kw.sort() parts = [] for name, value in kw: if value is None: continue if name.endswith('_'): name = name[:-1] parts.append('%s="%s"' % (html_quote(name), html_quote(value))) return html(' '.join(parts)) class HTMLTemplate(Template): default_namespace = Template.default_namespace.copy() default_namespace.update(dict( html=html, attr=attr, url=url, html_quote=html_quote, )) def _repr(self, value, pos): if hasattr(value, '__html__'): value = value.__html__() quote = False else: quote = True plain = Template._repr(self, value, pos) if quote: return html_quote(plain) else: return plain def sub_html(content, **kw): name = kw.get('__name') tmpl = HTMLTemplate(content, name=name) return tmpl.substitute(kw) class TemplateDef(object): def __init__(self, template, func_name, func_signature, body, ns, pos, bound_self=None): self._template = template self._func_name = func_name self._func_signature = func_signature self._body = body self._ns = ns self._pos = pos self._bound_self = bound_self def __repr__(self): return '<tempita function %s(%s) at %s:%s>' % ( self._func_name, self._func_signature, self._template.name, self._pos) def __str__(self): return self() def __call__(self, *args, **kw): values = self._parse_signature(args, kw) ns = self._ns.copy() ns.update(values) if self._bound_self is not None: ns['self'] = self._bound_self out = [] subdefs = {} self._template._interpret_codes(self._body, ns, out, subdefs) return ''.join(out) def __get__(self, obj, type=None): if obj is None: return self return self.__class__( self._template, self._func_name, self._func_signature, self._body, self._ns, self._pos, bound_self=obj) def _parse_signature(self, args, kw): values = {} sig_args, var_args, var_kw, defaults = self._func_signature extra_kw = {} for name, value in iteritems(kw): if not var_kw and name not in sig_args: raise TypeError( 'Unexpected argument %s' % name) if name in sig_args: values[sig_args] = value else: extra_kw[name] = value args = list(args) sig_args = list(sig_args) while args: while sig_args and sig_args[0] in values: sig_args.pop(0) if sig_args: name = sig_args.pop(0) values[name] = args.pop(0) elif var_args: values[var_args] = tuple(args) break else: raise TypeError( 'Extra position arguments: %s' % ', '.join(repr(v) for v in args)) for name, value_expr in iteritems(defaults): if name not in values: values[name] = self._template._eval( value_expr, self._ns, self._pos) for name in sig_args: if name not in values: raise TypeError( 'Missing argument: %s' % name) if var_kw: values[var_kw] = extra_kw return values class TemplateObject(object): def __init__(self, name): self.__name = name self.get = TemplateObjectGetter(self) def __repr__(self): return '<%s %s>' % (self.__class__.__name__, self.__name) class TemplateObjectGetter(object): def __init__(self, template_obj): self.__template_obj = template_obj def __getattr__(self, attr): return getattr(self.__template_obj, attr, Empty) def __repr__(self): return '<%s around %r>' % (self.__class__.__name__, self.__template_obj) class _Empty(object): def __call__(self, *args, **kw): return self def __str__(self): return '' def __repr__(self): return 'Empty' def __unicode__(self): return u'' def __iter__(self): return iter(()) def __bool__(self): return False if sys.version < "3": __nonzero__ = __bool__ Empty = _Empty() del _Empty ############################################################ ## Lexing and Parsing ############################################################ def lex(s, name=None, trim_whitespace=True, line_offset=0): """ Lex a string into chunks: >>> lex('hey') ['hey'] >>> lex('hey {{you}}') ['hey ', ('you', (1, 7))] >>> lex('hey {{') # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TemplateError: No }} to finish last expression at line 1 column 7 >>> lex('hey }}') # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TemplateError: }} outside expression at line 1 column 7 >>> lex('hey {{ {{') # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TemplateError: {{ inside expression at line 1 column 10 """ in_expr = False chunks = [] last = 0 last_pos = (1, 1) for match in token_re.finditer(s): expr = match.group(0) pos = find_position(s, match.end(), line_offset) if expr == '{{' and in_expr: raise TemplateError('{{ inside expression', position=pos, name=name) elif expr == '}}' and not in_expr: raise TemplateError('}} outside expression', position=pos, name=name) if expr == '{{': part = s[last:match.start()] if part: chunks.append(part) in_expr = True else: chunks.append((s[last:match.start()], last_pos)) in_expr = False last = match.end() last_pos = pos if in_expr: raise TemplateError('No }} to finish last expression', name=name, position=last_pos) part = s[last:] if part: chunks.append(part) if trim_whitespace: chunks = trim_lex(chunks) return chunks statement_re = re.compile(r'^(?:if |elif |for |def |inherit |default |py:)') single_statements = ['else', 'endif', 'endfor', 'enddef', 'continue', 'break'] trail_whitespace_re = re.compile(r'\n\r?[\t ]*$') lead_whitespace_re = re.compile(r'^[\t ]*\n') def trim_lex(tokens): r""" Takes a lexed set of tokens, and removes whitespace when there is a directive on a line by itself: >>> tokens = lex('{{if x}}\nx\n{{endif}}\ny', trim_whitespace=False) >>> tokens [('if x', (1, 3)), '\nx\n', ('endif', (3, 3)), '\ny'] >>> trim_lex(tokens) [('if x', (1, 3)), 'x\n', ('endif', (3, 3)), 'y'] """ last_trim = None for i in range(len(tokens)): current = tokens[i] if isinstance(tokens[i], basestring_): # we don't trim this continue item = current[0] if not statement_re.search(item) and item not in single_statements: continue if not i: prev = '' else: prev = tokens[i - 1] if i + 1 >= len(tokens): next_chunk = '' else: next_chunk = tokens[i + 1] if (not isinstance(next_chunk, basestring_) or not isinstance(prev, basestring_)): continue prev_ok = not prev or trail_whitespace_re.search(prev) if i == 1 and not prev.strip(): prev_ok = True if last_trim is not None and last_trim + 2 == i and not prev.strip(): prev_ok = 'last' if (prev_ok and (not next_chunk or lead_whitespace_re.search(next_chunk) or (i == len(tokens) - 2 and not next_chunk.strip()))): if prev: if ((i == 1 and not prev.strip()) or prev_ok == 'last'): tokens[i - 1] = '' else: m = trail_whitespace_re.search(prev) # +1 to leave the leading \n on: prev = prev[:m.start() + 1] tokens[i - 1] = prev if next_chunk: last_trim = i if i == len(tokens) - 2 and not next_chunk.strip(): tokens[i + 1] = '' else: m = lead_whitespace_re.search(next_chunk) next_chunk = next_chunk[m.end():] tokens[i + 1] = next_chunk return tokens def find_position(string, index, line_offset): """Given a string and index, return (line, column)""" leading = string[:index].splitlines() return (len(leading) + line_offset, len(leading[-1]) + 1) def parse(s, name=None, line_offset=0): r""" Parses a string into a kind of AST >>> parse('{{x}}') [('expr', (1, 3), 'x')] >>> parse('foo') ['foo'] >>> parse('{{if x}}test{{endif}}') [('cond', (1, 3), ('if', (1, 3), 'x', ['test']))] >>> parse('series->{{for x in y}}x={{x}}{{endfor}}') ['series->', ('for', (1, 11), ('x',), 'y', ['x=', ('expr', (1, 27), 'x')])] >>> parse('{{for x, y in z:}}{{continue}}{{endfor}}') [('for', (1, 3), ('x', 'y'), 'z', [('continue', (1, 21))])] >>> parse('{{py:x=1}}') [('py', (1, 3), 'x=1')] >>> parse('{{if x}}a{{elif y}}b{{else}}c{{endif}}') [('cond', (1, 3), ('if', (1, 3), 'x', ['a']), ('elif', (1, 12), 'y', ['b']), ('else', (1, 23), None, ['c']))] Some exceptions:: >>> parse('{{continue}}') # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TemplateError: continue outside of for loop at line 1 column 3 >>> parse('{{if x}}foo') # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TemplateError: No {{endif}} at line 1 column 3 >>> parse('{{else}}') # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TemplateError: else outside of an if block at line 1 column 3 >>> parse('{{if x}}{{for x in y}}{{endif}}{{endfor}}') # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TemplateError: Unexpected endif at line 1 column 25 >>> parse('{{if}}{{endif}}') # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TemplateError: if with no expression at line 1 column 3 >>> parse('{{for x y}}{{endfor}}') # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TemplateError: Bad for (no "in") in 'x y' at line 1 column 3 >>> parse('{{py:x=1\ny=2}}') # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TemplateError: Multi-line py blocks must start with a newline at line 1 column 3 """ tokens = lex(s, name=name, line_offset=line_offset) result = [] while tokens: next_chunk, tokens = parse_expr(tokens, name) result.append(next_chunk) return result def parse_expr(tokens, name, context=()): if isinstance(tokens[0], basestring_): return tokens[0], tokens[1:] expr, pos = tokens[0] expr = expr.strip() if expr.startswith('py:'): expr = expr[3:].lstrip(' \t') if expr.startswith('\n') or expr.startswith('\r'): expr = expr.lstrip('\r\n') if '\r' in expr: expr = expr.replace('\r\n', '\n') expr = expr.replace('\r', '') expr += '\n' else: if '\n' in expr: raise TemplateError( 'Multi-line py blocks must start with a newline', position=pos, name=name) return ('py', pos, expr), tokens[1:] elif expr in ('continue', 'break'): if 'for' not in context: raise TemplateError( 'continue outside of for loop', position=pos, name=name) return (expr, pos), tokens[1:] elif expr.startswith('if '): return parse_cond(tokens, name, context) elif (expr.startswith('elif ') or expr == 'else'): raise TemplateError( '%s outside of an if block' % expr.split()[0], position=pos, name=name) elif expr in ('if', 'elif', 'for'): raise TemplateError( '%s with no expression' % expr, position=pos, name=name) elif expr in ('endif', 'endfor', 'enddef'): raise TemplateError( 'Unexpected %s' % expr, position=pos, name=name) elif expr.startswith('for '): return parse_for(tokens, name, context) elif expr.startswith('default '): return parse_default(tokens, name, context) elif expr.startswith('inherit '): return parse_inherit(tokens, name, context) elif expr.startswith('def '): return parse_def(tokens, name, context) elif expr.startswith('#'): return ('comment', pos, tokens[0][0]), tokens[1:] return ('expr', pos, tokens[0][0]), tokens[1:] def parse_cond(tokens, name, context): start = tokens[0][1] pieces = [] context = context + ('if',) while 1: if not tokens: raise TemplateError( 'Missing {{endif}}', position=start, name=name) if (isinstance(tokens[0], tuple) and tokens[0][0] == 'endif'): return ('cond', start) + tuple(pieces), tokens[1:] next_chunk, tokens = parse_one_cond(tokens, name, context) pieces.append(next_chunk) def parse_one_cond(tokens, name, context): (first, pos), tokens = tokens[0], tokens[1:] content = [] if first.endswith(':'): first = first[:-1] if first.startswith('if '): part = ('if', pos, first[3:].lstrip(), content) elif first.startswith('elif '): part = ('elif', pos, first[5:].lstrip(), content) elif first == 'else': part = ('else', pos, None, content) else: assert 0, "Unexpected token %r at %s" % (first, pos) while 1: if not tokens: raise TemplateError( 'No {{endif}}', position=pos, name=name) if (isinstance(tokens[0], tuple) and (tokens[0][0] == 'endif' or tokens[0][0].startswith('elif ') or tokens[0][0] == 'else')): return part, tokens next_chunk, tokens = parse_expr(tokens, name, context) content.append(next_chunk) def parse_for(tokens, name, context): first, pos = tokens[0] tokens = tokens[1:] context = ('for',) + context content = [] assert first.startswith('for ') if first.endswith(':'): first = first[:-1] first = first[3:].strip() match = in_re.search(first) if not match: raise TemplateError( 'Bad for (no "in") in %r' % first, position=pos, name=name) vars = first[:match.start()] if '(' in vars: raise TemplateError( 'You cannot have () in the variable section of a for loop (%r)' % vars, position=pos, name=name) vars = tuple([ v.strip() for v in first[:match.start()].split(',') if v.strip()]) expr = first[match.end():] while 1: if not tokens: raise TemplateError( 'No {{endfor}}', position=pos, name=name) if (isinstance(tokens[0], tuple) and tokens[0][0] == 'endfor'): return ('for', pos, vars, expr, content), tokens[1:] next_chunk, tokens = parse_expr(tokens, name, context) content.append(next_chunk) def parse_default(tokens, name, context): first, pos = tokens[0] assert first.startswith('default ') first = first.split(None, 1)[1] parts = first.split('=', 1) if len(parts) == 1: raise TemplateError( "Expression must be {{default var=value}}; no = found in %r" % first, position=pos, name=name) var = parts[0].strip() if ',' in var: raise TemplateError( "{{default x, y = ...}} is not supported", position=pos, name=name) if not var_re.search(var): raise TemplateError( "Not a valid variable name for {{default}}: %r" % var, position=pos, name=name) expr = parts[1].strip() return ('default', pos, var, expr), tokens[1:] def parse_inherit(tokens, name, context): first, pos = tokens[0] assert first.startswith('inherit ') expr = first.split(None, 1)[1] return ('inherit', pos, expr), tokens[1:] def parse_def(tokens, name, context): first, start = tokens[0] tokens = tokens[1:] assert first.startswith('def ') first = first.split(None, 1)[1] if first.endswith(':'): first = first[:-1] if '(' not in first: func_name = first sig = ((), None, None, {}) elif not first.endswith(')'): raise TemplateError("Function definition doesn't end with ): %s" % first, position=start, name=name) else: first = first[:-1] func_name, sig_text = first.split('(', 1) sig = parse_signature(sig_text, name, start) context = context + ('def',) content = [] while 1: if not tokens: raise TemplateError( 'Missing {{enddef}}', position=start, name=name) if (isinstance(tokens[0], tuple) and tokens[0][0] == 'enddef'): return ('def', start, func_name, sig, content), tokens[1:] next_chunk, tokens = parse_expr(tokens, name, context) content.append(next_chunk) def parse_signature(sig_text, name, pos): if PY2 and isinstance(sig_text, str): lines = BytesIO(sig_text).readline else: lines = StringIO(sig_text).readline tokens = tokenize.generate_tokens(lines) sig_args = [] var_arg = None var_kw = None defaults = {} def get_token(pos=False): try: tok_type, tok_string, (srow, scol), (erow, ecol), line = next(tokens) except StopIteration: return tokenize.ENDMARKER, '' if pos: return tok_type, tok_string, (srow, scol), (erow, ecol) else: return tok_type, tok_string while 1: var_arg_type = None tok_type, tok_string = get_token() if tok_type == tokenize.ENDMARKER: break if tok_type == tokenize.OP and (tok_string == '*' or tok_string == '**'): var_arg_type = tok_string tok_type, tok_string = get_token() if tok_type != tokenize.NAME: raise TemplateError('Invalid signature: (%s)' % sig_text, position=pos, name=name) var_name = tok_string tok_type, tok_string = get_token() if tok_type == tokenize.ENDMARKER or (tok_type == tokenize.OP and tok_string == ','): if var_arg_type == '*': var_arg = var_name elif var_arg_type == '**': var_kw = var_name else: sig_args.append(var_name) if tok_type == tokenize.ENDMARKER: break continue if var_arg_type is not None: raise TemplateError('Invalid signature: (%s)' % sig_text, position=pos, name=name) if tok_type == tokenize.OP and tok_string == '=': nest_type = None unnest_type = None nest_count = 0 start_pos = end_pos = None parts = [] while 1: tok_type, tok_string, s, e = get_token(True) if start_pos is None: start_pos = s end_pos = e if tok_type == tokenize.ENDMARKER and nest_count: raise TemplateError('Invalid signature: (%s)' % sig_text, position=pos, name=name) if (not nest_count and (tok_type == tokenize.ENDMARKER or (tok_type == tokenize.OP and tok_string == ','))): default_expr = isolate_expression(sig_text, start_pos, end_pos) defaults[var_name] = default_expr sig_args.append(var_name) break parts.append((tok_type, tok_string)) if nest_count and tok_type == tokenize.OP and tok_string == nest_type: nest_count += 1 elif nest_count and tok_type == tokenize.OP and tok_string == unnest_type: nest_count -= 1 if not nest_count: nest_type = unnest_type = None elif not nest_count and tok_type == tokenize.OP and tok_string in ('(', '[', '{'): nest_type = tok_string nest_count = 1 unnest_type = {'(': ')', '[': ']', '{': '}'}[nest_type] return sig_args, var_arg, var_kw, defaults def isolate_expression(string, start_pos, end_pos): srow, scol = start_pos srow -= 1 erow, ecol = end_pos erow -= 1 lines = string.splitlines(True) if srow == erow: return lines[srow][scol:ecol] parts = [lines[srow][scol:]] parts.extend(lines[srow+1:erow]) if erow < len(lines): # It'll sometimes give (end_row_past_finish, 0) parts.append(lines[erow][:ecol]) return ''.join(parts) _fill_command_usage = """\ %prog [OPTIONS] TEMPLATE arg=value Use py:arg=value to set a Python value; otherwise all values are strings. """ def fill_command(args=None): import sys import optparse import pkg_resources import os if args is None: args = sys.argv[1:] dist = pkg_resources.get_distribution('Paste') parser = optparse.OptionParser( version=coerce_text(dist), usage=_fill_command_usage) parser.add_option( '-o', '--output', dest='output', metavar="FILENAME", help="File to write output to (default stdout)") parser.add_option( '--html', dest='use_html', action='store_true', help="Use HTML style filling (including automatic HTML quoting)") parser.add_option( '--env', dest='use_env', action='store_true', help="Put the environment in as top-level variables") options, args = parser.parse_args(args) if len(args) < 1: print('You must give a template filename') sys.exit(2) template_name = args[0] args = args[1:] vars = {} if options.use_env: vars.update(os.environ) for value in args: if '=' not in value: print(('Bad argument: %r' % value)) sys.exit(2) name, value = value.split('=', 1) if name.startswith('py:'): name = name[:3] value = eval(value) vars[name] = value if template_name == '-': template_content = sys.stdin.read() template_name = '<stdin>' else: f = open(template_name, 'rb') template_content = f.read() f.close() if options.use_html: TemplateClass = HTMLTemplate else: TemplateClass = Template template = TemplateClass(template_content, name=template_name) result = template.substitute(vars) if options.output: f = open(options.output, 'wb') f.write(result) f.close() else: sys.stdout.write(result) if __name__ == '__main__': fill_command() �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/tempita/_looper.py������������������������������������������������0000664�0000000�0000000�00000010123�13204544724�0022563�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������""" Helper for looping over sequences, particular in templates. Often in a loop in a template it's handy to know what's next up, previously up, if this is the first or last item in the sequence, etc. These can be awkward to manage in a normal Python loop, but using the looper you can get a better sense of the context. Use like:: >>> for loop, item in looper(['a', 'b', 'c']): ... print loop.number, item ... if not loop.last: ... print '---' 1 a --- 2 b --- 3 c """ import sys from mapproxy.util.ext.tempita.compat3 import basestring_ __all__ = ['looper'] class looper(object): """ Helper for looping (particularly in templates) Use this like:: for loop, item in looper(seq): if loop.first: ... """ def __init__(self, seq): self.seq = seq def __iter__(self): return looper_iter(self.seq) def __repr__(self): return '<%s for %r>' % ( self.__class__.__name__, self.seq) class looper_iter(object): def __init__(self, seq): self.seq = list(seq) self.pos = 0 def __iter__(self): return self def __next__(self): if self.pos >= len(self.seq): raise StopIteration result = loop_pos(self.seq, self.pos), self.seq[self.pos] self.pos += 1 return result if sys.version < "3": next = __next__ class loop_pos(object): def __init__(self, seq, pos): self.seq = seq self.pos = pos def __repr__(self): return '<loop pos=%r at %r>' % ( self.seq[self.pos], self.pos) def index(self): return self.pos index = property(index) def number(self): return self.pos + 1 number = property(number) def item(self): return self.seq[self.pos] item = property(item) def __next__(self): try: return self.seq[self.pos + 1] except IndexError: return None __next__ = property(__next__) if sys.version < "3": next = __next__ def previous(self): if self.pos == 0: return None return self.seq[self.pos - 1] previous = property(previous) def odd(self): return not self.pos % 2 odd = property(odd) def even(self): return self.pos % 2 even = property(even) def first(self): return self.pos == 0 first = property(first) def last(self): return self.pos == len(self.seq) - 1 last = property(last) def length(self): return len(self.seq) length = property(length) def first_group(self, getter=None): """ Returns true if this item is the start of a new group, where groups mean that some attribute has changed. The getter can be None (the item itself changes), an attribute name like ``'.attr'``, a function, or a dict key or list index. """ if self.first: return True return self._compare_group(self.item, self.previous, getter) def last_group(self, getter=None): """ Returns true if this item is the end of a new group, where groups mean that some attribute has changed. The getter can be None (the item itself changes), an attribute name like ``'.attr'``, a function, or a dict key or list index. """ if self.last: return True return self._compare_group(self.item, self.__next__, getter) def _compare_group(self, item, other, getter): if getter is None: return item != other elif (isinstance(getter, basestring_) and getter.startswith('.')): getter = getter[1:] if getter.endswith('()'): getter = getter[:-2] return getattr(item, getter)() != getattr(other, getter)() else: return getattr(item, getter) != getattr(other, getter) elif hasattr(getter, '__call__'): return getter(item) != getter(other) else: return item[getter] != other[getter] ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/tempita/compat3.py������������������������������������������������0000664�0000000�0000000�00000001521�13204544724�0022474�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import sys __all__ = ['b', 'basestring_', 'bytes', 'next', 'is_unicode'] if sys.version < "3": b = bytes = str basestring_ = basestring else: def b(s): if isinstance(s, str): return s.encode('latin1') return bytes(s) basestring_ = (bytes, str) bytes = bytes text = str if sys.version < "3": def next(obj): return obj.next() else: next = next if sys.version < "3": def is_unicode(obj): return isinstance(obj, unicode) else: def is_unicode(obj): return isinstance(obj, str) def coerce_text(v): if not isinstance(v, basestring_): if sys.version < "3": attr = '__unicode__' else: attr = '__str__' if hasattr(v, attr): return unicode(v) else: return bytes(v) return v �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/wmsparse/���������������������������������������������������������0000775�0000000�0000000�00000000000�13204544724�0020753�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/wmsparse/__init__.py����������������������������������������������0000664�0000000�0000000�00000000107�13204544724�0023062�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from .parse import parse_capabilities __all__ = ['parse_capabilities']���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/wmsparse/parse.py�������������������������������������������������0000664�0000000�0000000�00000024514�13204544724�0022445�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import print_function import math from .util import resolve_ns from xml.etree import ElementTree as etree from mapproxy.compat import string_type from mapproxy.request.wms import switch_bbox_epsg_axis_order class WMSCapabilities(object): _default_namespace = None _namespaces = { 'xlink': 'http://www.w3.org/1999/xlink', } version = None def __init__(self, tree): self.tree = tree self._layer_tree = None def resolve_ns(self, xpath): return resolve_ns(xpath, self._namespaces, self._default_namespace) def findtext(self, tree, xpath): return tree.findtext(self.resolve_ns(xpath)) def find(self, tree, xpath): return tree.find(self.resolve_ns(xpath)) def findall(self, tree, xpath): return tree.findall(self.resolve_ns(xpath)) def attrib(self, elem, name): return elem.attrib[self.resolve_ns(name)] def metadata(self): md = dict( name = self.findtext(self.tree, 'Service/Name'), title = self.findtext(self.tree, 'Service/Title'), abstract = self.findtext(self.tree, 'Service/Abstract'), fees = self.findtext(self.tree, 'Service/Fees'), access_constraints = self.findtext(self.tree, 'Service/AccessConstraints'), ) elem = self.find(self.tree, 'Service/OnlineResource') if elem is not None: md['online_resource'] = self.attrib(elem, 'xlink:href') md['contact'] = self.parse_contact() return md def parse_contact(self): elem = self.find(self.tree, 'Service/ContactInformation') if elem is None or len(elem) is 0: elem = etree.Element(None) md = dict( person = self.findtext(elem, 'ContactPersonPrimary/ContactPerson'), organization = self.findtext(elem, 'ContactPersonPrimary/ContactOrganization'), position = self.findtext(elem, 'ContactPosition'), address = self.findtext(elem, 'ContactAddress/Address'), city = self.findtext(elem, 'ContactAddress/City'), postcode = self.findtext(elem, 'ContactAddress/PostCode'), country = self.findtext(elem, 'ContactAddress/Country'), phone = self.findtext(elem, 'ContactVoiceTelephone'), fax = self.findtext(elem, 'ContactFacsimileTelephone'), email = self.findtext(elem, 'ContactElectronicMailAddress'), ) return md def layers(self): if not self._layer_tree: root_layer = self.find(self.tree, 'Capability/Layer') self._layer_tree = self.parse_layer(root_layer, None) return self._layer_tree def layers_list(self): layers = [] def append_layer(layer): if layer.get('name'): layers.append(layer) for child_layer in layer.get('layers', []): append_layer(child_layer) append_layer(self.layers()) return layers def requests(self): requests_elem = self.find(self.tree, 'Capability/Request') resources = {} resource = self.find(requests_elem, 'GetMap/DCPType/HTTP/Get/OnlineResource') if resource != None: resources['GetMap'] = self.attrib(resource, 'xlink:href') return resources def parse_layer(self, layer_elem, parent_layer): child_layers = [] layer = self.parse_layer_data(layer_elem, parent_layer or {}) child_layer_elems = self.findall(layer_elem, 'Layer') for child_elem in child_layer_elems: child_layers.append(self.parse_layer(child_elem, layer)) layer['layers'] = child_layers return layer def parse_layer_data(self, elem, parent_layer): layer = dict( queryable=elem.attrib.get('queryable') == '1', opaque=elem.attrib.get('opaque') == '1', title=self.findtext(elem, 'Title'), abstract=self.findtext(elem, 'Abstract'), name=self.findtext(elem, 'Name'), ) layer['srs'] = self.layer_srs(elem, parent_layer) layer['res_hint'] = self.layer_res_hint(elem, parent_layer) layer['llbbox'] = self.layer_llbbox(elem, parent_layer) layer['bbox_srs'] = self.layer_bbox_srs(elem, parent_layer) layer['url'] = self.requests()['GetMap'] layer['legend'] = self.layer_legend(elem) return layer def layer_legend(self, elem): style_elems = self.findall(elem, 'Style') legend_elem = None # we don't support styles, but will use the # LegendURL for the default style for elem in style_elems: if self.findtext(elem, 'Name') in ('default', ''): legend_elem = self.find(elem, 'LegendURL') break if legend_elem is None: return legend = {} legend_url = self.find(legend_elem, 'OnlineResource') legend['url'] = self.attrib(legend_url, 'xlink:href') return legend def layer_res_hint(self, elem, parent_layer): elem = self.find(elem, 'ScaleHint') if elem is None: return parent_layer.get('res_hint') # ScaleHints are the diagonal pixel resolutions # NOTE: max is not the maximum resolution, but the max # value, so it's actualy the min_res min_res = elem.attrib.get('max') max_res = elem.attrib.get('min') if min_res: min_res = math.sqrt(float(min_res) ** 2 / 2.0) if max_res: max_res = math.sqrt(float(max_res) ** 2 / 2.0) return min_res, max_res class WMS111Capabilities(WMSCapabilities): version = '1.1.1' def layer_llbbox(self, elem, parent_layer): llbbox_elem = self.find(elem, 'LatLonBoundingBox') llbbox = None if llbbox_elem is not None: llbbox = ( llbbox_elem.attrib['minx'], llbbox_elem.attrib['miny'], llbbox_elem.attrib['maxx'], llbbox_elem.attrib['maxy'] ) llbbox = [float(x) for x in llbbox] elif parent_layer and 'llbbox' in parent_layer: llbbox = parent_layer['llbbox'] return llbbox def layer_srs(self, elem, parent_layer=None): srs_elements = self.findall(elem, 'SRS') srs_codes = set() for srs in srs_elements: srs = srs.text.strip().upper() if ' ' in srs: # handle multiple codes in one SRS tag (WMS 1.1.1 7.1.4.5.5) srs_codes.update(srs.split()) else: srs_codes.add(srs) # unique srs-codes in either srs or parent_layer['srs'] inherited_srs = parent_layer.get('srs', set()) if parent_layer else set() return srs_codes | inherited_srs def layer_bbox_srs(self, elem, parent_layer=None): bbox_srs = {} bbox_srs_elems = self.findall(elem, 'BoundingBox') if len(bbox_srs_elems) > 0: for bbox_srs_elem in bbox_srs_elems: srs = bbox_srs_elem.attrib['SRS'] bbox = ( bbox_srs_elem.attrib['minx'], bbox_srs_elem.attrib['miny'], bbox_srs_elem.attrib['maxx'], bbox_srs_elem.attrib['maxy'] ) bbox = [float(x) for x in bbox] bbox_srs[srs] = bbox elif parent_layer: bbox_srs = parent_layer['bbox_srs'] return bbox_srs class WMS130Capabilities(WMSCapabilities): version = '1.3.0' _default_namespace = 'http://www.opengis.net/wms' _ns = { 'sld': "http://www.opengis.net/sld", 'xlink': "http://www.w3.org/1999/xlink", } def layer_llbbox(self, elem, parent_layer): llbbox_elem = self.find(elem, 'EX_GeographicBoundingBox') llbbox = None if llbbox_elem is not None: llbbox = ( self.find(llbbox_elem, 'westBoundLongitude').text, self.find(llbbox_elem, 'southBoundLatitude').text, self.find(llbbox_elem, 'eastBoundLongitude').text, self.find(llbbox_elem, 'northBoundLatitude').text ) llbbox = [float(x) for x in llbbox] elif parent_layer and 'llbbox' in parent_layer: llbbox = parent_layer['llbbox'] return llbbox def layer_srs(self, elem, parent_layer=None): srs_elements = self.findall(elem, 'CRS') srs_codes = set([srs.text.strip().upper() for srs in srs_elements]) # unique srs-codes in either srs or parent_layer['srs'] inherited_srs = parent_layer.get('srs', set()) if parent_layer else set() return srs_codes | inherited_srs def layer_bbox_srs(self, elem, parent_layer=None): bbox_srs = {} bbox_srs_elems = self.findall(elem, 'BoundingBox') if len(bbox_srs_elems) > 0: for bbox_srs_elem in bbox_srs_elems: srs = bbox_srs_elem.attrib['CRS'] bbox = ( bbox_srs_elem.attrib['minx'], bbox_srs_elem.attrib['miny'], bbox_srs_elem.attrib['maxx'], bbox_srs_elem.attrib['maxy'] ) bbox = [float(x) for x in bbox] bbox = switch_bbox_epsg_axis_order(bbox, srs) bbox_srs[srs] = bbox elif parent_layer: bbox_srs = parent_layer['bbox_srs'] return bbox_srs def yaml_sources(cap): sources = {} for layer in cap.layers(): layer_name = layer['name'] + '_wms' req = dict(url='http://example', layers=layer['name']) if not layer['opaque']: req['transparent'] = True sources[layer_name] = dict( type='wms', req=req ) import yaml print(yaml.dump(dict(sources=sources), default_flow_style=False)) def parse_capabilities(fileobj): if isinstance(fileobj, string_type): fileobj = open(fileobj, 'rb') tree = etree.parse(fileobj) root_tag = tree.getroot().tag if root_tag == 'WMT_MS_Capabilities': return WMS111Capabilities(tree) elif root_tag == '{http://www.opengis.net/wms}WMS_Capabilities': return WMS130Capabilities(tree) else: raise ValueError('unknown start tag in capabilities: ' + root_tag) if __name__ == '__main__': import sys cap = parse_capabilities(sys.argv[1]) yaml_sources(cap) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/wmsparse/test/����������������������������������������������������0000775�0000000�0000000�00000000000�13204544724�0021732�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/wmsparse/test/__init__.py�����������������������������������������0000664�0000000�0000000�00000000000�13204544724�0024031�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/wmsparse/test/test_parse.py���������������������������������������0000664�0000000�0000000�00000010570�13204544724�0024460�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import os from ..parse import parse_capabilities from nose.tools import eq_ def local_filename(filename): return os.path.join(os.path.dirname(__file__), filename) class TestWMS111(object): def test_parse_metadata(self): cap = parse_capabilities(local_filename('wms-omniscale-111.xml')) md = cap.metadata() eq_(md['name'], 'OGC:WMS') eq_(md['title'], 'Omniscale OpenStreetMap WMS') eq_(md['access_constraints'], 'Here be dragons.') eq_(md['fees'], 'none') eq_(md['online_resource'], 'http://omniscale.de/') eq_(md['abstract'], 'Omniscale OpenStreetMap WMS (powered by MapProxy)') eq_(md['contact']['person'], 'Oliver Tonnhofer') eq_(md['contact']['organization'], 'Omniscale') eq_(md['contact']['position'], 'Technical Director') eq_(md['contact']['address'], 'Nadorster Str. 60') eq_(md['contact']['city'], 'Oldenburg') eq_(md['contact']['postcode'], '26123') eq_(md['contact']['country'], 'Germany') eq_(md['contact']['phone'], '+49(0)441-9392774-0') eq_(md['contact']['fax'], '+49(0)441-9392774-9') eq_(md['contact']['email'], 'osm@omniscale.de') def test_parse_layer(self): cap = parse_capabilities(local_filename('wms-omniscale-111.xml')) lyrs = cap.layers_list() eq_(len(lyrs), 2) eq_(lyrs[0]['llbbox'], [-180.0, -85.0511287798, 180.0, 85.0511287798]) eq_(lyrs[0]['srs'], set(['EPSG:4326', 'EPSG:4258', 'CRS:84', 'EPSG:900913', 'EPSG:31466', 'EPSG:31467', 'EPSG:31468', 'EPSG:25831', 'EPSG:25832', 'EPSG:25833', 'EPSG:3857', ]) ) eq_(len(lyrs[0]['bbox_srs']), 1) eq_(lyrs[0]['bbox_srs']['EPSG:4326'], [-180.0, -85.0511287798, 180.0, 85.0511287798]) def test_parse_layer_2(self): cap = parse_capabilities(local_filename('wms-large-111.xml')) lyrs = cap.layers_list() eq_(len(lyrs), 46) eq_(lyrs[0]['llbbox'], [-10.4, 35.7, 43.0, 74.1]) eq_(lyrs[0]['srs'], set(['EPSG:31467', 'EPSG:31466', 'EPSG:31465', 'EPSG:31464', 'EPSG:31463', 'EPSG:31462', 'EPSG:4326', 'EPSG:31469', 'EPSG:31468', 'EPSG:31257', 'EPSG:31287', 'EPSG:31286', 'EPSG:31285', 'EPSG:31284', 'EPSG:31258', 'EPSG:31259', 'EPSG:31492', 'EPSG:31493', 'EPSG:25833', 'EPSG:25832', 'EPSG:31494', 'EPSG:31495', 'EPSG:28992', ]) ) eq_(lyrs[1]['name'], 'Grenzen') eq_(lyrs[1]['legend']['url'], "http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Grenzen&format=image/png&STYLE=default" ) class TestWMS130(object): def test_parse_metadata(self): cap = parse_capabilities(local_filename('wms-omniscale-130.xml')) md = cap.metadata() eq_(md['name'], 'WMS') eq_(md['title'], 'Omniscale OpenStreetMap WMS') req = cap.requests() eq_(req['GetMap'], 'http://osm.omniscale.net/proxy/service?') def test_parse_layer(self): cap = parse_capabilities(local_filename('wms-omniscale-130.xml')) lyrs = cap.layers_list() eq_(len(lyrs), 2) eq_(lyrs[0]['llbbox'], [-180.0, -85.0511287798, 180.0, 85.0511287798]) eq_(lyrs[0]['srs'], set(['EPSG:4326', 'EPSG:4258', 'CRS:84', 'EPSG:900913', 'EPSG:31466', 'EPSG:31467', 'EPSG:31468', 'EPSG:25831', 'EPSG:25832', 'EPSG:25833', 'EPSG:3857', ]) ) eq_(len(lyrs[0]['bbox_srs']), 4) eq_(set(lyrs[0]['bbox_srs'].keys()), set(['CRS:84', 'EPSG:900913', 'EPSG:4326', 'EPSG:3857'])) eq_(lyrs[0]['bbox_srs']['EPSG:3857'], [-20037508.3428, -20037508.3428, 20037508.3428, 20037508.3428]) # EPSG:4326 bbox should be switched to long/lat eq_(lyrs[0]['bbox_srs']['EPSG:4326'], (-180.0, -85.0511287798, 180.0, 85.0511287798)) class TestLargeWMSCapabilities(object): def test_parse_metadata(self): cap = parse_capabilities(local_filename('wms_nasa_cap.xml')) md = cap.metadata() eq_(md['name'], 'OGC:WMS') eq_(md['title'], 'JPL Global Imagery Service') def test_parse_layer(self): cap = parse_capabilities(local_filename('wms_nasa_cap.xml')) lyrs = cap.layers_list() eq_(len(lyrs), 15) eq_(len(lyrs[0]['bbox_srs']), 0) ����������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/wmsparse/test/test_util.py����������������������������������������0000664�0000000�0000000�00000001051�13204544724�0024315�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from ..util import resolve_ns from nose.tools import eq_ def test_resolve_ns(): eq_(resolve_ns('/bar/bar', {}, None), '/bar/bar') eq_(resolve_ns('/bar/bar', {}, 'http://foo'), '/{http://foo}bar/{http://foo}bar') eq_(resolve_ns('/bar/xlink:bar', {'xlink': 'http://www.w3.org/1999/xlink'}, 'http://foo'), '/{http://foo}bar/{http://www.w3.org/1999/xlink}bar') eq_(resolve_ns('bar/xlink:bar', {'xlink': 'http://www.w3.org/1999/xlink'}, 'http://foo'), '{http://foo}bar/{http://www.w3.org/1999/xlink}bar') ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mapproxy-1.11.0/mapproxy/util/ext/wmsparse/test/wms-large-111.xml�����������������������������������0000664�0000000�0000000�00000236332�13204544724�0024663�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?xml version='1.0' encoding="UTF-8" standalone="no" ?> <!DOCTYPE WMT_MS_Capabilities SYSTEM "http://schemas.opengis.net/wms/1.1.1/WMS_MS_Capabilities.dtd" [ <!ELEMENT VendorSpecificCapabilities EMPTY> ]> <!-- end of DOCTYPE declaration --> <WMT_MS_Capabilities version="1.1.1"> <!-- MapServer version 5.2.0 OUTPUT=GIF OUTPUT=PNG OUTPUT=JPEG OUTPUT=WBMP OUTPUT=SVG SUPPORTS=PROJ SUPPORTS=AGG SUPPORTS=FREETYPE SUPPORTS=ICONV SUPPORTS=WMS_SERVER SUPPORTS=WMS_CLIENT SUPPORTS=WFS_SERVER SUPPORTS=WFS_CLIENT SUPPORTS=WCS_SERVER SUPPORTS=GEOS INPUT=TIFF INPUT=EPPL7 INPUT=POSTGIS INPUT=OGR INPUT=GDAL INPUT=SHAPEFILE --> <Service> <Name>OGC:WMS</Name> <Title>OSM Open Street Map osm OpenStreetMap Name Organization Development postal
Fakestreet 23
Somewhere 12345 Germany
0 info@example.org
none none application/vnd.ogc.wms_xml image/gif image/png image/png; mode=24bit image/jpeg image/vnd.wap.wbmp image/tiff image/svg+xml text/plain text/html application/vnd.ogc.gml text/xml image/gif image/png image/png; mode=24bit image/jpeg image/vnd.wap.wbmp text/xml application/vnd.ogc.se_xml application/vnd.ogc.se_inimage application/vnd.ogc.se_blank OSM OSM EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:4326 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Grenzen Europa EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Landwirtschaft Landwirtschaft Landwirtschaft Bauernhof OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Industriegebiet Industriegebiet Industrie Industriegebiet OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Bauland Bauland Bauland OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Gruenflaeche Gruenflaeche Grünfläche Land OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 unkultiviertes_Land unkultiviertes_Land unkultiviertes Land Unterholz Busch Land OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Park Park Park Land OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Naherholungsgebiet Naherholungsgebiet Naherholungsgebiet Land OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Wald Wald/Forst Wald/Forst Land OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Wiese Wiese Wiese Land OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Fussgaengerzone Fußgängerzone Fußgängerzone Strassen OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Gebaeude Gebäude Gebäude OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Wasser Gewaesser Gewaesser OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Fluesse Fluesse Flüße Wasser OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Baeche Baeche Bach Baeche Wasser OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Kanal Kanal Kanal Wasser OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Wasserbecken Wasser- und Speicherbecken Wasserbecken Wasser OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Insel Insel Insel OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Kueste Kueste Insel Küste OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Inselpunkte Inselpunkte Insel OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Strand Strand Strand Land OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Fussgaengerweg Fußgängerwege Fußgängerwege Strassen OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Radweg Radweg Radweg Strassen OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Wege Wege Wege Strassen OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Wohnstrasse Wohnstrasse Wohnstrasse Strassen OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Zufahrtswege Zufahrtswege Zufahrtswege Strassen OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 einfache_Strasse einfache Strasse einfache Strasse Strassen OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Landstrasse Landstrasse Landstrasse Strassen OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Bundesstrasse Bundesstrasse Bundesstrasse Strassen OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Kraftfahrstrasse Kraftfahrstrasse Kraftfahrstrasse Strassen OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Autobahn Autobahn Autobahn Strassen OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Ortschaft Ortschaft Ortschaft Orte OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Weiler Weiler Weiler Orte OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Stadtteil Stadtteil Stadtteil Orte OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Dorf Dorf Dorf Orte OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Stadt Stadt Stadt Orte OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Grossstadt Grossstadt Stadt Orte OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Bahn Bahn Bahn Zug OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Bahnhof Bahnhof POI Bahnhof OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Airport Flughafen POI Flughafen OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Kirchengelaende Kirchengelände Kirche POI OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Friedhof Friedhof Friedhof Land OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Kirche Kirche Kirche POI OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 Graeber Gräber Friedhof Gräber Land OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 copyright Copyright Copyright Lizenz OSM EPSG:4326 EPSG:31467 EPSG:31466 EPSG:31468 EPSG:31469 EPSG:31492 EPSG:31493 EPSG:31494 EPSG:31495 EPSG:31462 EPSG:31463 EPSG:31464 EPSG:31465 EPSG:25832 EPSG:25833 EPSG:31257 EPSG:31258 EPSG:31259 EPSG:31284 EPSG:31285 EPSG:31286 EPSG:31287 EPSG:28992 mapproxy-1.11.0/mapproxy/util/ext/wmsparse/test/wms-omniscale-111.xml000066400000000000000000000072101320454472400255320ustar00rootroot00000000000000 ]> OGC:WMS Omniscale OpenStreetMap WMS Omniscale OpenStreetMap WMS (powered by MapProxy) Oliver Tonnhofer Omniscale Technical Director postal
Nadorster Str. 60
Oldenburg 26123 Germany
+49(0)441-9392774-0 +49(0)441-9392774-9 osm@omniscale.de
none Here be dragons.
application/vnd.ogc.wms_xml image/jpeg image/png image/gif image/GeoTIFF image/tiff text/plain text/html application/vnd.ogc.gml application/vnd.ogc.se_xml application/vnd.ogc.se_inimage application/vnd.ogc.se_blank Omniscale OpenStreetMap WMS EPSG:4326 EPSG:4258 CRS:84 EPSG:900913 EPSG:31466 EPSG:31467 EPSG:31468 EPSG:25831 EPSG:25832 EPSG:25833 EPSG:3857 osm OpenStreetMap (complete map) osm_roads OpenStreetMap (streets only)
mapproxy-1.11.0/mapproxy/util/ext/wmsparse/test/wms-omniscale-130.xml000066400000000000000000000126361320454472400255430ustar00rootroot00000000000000 WMS Omniscale OpenStreetMap WMS Omniscale OpenStreetMap WMS (powered by MapProxy) Oliver Tonnhofer Omniscale Technical Director postal
Nadorster Str. 60
Oldenburg 26123 Germany
+49(0)441-9392774-0 +49(0)441-9392774-9 osm@omniscale.de
none This service is intended for private and evaluation use only. The data is licensed as Open Data Commons Open Database License (ODbL 1.0) (http://opendatacommons.org/licenses/odbl/1.0/)
text/xml image/gif image/png image/tiff image/jpeg image/GeoTIFF text/plain text/html text/xml XML INIMAGE BLANK Omniscale OpenStreetMap WMS EPSG:4326 EPSG:4258 CRS:84 EPSG:900913 EPSG:31466 EPSG:31467 EPSG:31468 EPSG:25831 EPSG:25832 EPSG:25833 EPSG:3857 -180 180 -85.0511287798 85.0511287798 osm OpenStreetMap (complete map) -180 180 -85.0511287798 85.0511287798 osm_roads OpenStreetMap (streets only) -180 180 -85.0511287798 85.0511287798
mapproxy-1.11.0/mapproxy/util/ext/wmsparse/test/wms_nasa_cap.xml000066400000000000000000000540551320454472400251200ustar00rootroot00000000000000 ]> OGC:WMS JPL Global Imagery Service WMS Server maintained by JPL, worldwide satellite imagery. ImageryBaseMapsEarthCover Imagery BaseMaps EarthCover JPL Jet Propulsion Laboratory Landsat WMS SLD Global Lucian Plesea JPL lucian.plesea@jpl.nasa.gov none Server is load limited text/xml application/vnd.ogc.wms_xml image/jpeg image/png image/geotiff image/tiff application/vnd.google-earth.kml+xml application/vnd.ogc.se_xml OnEarth Web Map Server EPSG:4326 AUTO:42003 EPSG:4326 AUTO:42003 global_mosaic WMS Global Mosaic, pan sharpened Release 2 of the WMS Global Mosaic, a seamless mosaic of Landsat7 scenes. Spatial resolution is 0.5 second for the pan band, 1 second for the visual and near-IR bands and 2 second for the thermal bands Use this layer to request individual grayscale bands. The default styles may have gamma, sharpening and saturation filters applied. The grayscale styles have no extra processing applied, and will return the image data as stored on the server. The source dataset is part of the NASA Scientific Data Purchase, and contains scenes acquired in 1999-2003. This layer provides pan-sharpened images, where the pan band is used for the image brightness regardless of the color combination requested. text/xml 20000 global_mosaic_base WMS Global Mosaic, not pan sharpened Release 2 of the WMS Global Mosaic, a seamless mosaic of Landsat7 scenes. Spatial resolution is 0.5 second for the pan band, 1 second for the visual and near-IR bands and 2 second for the thermal bands Use this layer to request individual grayscale bands. The default styles may have gamma, sharpening and saturation filters applied. The source dataset is part of the NASA Scientific Data Purchase, and contains scenes acquired in 1999-2003. Release 2. 20000 us_landsat_wgs84 CONUS mosaic of 1990 MRLC dataset CONUS seamless mosaic of Landsat5 scenes. Maximum resolution is 1 arc-second. The default styles may have gamma, sharpening and saturation filters applied. The source dataset is part of the MRLC 1990 dataset. This layer is not precisely geo-referenced! srtm_mag SRTM reflectance magnitude, 30m This is the radar reflectance image produced by the SRTM mission. It is the best available snapshot of the surface of the earth, being the highest resolution image collected in the shortest ammount of time, with near-global 30m coverage collected during an 11-day Endeavour mission, in February of 2000. Five basic bands are available as WMS styles, ss1, ss2, ss3 and ss4 being SRTM image subswath averages, the "all" style being an average of the four subswath composites. The "default" style is derived from the "all" band, using an arbitrary color map to make more detail visible. The subswath composites also available as WMS bands, band 0 correspoinding to ss1, 1 to "ss2", 2 to "ss3", 3 to "ss4" and 5 to "all". A radar image has little in common with a visual image, depending mostly on the material and orientation of the object. Areas with low detail such as lakes and sand tend to have no reflection, and very steep terrain can obscure certain areas from the side look ing SRTM instrument, both fenomena generating voids in the SRTM reflectance image. Urban areas tend to have stronger reflectance. The banding artifacts still visible in the images are the result of the combination of data from multiple orbits or are intrinsic to the SRTM instrument. 20000 daily_planet Current global view of the earth, morning A contiunously updating composite of visual images from TERRA MODIS scenes, see http://modis.gsfc.nasa.gov for details about MODIS. This dataset is built local on the OnEarth server, it updates as soon as scenes are available, usually with a 6 to 24 hour delay from real time. Images are produced from MODIS scenes using the HDFLook application. Base resolution is 8 arcseconds per pixel. The WMS "time" dimension can be used to retrieve past data, by using the YYYY-MM-DD notation. 2007-12-01/2010-03-20/P1D daily_afternoon Current global view of the earth in the afternoon A contiunously updating composite of visual images from AQUA MODIS scenes, see http://modis.gsfc.nasa.gov for details about MODIS. This dataset is built local on the OnEarth server, it updates as soon as scenes are available, usually with a 6 to 24 hour delay from real time. Images are produced from MODIS scenes using the HDFLook application. Base resolution is 8 arcseconds per pixel. The WMS "time" dimension can be used to retrieve past data, by using the YYYY-MM-DD notation. 2008-12-01/2010-03-20/P1D BMNG Blue Marble Next Generation, Global MODIS derived image A set of twelve images built from MODIS data, one for each month of 2004. The native resolution is 15 arcseconds, native size is 86400x43200 pixels. For each month, three versions are available from this server. The versions with land topography and bathymetry shading are named after the month they represent. The styles with names prefixed by _nb have land topography shading but No Bathymetry. The styles with names prefixed by _ns have No extra Shading. modis Blue Marble, Global MODIS derived image huemapped_srtm SRTM derived global elevation, 3 arc-second, hue mapped An SRTM derived elevation dataset, where elevation is mapped to hue, resulting a color image 12000 srtmplus Global 1km elevation, seamless SRTM land elevation and ocean depth The SRTM30 Plus dataset, a 30 arc-second seamless combination of GTOPO30, SRTM derived land elevation and UCSD Sandwell bathymetry data. The default style is scaled to 8 bit, non-linear. It is possible to request the elevation data in meters by the short_int tyle and requesting PNG format. The resulting PNG file will be a unsigned 16 bit per pixel image. The values are then the elevation in meters. Values are signed 16 bit integers, but PNG will present them as unsigned, any values larger than 32767 should be interpreted as negative numbers. For elevation values in feet, request PNG format with the style feet_short_int. 120000 worldwind_dem SRTM derived global elevation, 3 arc-second A global elevation model, prepared from the 3 arc-second SRTM dataset by filling some of the problem areas. Prepared by the NASA Learning Technologies. The default style is scaled to 8 bit, non-linear. It is possible to request the elevation data in meters by the short_int tyle and requesting PNG format. The resulting PNG file will be a unsigned 16 bit per pixel image. The values are then the elevation in meters. Values are signed 16 bit integers, but PNG will present them as unsigned, leading to a few areas with very large values (65000+) For elevation values in feet, request PNG format with the style feet_short_int. 120000 us_ned United States elevation, 30m Continental United States elevation, produced from the USGS National Elevation. The default style is scaled to 8 bit from the orginal floating point data. 24000 us_elevation Digital Elevation Map of the United States, DTED dataset, 3 second resolution, grayscale DTED Level 3 US elevation. The default style is scaled to 8 bit. It is possible to request the elevation data in meters by the short_int tyle and requesting PNG format. The resulting PNG file will be a unsigned 16 bit per pixel image. The values are elevation in meters, zero clipped (no negative values). us_colordem Digital Elevation Map of the United States, DTED dataset, 3 second resolution, hue mapped The DTED Level 3 US elevation, mapped to a color image using the full spectrum. This result is not achievable by using SLD, so it is presented as a different layer. 20000 gdem ASTER DEM, tiled only, 1.5 arc-second per pixel Subsampled version of the ASTER Global Digital Elevation Map (GDEM). Details are available at http://asterweb.jpl.nasa.gov/gdem.asp. Redistribution of the full resolution original data is not allowed, this dataset is subsampled to 1/2400 pixels per degree (1.5 arc-sec, 45m). Tiles, described by the http://onearth.jpl.nasa.gov/wms.cgi?request=GetTileService, are 16bit PNG files, where the 16 bit values should be interpreted as signed short integers, in meters. 20000 mapproxy-1.11.0/mapproxy/util/ext/wmsparse/util.py000066400000000000000000000010721320454472400223020ustar00rootroot00000000000000import re xpath_elem = re.compile('(^|/)([^/]+:)?([^/]+)') def resolve_ns(xpath, namespaces, default=None): """ Resolve namespaces in xpath to absolute URL as required by etree. """ def repl(match): ns = match.group(2) if ns: abs_ns = namespaces.get(ns[:-1], default) else: abs_ns = default if not abs_ns: return '%s%s' % (match.group(1), match.group(3)) else: return '%s{%s}%s' % (match.group(1), abs_ns, match.group(3)) return xpath_elem.sub(repl, xpath) mapproxy-1.11.0/mapproxy/util/fs.py000066400000000000000000000121301320454472400172710ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010-2013 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ File system related utility functions. """ from __future__ import absolute_import import time import os import sys import random import errno import shutil def swap_dir(src_dir, dst_dir, keep_old=False, backup_ext='.tmp'): """ Rename `src_dir` to `dst_dir`. The `dst_dir` is first renamed to `dst_dir` + `backup_ext` to keep the interruption short. Then the `src_dir` is renamed. If `keep_old` is False, the old content of `dst_dir` will be removed. """ tmp_dir = dst_dir + backup_ext if os.path.exists(dst_dir): os.rename(dst_dir, tmp_dir) _force_rename_dir(src_dir, dst_dir) if os.path.exists(tmp_dir) and not keep_old: shutil.rmtree(tmp_dir) def _force_rename_dir(src_dir, dst_dir): """ Rename `src_dir` to `dst_dir`. If `dst_dir` exists, it will be removed. """ # someone might recreate the directory between rmtree and rename, # so we try to remove it until we can rename our new directory rename_tries = 0 while rename_tries < 10: try: os.rename(src_dir, dst_dir) except OSError as ex: if ex.errno == errno.ENOTEMPTY or ex.errno == errno.EEXIST: if rename_tries > 0: time.sleep(2**rename_tries / 100.0) # from 10ms to 5s rename_tries += 1 shutil.rmtree(dst_dir) else: raise else: break # on success def cleanup_directory(directory, before_timestamp, remove_empty_dirs=True, file_handler=None): if file_handler is None: if before_timestamp == 0 and remove_empty_dirs == True and os.path.exists(directory): shutil.rmtree(directory, ignore_errors=True) return file_handler = os.remove if os.path.exists(directory): for dirpath, dirnames, filenames in os.walk(directory, topdown=False): if not filenames: if (remove_empty_dirs and not os.listdir(dirpath) and dirpath != directory): os.rmdir(dirpath) continue for filename in filenames: filename = os.path.join(dirpath, filename) try: if before_timestamp == 0: file_handler(filename) if os.lstat(filename).st_mtime < before_timestamp: file_handler(filename) except OSError as ex: if ex.errno != errno.ENOENT: raise if remove_empty_dirs: remove_dir_if_emtpy(dirpath) if remove_empty_dirs: remove_dir_if_emtpy(directory) def remove_dir_if_emtpy(directory): try: os.rmdir(directory) except OSError as ex: if ex.errno != errno.ENOENT and ex.errno != errno.ENOTEMPTY: raise def ensure_directory(file_name): """ Create directory if it does not exist, else do nothing. """ dir_name = os.path.dirname(file_name) if not os.path.exists(dir_name): try: os.makedirs(dir_name) except OSError as e: if e.errno != errno.EEXIST: raise e def write_atomic(filename, data): """ write_atomic writes `data` to a random file in filename's directory first and renames that file to the target filename afterwards. Rename is atomic on all POSIX platforms. Falls back to normal write on Windows. """ if not sys.platform.startswith('win'): # write to random filename to prevent concurrent writes in cases # where file locking does not work (network fs) path_tmp = filename + '.tmp-' + str(random.randint(0, 99999999)) try: fd = os.open(path_tmp, os.O_EXCL | os.O_CREAT | os.O_WRONLY, 0o666) with os.fdopen(fd, 'wb') as f: f.write(data) os.rename(path_tmp, filename) except OSError as ex: try: os.unlink(path_tmp) except OSError: pass raise ex else: with open(filename, 'wb') as f: f.write(data) def find_exec(executable): """ Search executable in PATH environment. Return path if found, None if not. """ path = os.environ.get('PATH') if not path: return for p in path.split(os.path.pathsep): p = os.path.join(p, executable) if os.path.exists(p): return p p += '.exe' if os.path.exists(p): return p mapproxy-1.11.0/mapproxy/util/geom.py000066400000000000000000000214371320454472400176220ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import os import json import codecs from functools import partial from contextlib import closing from mapproxy.grid import tile_grid from mapproxy.compat import string_type, text_type import logging log_config = logging.getLogger('mapproxy.config.coverage') try: import shapely.wkt import shapely.geometry import shapely.ops import shapely.prepared try: # shapely >=1.6 from shapely.errors import ReadingError except ImportError: from shapely.geos import ReadingError geom_support = True except ImportError: geom_support = False class GeometryError(Exception): pass class EmptyGeometryError(Exception): pass class CoverageReadError(Exception): pass def require_geom_support(): if not geom_support: raise ImportError('Shapely required for geometry support') def load_datasource(datasource, where=None): """ Loads polygons from WKT text files or OGR datasources. Returns a list of Shapely Polygons. """ # check if it is a wkt or geojson file if os.path.exists(os.path.abspath(datasource)): with open(os.path.abspath(datasource), 'rb') as fp: data = fp.read(50) if data.lower().lstrip().startswith((b'polygon', b'multipolygon')): return load_polygons(datasource) # only load geojson directly if we don't have a filter if where is None and data and data.startswith(b'{'): return load_geojson(datasource) # otherwise pass to OGR return load_ogr_datasource(datasource, where=where) def load_ogr_datasource(datasource, where=None): """ Loads polygons from any OGR datasource. Returns a list of Shapely Polygons. """ from mapproxy.util.ogr import OGRShapeReader, OGRShapeReaderError polygons = [] try: with closing(OGRShapeReader(datasource)) as reader: for wkt in reader.wkts(where): if not isinstance(wkt, text_type): wkt = wkt.decode() try: geom = shapely.wkt.loads(wkt) except ReadingError as ex: raise GeometryError(ex) if geom.type == 'Polygon': polygons.append(geom) elif geom.type == 'MultiPolygon': for p in geom: polygons.append(p) else: log_config.warn('skipping %s geometry from %s: not a Polygon/MultiPolygon', geom.type, datasource) except OGRShapeReaderError as ex: raise CoverageReadError(ex) return polygons def load_polygons(geom_files): """ Loads WKT polygons from one or more text files. Returns a list of Shapely Polygons. """ polygons = [] if isinstance(geom_files, string_type): geom_files = [geom_files] for geom_file in geom_files: # open with utf-8-sig encoding to get rid of UTF8 BOM from MS Notepad with codecs.open(geom_file, encoding='utf-8-sig') as f: polygons.extend(load_polygon_lines(f, source=geom_files)) return polygons def load_geojson(datasource): with open(datasource) as f: geojson = json.load(f) t = geojson.get('type') if not t: raise CoverageReadError("not a GeoJSON") geometries = [] if t == 'FeatureCollection': for f in geojson.get('features'): geom = f.get('geometry') if geom: geometries.append(geom) elif t == 'Feature': if 'geometry' in geojson: geometries.append(geojson['geometry']) elif t in ('Polygon', 'MultiPolygon'): geometries.append(geojson) else: log_config.warn('skipping feature of type %s from %s: not a Polygon/MultiPolygon', t, datasource) polygons = [] for geom in geometries: geom = shapely.geometry.asShape(geom) if geom.type == 'Polygon': polygons.append(geom) elif geom.type == 'MultiPolygon': for p in geom: polygons.append(p) else: log_config.warn('ignoring non-polygon geometry (%s) from %s', geom.type, datasource) return polygons def load_polygon_lines(line_iter, source=''): polygons = [] for line in line_iter: if not line.strip(): continue geom = shapely.wkt.loads(line) if geom.type == 'Polygon': polygons.append(geom) elif geom.type == 'MultiPolygon': for p in geom: polygons.append(p) else: log_config.warn('ignoring non-polygon geometry (%s) from %s', geom.type, source) return polygons def build_multipolygon(polygons, simplify=False): if not polygons: raise EmptyGeometryError('no polygons') if len(polygons) == 1: geom = polygons[0] if simplify: geom = simplify_geom(geom) return geom.bounds, geom if simplify: polygons = [simplify_geom(g) for g in polygons] # eliminate any self-overlaps mp = shapely.ops.cascaded_union(polygons) return mp.bounds, mp def simplify_geom(geom): bounds = geom.bounds if not bounds: raise EmptyGeometryError('Empty geometry given') w, h = bounds[2] - bounds[0], bounds[3] - bounds[1] tolerance = min((w/1e6, h/1e6)) geom = geom.simplify(tolerance, preserve_topology=True) if not geom.is_valid: geom = geom.buffer(0) return geom def bbox_polygon(bbox): """ Create Polygon that covers the given bbox. """ return shapely.geometry.Polygon(( (bbox[0], bbox[1]), (bbox[2], bbox[1]), (bbox[2], bbox[3]), (bbox[0], bbox[3]), )) def transform_geometry(from_srs, to_srs, geometry): transf = partial(transform_xy, from_srs, to_srs) if geometry.type == 'Polygon': result = transform_polygon(transf, geometry) elif geometry.type == 'MultiPolygon': result = transform_multipolygon(transf, geometry) else: raise ValueError('cannot transform %s' % geometry.type) if not result.is_valid: result = result.buffer(0) return result def transform_polygon(transf, polygon): ext = transf(polygon.exterior.xy) ints = [transf(ring.xy) for ring in polygon.interiors] return shapely.geometry.Polygon(ext, ints) def transform_multipolygon(transf, multipolygon): transformed_polygons = [] for polygon in multipolygon: transformed_polygons.append(transform_polygon(transf, polygon)) return shapely.geometry.MultiPolygon(transformed_polygons) def transform_xy(from_srs, to_srs, xy): return list(from_srs.transform_to(to_srs, list(zip(*xy)))) def flatten_to_polygons(geometry): """ Return a list of all polygons of this (multi)`geometry`. """ if geometry.type == 'Polygon': return [geometry] if geometry.type == 'MultiPolygon': return list(geometry) if hasattr(geometry, 'geoms'): # GeometryCollection or MultiLineString? return list of all polygons geoms = [] for part in geometry.geoms: if part.type == 'Polygon': geoms.append(part) if geoms: return geoms return [] def load_expire_tiles(expire_dir, grid=None): if grid is None: grid = tile_grid(3857, origin='nw') tiles = set() def parse(filename): with open(filename) as f: try: for line in f: if not line: continue tile = tuple(map(int, line.split('/'))) tiles.add(tile) except: log_config.warn('found error in %s, skipping rest of file', filename) if os.path.isdir(expire_dir): for root, dirs, files in os.walk(expire_dir): for name in files: filename = os.path.join(root, name) parse(filename) else: parse(expire_dir) boxes = [] for tile in tiles: z, x, y = tile boxes.append(shapely.geometry.box(*grid.tile_bbox((x, y, z)))) return boxes mapproxy-1.11.0/mapproxy/util/lib.py000066400000000000000000000063121320454472400174340ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ ctypes utilities. """ from __future__ import print_function import sys import os from ctypes import CDLL from ctypes.util import find_library as _find_library from mapproxy.compat import string_type default_locations = dict( darwin=dict( paths = ['/opt/local/lib'], exts = ['.dylib'], ), win32=dict( paths = [os.path.dirname(os.__file__) + '/../../../DLLs'], exts = ['.dll'] ), other=dict( paths = [], # MAPPROXY_LIB_PATH will add paths here exts = ['.so'] ), ) additional_lib_path = os.environ.get('MAPPROXY_LIB_PATH') if additional_lib_path: additional_lib_path = additional_lib_path.split(os.pathsep) additional_lib_path.reverse() for locs in default_locations.values(): for path in additional_lib_path: locs['paths'].insert(0, path) def load_library(lib_names, locations_conf=default_locations): """ Load the `lib_name` library with ctypes. If ctypes.util.find_library does not find the library, different path and filename extensions will be tried. Retruns the loaded library or None. """ if isinstance(lib_names, string_type): lib_names = [lib_names] for lib_name in lib_names: lib = load_library_(lib_name, locations_conf) if lib is not None: return lib def load_library_(lib_name, locations_conf=default_locations): lib_path = find_library(lib_name) if lib_path: return CDLL(lib_path) if sys.platform in locations_conf: paths = locations_conf[sys.platform]['paths'] exts = locations_conf[sys.platform]['exts'] lib_path = find_library(lib_name, paths, exts) else: paths = locations_conf['other']['paths'] exts = locations_conf['other']['exts'] lib_path = find_library(lib_name, paths, exts) if lib_path: return CDLL(lib_path) def find_library(lib_name, paths=None, exts=None): """ Search for library in all permutations of `paths` and `exts`. If nothing is found None is returned. """ if not paths or not exts: lib = _find_library(lib_name) if lib is None and lib_name.startswith('lib'): lib = _find_library(lib_name[3:]) return lib for lib_name in [lib_name] + ([lib_name[3:]] if lib_name.startswith('lib') else []): for path in paths: for ext in exts: lib_path = os.path.join(path, lib_name + ext) if os.path.exists(lib_path): return lib_path return None if __name__ == '__main__': print(load_library(sys.argv[1])) mapproxy-1.11.0/mapproxy/util/lock.py000066400000000000000000000120371320454472400176170ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010-2014 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Utility methods and classes (file locking, asynchronous execution pools, etc.). """ import random import time import os import errno from mapproxy.util.ext.lockfile import LockFile, LockError import logging log = logging.getLogger(__name__) __all__ = ['LockTimeout', 'FileLock', 'LockError', 'cleanup_lockdir', 'SemLock'] class LockTimeout(Exception): pass class FileLock(object): def __init__(self, lock_file, timeout=60.0, step=0.01, remove_on_unlock=False): self.lock_file = lock_file self.timeout = timeout self.step = step self.remove_on_unlock = remove_on_unlock self._locked = False def __enter__(self): self.lock() def __exit__(self, _exc_type, _exc_value, _traceback): self.unlock() def _make_lockdir(self): if not os.path.exists(os.path.dirname(self.lock_file)): try: os.makedirs(os.path.dirname(self.lock_file)) except OSError as e: if e.errno is not errno.EEXIST: raise e def _try_lock(self): return LockFile(self.lock_file) def lock(self): self._make_lockdir() current_time = time.time() stop_time = current_time + self.timeout while not self._locked: try: self._lock = self._try_lock() except LockError: current_time = time.time() if current_time < stop_time: time.sleep(self.step) continue else: raise LockTimeout('another process is still running with our lock') else: self._locked = True def unlock(self): if self._locked: self._locked = False if self.remove_on_unlock: try: # try to release lock by removing # this is not a clean way and more than one process might # grab the lock afterwards but it is ok when the task is # solved by the first process that got the lock (i.e. the # tile is created) os.remove(self.lock_file) except OSError: self._lock.close() else: self._lock.close() def __del__(self): self.unlock() _cleanup_counter = -1 def cleanup_lockdir(lockdir, suffix='.lck', max_lock_time=300, force=True): """ Remove files ending with `suffix` from `lockdir` if they are older then `max_lock_time` seconds. It will not cleanup on every call if `force` is ``False``. """ global _cleanup_counter _cleanup_counter += 1 if not force and _cleanup_counter % 50 != 0: return expire_time = time.time() - max_lock_time if not os.path.exists(lockdir): return if not os.path.isdir(lockdir): log.warn('lock dir not a directory: %s', lockdir) return for entry in os.listdir(lockdir): name = os.path.join(lockdir, entry) try: if os.path.isfile(name) and name.endswith(suffix): if os.path.getmtime(name) < expire_time: try: os.unlink(name) except IOError as ex: log.warn('could not remove old lock file %s: %s', name, ex) except OSError as e: # some one might have removed the file (ENOENT) # or we don't have permissions to remove it (EACCES) if e.errno in (errno.ENOENT, errno.EACCES): # ignore pass else: raise e class SemLock(FileLock): """ File-lock-based counting semaphore (i.e. this lock can be locked n-times). """ def __init__(self, lock_file, n, timeout=60.0, step=0.01): FileLock.__init__(self, lock_file, timeout=timeout, step=step) self.n = n def _try_lock(self): tries = 0 i = random.randint(0, self.n-1) while True: tries += 1 try: return LockFile(self.lock_file + str(i)) except LockError: if tries >= self.n: raise i = (i+1) % self.n class DummyLock(object): def __enter__(self): pass def __exit__(self, _exc_type, _exc_value, _traceback): pass def lock(self): pass def unlock(self): pass mapproxy-1.11.0/mapproxy/util/ogr.py000066400000000000000000000161161320454472400174600ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function import os import sys import ctypes from ctypes import c_void_p, c_char_p, c_int from mapproxy.util.lib import load_library def init_libgdal(): libgdal = load_library(['libgdal', 'libgdal1', 'gdal110', 'gdal19', 'gdal18', 'gdal17']) if not libgdal: return libgdal.OGROpen.argtypes = [c_char_p, c_int, c_void_p] libgdal.OGROpen.restype = c_void_p # CPLGetLastErrorMsg is not part of the official and gets # name mangled on Windows builds. try to support _Foo@0 # mangling, otherwise print no detailed errors if not hasattr(libgdal, 'CPLGetLastErrorMsg') and hasattr(libgdal, '_CPLGetLastErrorMsg@0'): libgdal.CPLGetLastErrorMsg = getattr(libgdal, '_CPLGetLastErrorMsg@0') if hasattr(libgdal, 'CPLGetLastErrorMsg'): libgdal.CPLGetLastErrorMsg.argtypes = [] libgdal.CPLGetLastErrorMsg.restype = c_char_p else: libgdal.CPLGetLastErrorMsg = None libgdal.OGR_DS_GetLayer.argtypes = [c_void_p, c_int] libgdal.OGR_DS_GetLayer.restype = c_void_p libgdal.OGR_FD_GetName.argtypes = [c_void_p] libgdal.OGR_FD_GetName.restype = c_char_p libgdal.OGR_L_GetLayerDefn.argtypes = [c_void_p] libgdal.OGR_L_GetLayerDefn.restype = c_void_p libgdal.OGR_DS_Destroy.argtypes = [c_void_p] libgdal.OGR_DS_ExecuteSQL.argtypes = [c_void_p, c_char_p, c_void_p, c_char_p] libgdal.OGR_DS_ExecuteSQL.restype = c_void_p libgdal.OGR_DS_ReleaseResultSet.argtypes = [c_void_p, c_void_p] libgdal.OGR_L_ResetReading.argtypes = [c_void_p] libgdal.OGR_L_GetNextFeature.argtypes = [c_void_p] libgdal.OGR_L_GetNextFeature.restype = c_void_p libgdal.OGR_F_Destroy.argtypes = [c_void_p] libgdal.OGR_F_GetGeometryRef.argtypes = [c_void_p] libgdal.OGR_F_GetGeometryRef.restype = c_void_p libgdal.OGR_G_ExportToWkt.argtypes = [c_void_p, ctypes.POINTER(c_char_p)] libgdal.OGR_G_ExportToWkt.restype = c_void_p libgdal.VSIFree.argtypes = [c_void_p] libgdal.OGRRegisterAll() return libgdal class OGRShapeReaderError(Exception): pass class CtypesOGRShapeReader(object): def __init__(self, datasource): self.datasource = datasource self._ds = None def open(self): if self._ds: return self._ds = libgdal.OGROpen(self.datasource.encode(sys.getdefaultencoding()), False, None) if self._ds is None: msg = None if libgdal.CPLGetLastErrorMsg: msg = libgdal.CPLGetLastErrorMsg() if not msg: msg = 'failed to open %s' % self.datasource raise OGRShapeReaderError(msg) def wkts(self, where=None): if not self._ds: self.open() if where: if not where.lower().startswith('select'): layer = libgdal.OGR_DS_GetLayer(self._ds, 0) layer_def = libgdal.OGR_L_GetLayerDefn(layer) name = libgdal.OGR_FD_GetName(layer_def) where = 'select * from %s where %s' % (name.decode('utf-8'), where) layer = libgdal.OGR_DS_ExecuteSQL(self._ds, where.encode('utf-8'), None, None) else: layer = libgdal.OGR_DS_GetLayer(self._ds, 0) if layer is None: msg = None if libgdal.CPLGetLastErrorMsg: msg = libgdal.CPLGetLastErrorMsg() raise OGRShapeReaderError(msg) libgdal.OGR_L_ResetReading(layer) while True: feature = libgdal.OGR_L_GetNextFeature(layer) if feature is None: break geom = libgdal.OGR_F_GetGeometryRef(feature) if geom is None: libgdal.OGR_F_Destroy(feature) continue res = c_char_p() libgdal.OGR_G_ExportToWkt(geom, ctypes.byref(res)) yield res.value libgdal.VSIFree(res) libgdal.OGR_F_Destroy(feature) if where: libgdal.OGR_DS_ReleaseResultSet(self._ds, layer) def close(self): if self._ds: libgdal.OGR_DS_Destroy(self._ds) self._ds = None def __del__(self): self.close() class OSGeoOGRShapeReader(object): def __init__(self, datasource): self.datasource = datasource self._ds = None def open(self): if self._ds: return self._ds = ogr.Open(self.datasource, False) if self._ds is None: msg = gdal.GetLastErrorMsg() if not msg: msg = 'failed to open %s' % self.datasource raise OGRShapeReaderError(msg) def wkts(self, where=None): if not self._ds: self.open() if where: if not where.lower().startswith('select'): layer = self._ds.GetLayerByIndex(0) name = layer.GetName() where = 'select * from %s where %s' % (name, where) layer = self._ds.ExecuteSQL(where) else: layer = self._ds.GetLayerByIndex(0) if layer is None: msg = gdal.GetLastErrorMsg() raise OGRShapeReaderError(msg) layer.ResetReading() while True: feature = layer.GetNextFeature() if feature is None: break geom = feature.geometry() yield geom.ExportToWkt() def close(self): if self._ds: self._ds = None ogr = gdal = None def try_osgeoogr_import(): global ogr, gdal try: from osgeo import ogr; ogr from osgeo import gdal; gdal except ImportError: return return OSGeoOGRShapeReader libgdal = None def try_libogr_import(): global libgdal libgdal = init_libgdal() if libgdal is not None: return CtypesOGRShapeReader ogr_imports = [] if 'MAPPROXY_USE_OSGEOOGR' in os.environ: ogr_imports = [try_osgeoogr_import] if 'MAPPROXY_USE_LIBOGR' in os.environ: ogr_imports = [try_libogr_import] if not ogr_imports: if sys.platform == 'win32': # prefer osgeo.ogr on windows ogr_imports = [try_osgeoogr_import, try_libogr_import] else: ogr_imports = [try_libogr_import, try_osgeoogr_import] for try_import in ogr_imports: res = try_import() if res: OGRShapeReader = res break else: raise ImportError('could not find osgeo.ogr package or libgdal') if __name__ == '__main__': import sys reader = OGRShapeReader(sys.argv[1]) where = None if len(sys.argv) == 3: where = sys.argv[2] for wkt in reader.wkts(where): print(wkt) mapproxy-1.11.0/mapproxy/util/py.py000066400000000000000000000045411320454472400173200ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Python related helper functions. """ from functools import wraps from mapproxy.compat import PY2 def reraise_exception(new_exc, exc_info): """ Reraise exception (`new_exc`) with the given `exc_info`. """ _exc_class, _exc, tb = exc_info if PY2: exec('raise new_exc.__class__, new_exc, tb') else: raise new_exc.with_traceback(tb) def reraise(exc_info): """ Reraise exception from exc_info`. """ exc_class, exc, tb = exc_info if PY2: exec('raise exc_class, exc, tb') else: raise exc.with_traceback(tb) class cached_property(object): """A decorator that converts a function into a lazy property. The function wrapped is called the first time to retrieve the result and than that calculated result is used the next time you access the value:: class Foo(object): @cached_property def foo(self): # calculate something important here return 42 """ def __init__(self, func, name=None, doc=None): self.func = func self.__name__ = name or func.__name__ self.__doc__ = doc or func.__doc__ def __get__(self, obj, type=None): if obj is None: return self value = self.func(obj) setattr(obj, self.__name__, value) return value def memoize(func): @wraps(func) def wrapper(self, *args, **kwargs): if not hasattr(self, '__memoize_cache'): self.__memoize_cache = {} cache = self.__memoize_cache.setdefault(func, {}) key = args + tuple(kwargs.items()) if key not in cache: cache[key] = func(self, *args, **kwargs) return cache[key] return wrapper mapproxy-1.11.0/mapproxy/util/times.py000066400000000000000000000046031320454472400200100ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010-213 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import """ Date and time utilities. """ from time import mktime import datetime import calendar from email.utils import parsedate from wsgiref.handlers import format_date_time from mapproxy import compat def parse_httpdate(date): date = parsedate(date) if date is None: return None if date[0] < 1970: date = (date[0] + 2000,) +date[1:] return calendar.timegm(date) def timestamp(date): if isinstance(date, datetime.datetime): date = mktime(date.timetuple()) assert isinstance(date, compat.numeric_types) return date def format_httpdate(date): date = timestamp(date) return format_date_time(date) def timestamp_before(weeks=0, days=0, hours=0, minutes=0, seconds=0): """ >>> import time as time_ >>> time_.time() - timestamp_before(minutes=1) - 60 <= 1 True >>> time_.time() - timestamp_before(days=1, minutes=2) - 86520 <= 1 True >>> time_.time() - timestamp_before(hours=2) - 7200 <= 1 True """ delta = datetime.timedelta(weeks=weeks, days=days, hours=hours, minutes=minutes, seconds=seconds) before = datetime.datetime.now() - delta return mktime(before.timetuple()) def timestamp_from_isodate(isodate): """ >>> ts = timestamp_from_isodate('2009-06-09T10:57:00') >>> # we don't know which timezone the test will run >>> (1244537820.0 - 14 * 3600) < ts < (1244537820.0 + 14 * 3600) True >>> timestamp_from_isodate('2009-06-09T10:57') #doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: ... """ if isinstance(isodate, datetime.datetime): date = isodate else: date = datetime.datetime.strptime(isodate, "%Y-%m-%dT%H:%M:%S") return mktime(date.timetuple())mapproxy-1.11.0/mapproxy/util/wsgi.py000066400000000000000000000026131320454472400176370ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ WSGI utils """ def lighttpd_root_fix_filter_factory(global_conf): return LighttpdCGIRootFix class LighttpdCGIRootFix(object): """Wrap the application in this middleware if you are using lighttpd with FastCGI or CGI and the application is mounted on the URL root. :param app: the WSGI application """ def __init__(self, app): self.app = app def __call__(self, environ, start_response): script_name = environ.get('SCRIPT_NAME', '') path_info = environ.get('PATH_INFO', '') if path_info == script_name: environ['PATH_INFO'] = path_info else: environ['PATH_INFO'] = script_name + path_info environ['SCRIPT_NAME'] = '' return self.app(environ, start_response) mapproxy-1.11.0/mapproxy/util/yaml.py000066400000000000000000000030711320454472400176270ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2011 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import from mapproxy.compat import string_type import yaml class YAMLError(Exception): pass def load_yaml_file(file_or_filename): """ Load yaml from file object or filename. """ if isinstance(file_or_filename, string_type): with open(file_or_filename, 'rb') as f: return load_yaml(f) return load_yaml(file_or_filename) def load_yaml(doc): """ Load yaml from file object or string. """ try: if getattr(yaml, '__with_libyaml__', False): try: return yaml.load(doc, Loader=yaml.CLoader) except AttributeError: # handle cases where __with_libyaml__ is True but # CLoader doesn't work (missing .dispose()) return yaml.load(doc) return yaml.load(doc) except (yaml.scanner.ScannerError, yaml.parser.ParserError) as ex: raise YAMLError(str(ex)) mapproxy-1.11.0/mapproxy/version.py000066400000000000000000000017701320454472400174010ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function import pkg_resources def version_string(): """ Return the current version number of MapProxy. """ try: return pkg_resources.working_set.by_key['mapproxy'].version except KeyError: return 'unknown_version' __version__ = version = version_string() if __name__ == '__main__': print(__version__)mapproxy-1.11.0/mapproxy/wsgiapp.py000066400000000000000000000166711320454472400173740ustar00rootroot00000000000000# This file is part of the MapProxy project. # Copyright (C) 2010 Omniscale # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ The WSGI application. """ from __future__ import print_function import re import os import sys import time import threading import warnings try: # time.strptime is thread-safe, but not the first call. # Import _strptime as a workaround. See: http://bugs.python.org/issue7980 import _strptime except ImportError: pass from mapproxy.compat import iteritems from mapproxy.request import Request from mapproxy.response import Response from mapproxy.config import local_base_config from mapproxy.config.loader import load_configuration, ConfigurationError import logging log = logging.getLogger('mapproxy.config') log_wsgiapp = logging.getLogger('mapproxy.wsgiapp') def app_factory(global_options, mapproxy_conf, **local_options): """ Paster app_factory. """ conf = global_options.copy() conf.update(local_options) log_conf = conf.get('log_conf', None) reload_files = conf.get('reload_files', None) if reload_files is not None: init_paster_reload_files(reload_files) init_logging_system(log_conf, os.path.dirname(mapproxy_conf)) return make_wsgi_app(mapproxy_conf) def init_paster_reload_files(reload_files): file_patterns = reload_files.split('\n') file_patterns.append(os.path.join(os.path.dirname(__file__), 'defaults.yaml')) init_paster_file_watcher(file_patterns) def init_paster_file_watcher(file_patterns): from glob import glob for pattern in file_patterns: files = glob(pattern) _add_files_to_paster_file_watcher(files) def _add_files_to_paster_file_watcher(files): import paste.reloader for file in files: paste.reloader.watch_file(file) def init_logging_system(log_conf, base_dir): import logging.config try: import cloghandler # adds CRFHandler to log handlers cloghandler.ConcurrentRotatingFileHandler #disable pyflakes warning except ImportError: pass if log_conf: if not os.path.exists(log_conf): print('ERROR: log configuration %s not found.' % log_conf, file=sys.stderr) return logging.config.fileConfig(log_conf, dict(here=base_dir)) def init_null_logging(): import logging class NullHandler(logging.Handler): def emit(self, record): pass logging.getLogger().addHandler(NullHandler()) def make_wsgi_app(services_conf=None, debug=False, ignore_config_warnings=True, reloader=False): """ Create a MapProxyApp with the given services conf. :param services_conf: the file name of the mapproxy.yaml configuration :param reloader: reload mapproxy.yaml when it changed """ if reloader: make_app = lambda: make_wsgi_app(services_conf=services_conf, debug=debug, reloader=False) return ReloaderApp(services_conf, make_app) try: conf = load_configuration(mapproxy_conf=services_conf, ignore_warnings=ignore_config_warnings) services = conf.configured_services() except ConfigurationError as e: log.fatal(e) raise config_files = conf.config_files() app = MapProxyApp(services, conf.base_config) if debug: app = wrap_wsgi_debug(app, conf) app.config_files = config_files return app class ReloaderApp(object): def __init__(self, timestamp_file, make_app_func): self.timestamp_file = timestamp_file self.make_app_func = make_app_func self.app = make_app_func() self._app_init_lock = threading.Lock() def _needs_reload(self): for conf_file, timestamp in iteritems(self.app.config_files): m_time = os.path.getmtime(conf_file) if m_time > timestamp: return True return False def __call__(self, environ, start_response): if self._needs_reload(): with self._app_init_lock: if self._needs_reload(): try: self.app = self.make_app_func() except ConfigurationError: pass self.last_reload = time.time() return self.app(environ, start_response) def wrap_wsgi_debug(app, conf): conf.base_config.debug_mode = True try: from werkzeug.debug import DebuggedApplication app = DebuggedApplication(app, evalex=True) except ImportError: try: from paste.evalexception.middleware import EvalException app = EvalException(app) except ImportError: print('Error: Install Werkzeug or Paste for browser-based debugging.') return app class MapProxyApp(object): """ The MapProxy WSGI application. """ handler_path_re = re.compile('^/(\w+)') def __init__(self, services, base_config): self.handlers = {} self.base_config = base_config self.cors_origin = base_config.http.access_control_allow_origin for service in services: for name in service.names: self.handlers[name] = service def __call__(self, environ, start_response): resp = None req = Request(environ) if self.cors_origin: orig_start_response = start_response def start_response(status, headers, exc_info=None): headers.append(('Access-control-allow-origin', self.cors_origin)) return orig_start_response(status, headers, exc_info) with local_base_config(self.base_config): match = self.handler_path_re.match(req.path) if match: handler_name = match.group(1) if handler_name in self.handlers: try: resp = self.handlers[handler_name].handle(req) except Exception: if self.base_config.debug_mode: raise else: log_wsgiapp.fatal('fatal error in %s for %s?%s', handler_name, environ.get('PATH_INFO'), environ.get('QUERY_STRING'), exc_info=True) import traceback traceback.print_exc(file=environ['wsgi.errors']) resp = Response('internal error', status=500) if resp is None: if req.path in ('', '/'): resp = self.welcome_response(req.script_url) else: resp = Response('not found', mimetype='text/plain', status=404) return resp(environ, start_response) def welcome_response(self, script_url): import mapproxy.version html = "

Welcome to MapProxy %s

" % mapproxy.version.version if 'demo' in self.handlers: html += '

See all configured layers and services at: demo' % (script_url, ) return Response(html, mimetype='text/html') mapproxy-1.11.0/pylint.ini000066400000000000000000000156541320454472400155110ustar00rootroot00000000000000[MASTER] # Specify a configuration file. #rcfile= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= # Profiled execution. profile=no # Add to the black list. It should be a base name, not a # path. You may set this option multiple times. ignore=test # Pickle collected data for later comparisons. persistent=yes # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= [MESSAGES CONTROL] # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time. #enable= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appears only once). disable=W0231 [REPORTS] # Set the output format. Available formats are text, parseable, colorized, msvs # (visual studio) and html output-format=text # Include message's id in output include-ids=no # Put messages in a separate file for each module / package specified on the # command line instead of printing them on stdout. Reports (if any) will be # written in a file name "pylint_global.[txt|html]". files-output=no # Tells whether to display a full report or only the messages reports=yes # Python expression which should return a note less than 10 (10 is the highest # note). You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (R0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Add a comment according to your evaluation note. This is used by the global # evaluation report (R0004). comment=no [BASIC] # Required attributes for module, separated by a comma required-attributes= # List of builtins function names that should not be used, separated by a comma bad-functions=map,filter,apply,input # Regular expression which should only match correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Regular expression which should only match correct module level names const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__)|log)$ # Regular expression which should only match correct class names class-rgx=[A-Z_][a-zA-Z0-9]+$ # Regular expression which should only match correct function names function-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct method names method-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct instance attribute names attr-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct argument names argument-rgx=([a-z_][a-z0-9_]{2,30}|[xyz][01s]?|ll|ur)$ # Regular expression which should only match correct variable names variable-rgx=([a-z_][a-z0-9_]{2,30}|[xyz][01s]?|ll|ur)$ # Regular expression which should only match correct list comprehension / # generator expression variable names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ # Good variable names which should always be accepted, separated by a comma good-names=i,j,k,ex,Run,_ # Bad variable names which should always be refused, separated by a comma bad-names=foo,bar,baz,toto,tutu,tata # Regular expression which should only match functions or classes name which do # not require a docstring no-docstring-rgx=_.* [FORMAT] # Maximum number of characters on a single line. max-line-length=90 # Maximum number of lines in a module max-module-lines=1000 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME,XXX,TODO [SIMILARITIES] # Minimum lines number of a similarity. min-similarity-lines=4 # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes [TYPECHECK] # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # List of classes names for which member attributes should not be checked # (useful for classes with attributes dynamically set). ignored-classes=SQLObject,hashlib # When zope mode is activated, add a predefined set of Zope acquired attributes # to generated-members. zope=no # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E0201 when accessed. generated-members=REQUEST,acl_users,aq_parent [VARIABLES] # Tells whether we should check for unused import in __init__ files. init-import=no # A regular expression matching names used for dummy variables (i.e. not used). dummy-variables-rgx=_|dummy # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. additional-builtins= [CLASSES] # List of interface methods to ignore, separated by a comma. This is used for # instance to not check methods defines in Zope's Interface base class. ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__,__new__,setUp [DESIGN] # Maximum number of arguments for function / method max-args=8 # Argument names that match this expression will be ignored. Default to name # with leading underscore ignored-argument-names=_.* # Maximum number of locals for function / method body max-locals=15 # Maximum number of return / yield for function / method body max-returns=6 # Maximum number of branch for function / method body max-branchs=12 # Maximum number of statements in function / method body max-statements=50 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of attributes for a class (see R0902). max-attributes=10 # Minimum number of public methods for a class (see R0903). min-public-methods=1 # Maximum number of public methods for a class (see R0904). max-public-methods=20 [IMPORTS] # Deprecated modules which should not be used, separated by a comma deprecated-modules=regsub,string,TERMIOS,Bastion,rexec # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled) import-graph= # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled) ext-import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled) int-import-graph= mapproxy-1.11.0/release.py000066400000000000000000000100541320454472400154500ustar00rootroot00000000000000try: from nose.plugins.skip import SkipTest import sys if 'nosetest' in ''.join(sys.argv): raise SkipTest() except ImportError: pass import scriptine from scriptine import path from scriptine.shell import backtick_, sh PACKAGE_NAME = 'MapProxy' REMOTE_DOC_LOCATION = 'mapproxy.org:/opt/www/mapproxy.org/docs' REMOTE_REL_LOCATION = 'mapproxy.org:/opt/www/mapproxy.org/static/rel' VERSION_FILES = [ ('setup.py', 'version="###"'), ('doc/conf.py', "version = '##'"), ('doc/conf.py', "release = '###'"), ] def version_command(): print version() def prepare_command(tag=""): sh('python setup.py egg_info -D -b "%s"' % tag) def version(): package_name = PACKAGE_NAME version = backtick_('grep Version: %(package_name)s.egg-info/PKG-INFO' % locals()) version = version.split(':')[-1].strip() return version def clean_all_command(): path('build/').rmtree(ignore_errors=True) for pyc in path.cwd().walkfiles('*.pyc'): pyc.remove() def bump_version_command(version): short_version = '.'.join(version.split('.')[:2]) for filename, replace in VERSION_FILES: if '###' in replace: search_for = replace.replace('###', '[^\'"]+') replace_with = replace.replace('###', version) else: search_for = replace.replace('##', '[^\'"]+') replace_with = replace.replace('##', short_version) search_for = search_for.replace('"', '\\"') replace_with = replace_with.replace('"', '\\"') sh('''perl -p -i -e "s/%(search_for)s/%(replace_with)s/" %(filename)s ''' % locals()) prepare_command() def build_docs_command(): sh('python setup.py build_sphinx') ver = version() package_name = PACKAGE_NAME sh("tar -c -v -z -C build/sphinx/ -f dist/%(package_name)s-docs-%(ver)s.tar.gz -s " "'/^html/%(package_name)s-docs-%(ver)s/' html" % locals()) def upload_docs_command(): ver = version() remote_doc_location = REMOTE_DOC_LOCATION sh('rsync -a -v -P -z build/sphinx/html/ %(remote_doc_location)s/%(ver)s' % locals()) def build_sdist_command(): sh('python setup.py egg_info -b "" -D sdist') def build_wheel_command(): sh('python setup.py egg_info -b "" -D bdist_wheel') def upload_sdist_command(): sh('python setup.py egg_info -b "" -D sdist') ver = version() remote_rel_location = REMOTE_REL_LOCATION sh('scp dist/MapProxy-%(ver)s.* %(remote_rel_location)s' % locals()) def upload_test_sdist_command(): date = backtick_('date +%Y%m%d').strip() print('python setup.py egg_info -R -D -b ".dev%s" register -r testpypi sdist upload -r testpypi' % (date, )) def check_uncommited(): if sh('git diff-index --quiet HEAD --') != 0: print('ABORT: uncommited changes. please commit (and tag) release version number') sys.exit(1) def upload_final_sdist_command(): check_uncommited() build_sdist_command() ver = version() sh('twine upload dist/MapProxy-%(ver)s.tar.gz' % locals()) def upload_final_wheel_command(): check_uncommited() build_wheel_command() ver = version() sh('twine upload dist/MapProxy-%(ver)s-py2.py3-none-any.whl' % locals()) def link_latest_command(ver=None): if ver is None: ver = version() host, path = REMOTE_DOC_LOCATION.split(':') sh('ssh %(host)s "cd %(path)s && rm latest && ln -s %(ver)s latest"' % locals()) def update_deb_command(): import email.utils import time changelog_entry_template = """mapproxy (%(version)s) unstable; urgency=low * New upstream release. -- %(user)s <%(email)s> %(time)s """ with open('debian/changelog') as f: old_changelog = f.read() changelog = changelog_entry_template % { 'version': version(), 'user': backtick_('git config --get user.name').strip(), 'email': backtick_('git config --get user.email').strip(), 'time': email.utils.formatdate(time.time(), True), } changelog += old_changelog with open('debian/changelog', 'w') as f: f.write(changelog) if __name__ == '__main__': scriptine.run() mapproxy-1.11.0/requirements-tests.txt000066400000000000000000000010051320454472400200760ustar00rootroot00000000000000WebTest==2.0.25 lxml==3.7.3 nose==1.3.7 Shapely==1.5.17 PyYAML==3.12 Pillow==4.0.0 WebOb==1.7.1 coverage==4.3.4 requests==2.13.0 boto3==1.4.4 moto==0.4.31 eventlet==0.20.1 beautifulsoup4==4.5.3 boto==2.46.1 botocore==1.5.14 docutils==0.13.1 enum-compat==0.0.2 futures==3.0.5 greenlet==0.4.12 riak==2.6.1 httpretty==0.8.10 Jinja2==2.9.5 jmespath==0.9.1 MarkupSafe==0.23 olefile==0.44 python-dateutil==2.6.0 pytz==2016.10 s3transfer==0.1.10 six==1.10.0 waitress==1.0.2 Werkzeug==0.11.15 xmltodict==0.10.2 redis==2.10.5 mapproxy-1.11.0/requirements-travis.txt000066400000000000000000000002121320454472400202430ustar00rootroot00000000000000WebTest==1.4.0 webob==1.1.1 lxml==2.3.5 mocker==1.1.1 nose==1.1.2 Shapely==1.2.15 PyYAML==3.10 Pillow==1.7.7 eventlet==0.9.17 riak==2.6.1 mapproxy-1.11.0/setup.cfg000066400000000000000000000002251320454472400152760ustar00rootroot00000000000000[nosetests] cover-erase = 1 verbosity = 2 doctest-tests = 1 with-doctest = 1 [egg_info] #tag_build = .dev tag_date = true [bdist_wheel] universal=1mapproxy-1.11.0/setup.py000066400000000000000000000061701320454472400151740ustar00rootroot00000000000000import platform from setuptools import setup, find_packages import pkg_resources install_requires = [ 'PyYAML>=3.0,<3.99', ] def package_installed(pkg): """Check if package is installed""" req = pkg_resources.Requirement.parse(pkg) try: pkg_resources.get_provider(req) except pkg_resources.DistributionNotFound: return False else: return True # depend in Pillow if it is installed, otherwise # depend on PIL if it is installed, otherwise # require Pillow if package_installed('Pillow'): install_requires.append('Pillow !=2.4.0') elif package_installed('PIL'): install_requires.append('PIL>=1.1.6,<1.2.99') else: install_requires.append('Pillow !=2.4.0') if platform.python_version_tuple() < ('2', '6'): # for mapproxy-seed install_requires.append('multiprocessing>=2.6') def long_description(changelog_releases=10): import re import textwrap readme = open('README.rst').read() changes = ['Changes\n-------\n'] version_line_re = re.compile('^\d\.\d+\.\d+\S*\s20\d\d-\d\d-\d\d') for line in open('CHANGES.txt'): if version_line_re.match(line): if changelog_releases == 0: break changelog_releases -= 1 changes.append(line) changes.append(textwrap.dedent(''' Older changes ------------- See https://raw.github.com/mapproxy/mapproxy/master/CHANGES.txt ''')) return readme + ''.join(changes) setup( name='MapProxy', version="1.11.0", description='An accelerating proxy for tile and web map services', long_description=long_description(7), author='Oliver Tonnhofer', author_email='olt@omniscale.de', url='https://mapproxy.org', license='Apache Software License 2.0', packages=find_packages(), include_package_data=True, entry_points = { 'console_scripts': [ 'mapproxy-seed = mapproxy.seed.script:main', 'mapproxy-util = mapproxy.script.util:main', ], 'paste.app_factory': [ 'app = mapproxy.wsgiapp:app_factory', 'multiapp = mapproxy.multiapp:app_factory' ], 'paste.paster_create_template': [ 'mapproxy_conf=mapproxy.config_template:PasterConfigurationTemplate' ], 'paste.filter_factory': [ 'lighttpd_root_fix = mapproxy.util.wsgi:lighttpd_root_fix_filter_factory', ], }, package_data = {'': ['*.xml', '*.yaml', '*.ttf', '*.wsgi', '*.ini']}, install_requires=install_requires, classifiers=[ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Internet :: Proxy Servers", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Scientific/Engineering :: GIS", ], zip_safe=False, test_suite='nose.collector', ) mapproxy-1.11.0/tox.ini000066400000000000000000000015351320454472400147750ustar00rootroot00000000000000[tox] envlist = py26,py27,py33,py34,docs [testenv] commands = nosetests --with-xunit --xunit-file=nosetests-{envname}.xml mapproxy deps = WebTest==2.0.10 lxml==3.2.4 nose==1.3.0 Shapely==1.3.2 PyYAML==3.10 Pillow==2.3.1 WebOb==1.2.3 beautifulsoup4==4.3.2 coverage==3.7 requests==2.0.1 six==1.4.1 waitress==0.8.7 [testenv:hash] setenv = PYTHONHASHSEED = 100 [testenv:docs] changedir = doc basepython = python2.7 sitepackages = True deps = sphinx==1.2.2 sphinx-bootstrap-theme==0.4.7 commands = sphinx-build -b html -d {envtmpdir}/doctrees . {envtmpdir}/html sphinx-build -b latex -d {envtmpdir}/doctrees . {envtmpdir}/latex make -C {envtmpdir}/latex all-pdf rsync -a --delete-after {envtmpdir}/html/ {envtmpdir}/latex/MapProxy.pdf os@mapproxy.org:/opt/www/mapproxy.org/docs/nightly/