[mapproxy] 01/28: Imported Upstream version 1.8.2

Bas Couwenberg sebastic at debian.org
Wed Jul 27 00:32:55 UTC 2016


This is an automated email from the git hooks/post-receive script.

sebastic pushed a commit to branch master
in repository mapproxy.

commit c6e4a4b67c492e9426c181e8f8bcf249e72342ef
Author: Bas Couwenberg <sebastic at xs4all.nl>
Date:   Mon Jul 25 18:46:18 2016 +0200

    Imported Upstream version 1.8.2
---
 CHANGES.txt                                        |  721 +++++++
 COPYING.txt                                        |   60 +
 LICENSE.txt                                        |  202 ++
 MANIFEST.in                                        |   26 +
 MapProxy.egg-info/PKG-INFO                         |  227 ++
 MapProxy.egg-info/SOURCES.txt                      |  484 +++++
 MapProxy.egg-info/dependency_links.txt             |    1 +
 MapProxy.egg-info/entry_points.txt                 |   14 +
 MapProxy.egg-info/namespace_packages.txt           |    1 +
 MapProxy.egg-info/not-zip-safe                     |    1 +
 MapProxy.egg-info/pbr.json                         |    1 +
 MapProxy.egg-info/requires.txt                     |    2 +
 MapProxy.egg-info/top_level.txt                    |    1 +
 PKG-INFO                                           |  227 ++
 README.rst                                         |   12 +
 doc/GM.txt                                         |   23 +
 doc/Makefile                                       |   89 +
 doc/_static/logo.png                               |  Bin 0 -> 1368 bytes
 doc/_static/mapproxy.css                           |   68 +
 doc/_templates/layout.html                         |   16 +
 doc/_templates/navbar.html                         |   24 +
 doc/_templates/toctree.html                        |    1 +
 doc/auth.rst                                       |  531 +++++
 doc/caches.rst                                     |  290 +++
 doc/conf.py                                        |  204 ++
 doc/configuration.rst                              | 1055 +++++++++
 doc/configuration_examples.rst                     |  831 +++++++
 doc/coverages.rst                                  |  133 ++
 doc/decorate_img.rst                               |  106 +
 doc/deployment.rst                                 |  341 +++
 doc/development.rst                                |  104 +
 doc/imgs/bicubic.png                               |  Bin 0 -> 20437 bytes
 doc/imgs/bilinear.png                              |  Bin 0 -> 18588 bytes
 doc/imgs/labeling-dynamic.png                      |  Bin 0 -> 7633 bytes
 doc/imgs/labeling-meta-buffer.png                  |  Bin 0 -> 6825 bytes
 doc/imgs/labeling-metatiling-buffer.png            |  Bin 0 -> 4011 bytes
 doc/imgs/labeling-metatiling.png                   |  Bin 0 -> 2594 bytes
 doc/imgs/labeling-no-clip.png                      |  Bin 0 -> 7765 bytes
 doc/imgs/labeling-no-placement.png                 |  Bin 0 -> 5092 bytes
 doc/imgs/labeling-partial-false.png                |  Bin 0 -> 5321 bytes
 doc/imgs/labeling-repeated.png                     |  Bin 0 -> 6567 bytes
 doc/imgs/mapnik-webmerc-hq.png                     |  Bin 0 -> 32025 bytes
 doc/imgs/mapnik-webmerc.png                        |  Bin 0 -> 12649 bytes
 doc/imgs/mapproxy-demo.png                         |  Bin 0 -> 18652 bytes
 doc/imgs/nearest.png                               |  Bin 0 -> 15501 bytes
 doc/index.rst                                      |   38 +
 doc/inspire.rst                                    |  157 ++
 doc/install.rst                                    |  191 ++
 doc/install_osgeo4w.rst                            |   67 +
 doc/install_windows.rst                            |  113 +
 doc/labeling.rst                                   |  300 +++
 doc/mapproxy_2.rst                                 |   47 +
 doc/mapproxy_util.rst                              |  538 +++++
 doc/mapproxy_util_autoconfig.rst                   |  165 ++
 doc/seed.rst                                       |  487 +++++
 doc/services.rst                                   |  390 ++++
 doc/sources.rst                                    |  497 +++++
 doc/tutorial.rst                                   |  429 ++++
 doc/tutorial.yaml                                  |  109 +
 doc/yaml/cache_conf.yaml                           |   41 +
 doc/yaml/grid_conf.yaml                            |   48 +
 doc/yaml/merged_conf.yaml                          |   47 +
 doc/yaml/meta_conf.yaml                            |   49 +
 doc/yaml/seed.yaml                                 |   12 +
 doc/yaml/simple_conf.yaml                          |   36 +
 mapproxy/__init__.py                               |    1 +
 mapproxy/cache/__init__.py                         |   36 +
 mapproxy/cache/base.py                             |  111 +
 mapproxy/cache/couchdb.py                          |  305 +++
 mapproxy/cache/dummy.py                            |   34 +
 mapproxy/cache/file.py                             |  275 +++
 mapproxy/cache/legend.py                           |   84 +
 mapproxy/cache/mbtiles.py                          |  346 +++
 mapproxy/cache/meta.py                             |   78 +
 mapproxy/cache/renderd.py                          |   92 +
 mapproxy/cache/riak.py                             |  196 ++
 mapproxy/cache/sqlite.py                           |  413 ++++
 mapproxy/cache/tile.py                             |  482 +++++
 mapproxy/client/__init__.py                        |    0
 mapproxy/client/cgi.py                             |  140 ++
 mapproxy/client/http.py                            |  272 +++
 mapproxy/client/log.py                             |   33 +
 mapproxy/client/tile.py                            |  167 ++
 mapproxy/client/wms.py                             |  229 ++
 mapproxy/compat/__init__.py                        |   41 +
 mapproxy/compat/image.py                           |   69 +
 mapproxy/compat/itertools.py                       |   27 +
 mapproxy/compat/modules.py                         |    9 +
 mapproxy/config/__init__.py                        |   22 +
 mapproxy/config/config.py                          |  204 ++
 mapproxy/config/coverage.py                        |   75 +
 mapproxy/config/defaults.py                        |   95 +
 mapproxy/config/loader.py                          | 1758 +++++++++++++++
 mapproxy/config/spec.py                            |  510 +++++
 mapproxy/config/validator.py                       |  206 ++
 mapproxy/config_template/__init__.py               |   14 +
 mapproxy/config_template/base_config/config.wsgi   |   10 +
 .../config_template/base_config/full_example.yaml  |  566 +++++
 .../base_config/full_seed_example.yaml             |   79 +
 mapproxy/config_template/base_config/log.ini       |   35 +
 mapproxy/config_template/base_config/mapproxy.yaml |   61 +
 mapproxy/config_template/base_config/seed.yaml     |   27 +
 mapproxy/config_template/paster/etc/config.ini     |   21 +
 mapproxy/config_template/paster/etc/config.wsgi    |    6 +
 mapproxy/config_template/paster/etc/develop.ini    |   39 +
 mapproxy/config_template/paster/etc/log_deploy.ini |   45 +
 mapproxy/config_template/paster/etc/mapproxy.yaml  |  140 ++
 mapproxy/config_template/paster/etc/seed.yaml      |   50 +
 mapproxy/exception.py                              |  140 ++
 mapproxy/featureinfo.py                            |  162 ++
 mapproxy/grid.py                                   | 1163 ++++++++++
 mapproxy/image/__init__.py                         |  451 ++++
 mapproxy/image/fonts/DejaVuSans.ttf                |  Bin 0 -> 622020 bytes
 mapproxy/image/fonts/DejaVuSansMono.ttf            |  Bin 0 -> 320812 bytes
 mapproxy/image/fonts/LICENSE                       |   99 +
 mapproxy/image/fonts/__init__.py                   |    0
 mapproxy/image/mask.py                             |   55 +
 mapproxy/image/merge.py                            |  183 ++
 mapproxy/image/message.py                          |  347 +++
 mapproxy/image/opts.py                             |  167 ++
 mapproxy/image/tile.py                             |  167 ++
 mapproxy/image/transform.py                        |  195 ++
 mapproxy/layer.py                                  |  483 +++++
 mapproxy/multiapp.py                               |  228 ++
 mapproxy/proj.py                                   |  275 +++
 mapproxy/request/__init__.py                       |   18 +
 mapproxy/request/base.py                           |  467 ++++
 mapproxy/request/tile.py                           |  128 ++
 mapproxy/request/wms/__init__.py                   |  761 +++++++
 mapproxy/request/wms/exception.py                  |   99 +
 mapproxy/request/wmts.py                           |  387 ++++
 mapproxy/response.py                               |  228 ++
 mapproxy/script/__init__.py                        |    0
 mapproxy/script/conf/__init__.py                   |    0
 mapproxy/script/conf/app.py                        |  192 ++
 mapproxy/script/conf/caches.py                     |   45 +
 mapproxy/script/conf/layers.py                     |   54 +
 mapproxy/script/conf/seeds.py                      |   37 +
 mapproxy/script/conf/sources.py                    |   86 +
 mapproxy/script/conf/utils.py                      |  143 ++
 mapproxy/script/export.py                          |  261 +++
 mapproxy/script/grids.py                           |  191 ++
 mapproxy/script/scales.py                          |  126 ++
 mapproxy/script/util.py                            |  368 ++++
 mapproxy/script/wms_capabilities.py                |  152 ++
 mapproxy/seed/__init__.py                          |    0
 mapproxy/seed/cachelock.py                         |  122 ++
 mapproxy/seed/cleanup.py                           |   98 +
 mapproxy/seed/config.py                            |  462 ++++
 mapproxy/seed/script.py                            |  268 +++
 mapproxy/seed/seeder.py                            |  492 +++++
 mapproxy/seed/spec.py                              |   74 +
 mapproxy/seed/util.py                              |  294 +++
 mapproxy/service/__init__.py                       |   14 +
 mapproxy/service/base.py                           |   46 +
 mapproxy/service/demo.py                           |  247 +++
 mapproxy/service/kml.py                            |  326 +++
 mapproxy/service/ows.py                            |   38 +
 mapproxy/service/template_helper.py                |   73 +
 .../service/templates/demo/capabilities_demo.html  |   18 +
 mapproxy/service/templates/demo/demo.html          |  179 ++
 .../service/templates/demo/openlayers-demo.cfg     |   16 +
 mapproxy/service/templates/demo/static.html        |   34 +
 .../service/templates/demo/static/OpenLayers.js    |  619 ++++++
 .../service/templates/demo/static/img/blank.gif    |  Bin 0 -> 42 bytes
 .../templates/demo/static/img/east-mini.png        |  Bin 0 -> 451 bytes
 .../templates/demo/static/img/north-mini.png       |  Bin 0 -> 484 bytes
 .../templates/demo/static/img/south-mini.png       |  Bin 0 -> 481 bytes
 .../templates/demo/static/img/west-mini.png        |  Bin 0 -> 453 bytes
 .../templates/demo/static/img/zoom-minus-mini.png  |  Bin 0 -> 359 bytes
 .../templates/demo/static/img/zoom-plus-mini.png   |  Bin 0 -> 489 bytes
 .../templates/demo/static/img/zoom-world-mini.png  |  Bin 0 -> 1072 bytes
 mapproxy/service/templates/demo/static/logo.png    |  Bin 0 -> 1368 bytes
 mapproxy/service/templates/demo/static/site.css    |  132 ++
 .../demo/static/theme/default/framedCloud.css      |    0
 .../templates/demo/static/theme/default/google.css |   17 +
 .../demo/static/theme/default/ie6-style.css        |   10 +
 .../templates/demo/static/theme/default/style.css  |  482 +++++
 mapproxy/service/templates/demo/tms_demo.html      |   81 +
 mapproxy/service/templates/demo/wms_demo.html      |   77 +
 mapproxy/service/templates/demo/wmts_demo.html     |   74 +
 mapproxy/service/templates/tms_capabilities.xml    |   13 +
 mapproxy/service/templates/tms_exception.xml       |    4 +
 mapproxy/service/templates/tms_root_resource.xml   |    7 +
 .../service/templates/tms_tilemap_capabilities.xml |   14 +
 mapproxy/service/templates/wms100capabilities.xml  |  101 +
 mapproxy/service/templates/wms100exception.xml     |    4 +
 mapproxy/service/templates/wms110capabilities.xml  |  141 ++
 mapproxy/service/templates/wms110exception.xml     |    5 +
 mapproxy/service/templates/wms111capabilities.xml  |  172 ++
 mapproxy/service/templates/wms111exception.xml     |    5 +
 mapproxy/service/templates/wms130capabilities.xml  |  311 +++
 mapproxy/service/templates/wms130exception.xml     |    8 +
 mapproxy/service/templates/wmts100capabilities.xml |  120 ++
 mapproxy/service/templates/wmts100exception.xml    |    9 +
 mapproxy/service/tile.py                           |  479 +++++
 mapproxy/service/wms.py                            |  797 +++++++
 mapproxy/service/wmts.py                           |  298 +++
 mapproxy/source/__init__.py                        |   74 +
 mapproxy/source/error.py                           |   38 +
 mapproxy/source/mapnik.py                          |  151 ++
 mapproxy/source/tile.py                            |   96 +
 mapproxy/source/wms.py                             |  239 +++
 mapproxy/srs.py                                    |  422 ++++
 mapproxy/template.py                               |   52 +
 mapproxy/test/__init__.py                          |    0
 mapproxy/test/helper.py                            |  230 ++
 mapproxy/test/http.py                              |  428 ++++
 mapproxy/test/image.py                             |  209 ++
 mapproxy/test/mocker.py                            | 2268 ++++++++++++++++++++
 .../test/schemas/inspire/common/1.0/common.xsd     | 1461 +++++++++++++
 .../schemas/inspire/common/1.0/enums/enum_bul.xsd  |  108 +
 .../schemas/inspire/common/1.0/enums/enum_cze.xsd  |  108 +
 .../schemas/inspire/common/1.0/enums/enum_dan.xsd  |  108 +
 .../schemas/inspire/common/1.0/enums/enum_dut.xsd  |  108 +
 .../schemas/inspire/common/1.0/enums/enum_eng.xsd  |  155 ++
 .../schemas/inspire/common/1.0/enums/enum_est.xsd  |  108 +
 .../schemas/inspire/common/1.0/enums/enum_fin.xsd  |  108 +
 .../schemas/inspire/common/1.0/enums/enum_fre.xsd  |  108 +
 .../schemas/inspire/common/1.0/enums/enum_ger.xsd  |  108 +
 .../schemas/inspire/common/1.0/enums/enum_gle.xsd  |  109 +
 .../schemas/inspire/common/1.0/enums/enum_gre.xsd  |  108 +
 .../schemas/inspire/common/1.0/enums/enum_hun.xsd  |  108 +
 .../schemas/inspire/common/1.0/enums/enum_ita.xsd  |  108 +
 .../schemas/inspire/common/1.0/enums/enum_lav.xsd  |  108 +
 .../schemas/inspire/common/1.0/enums/enum_lit.xsd  |  108 +
 .../schemas/inspire/common/1.0/enums/enum_mlt.xsd  |  108 +
 .../schemas/inspire/common/1.0/enums/enum_pol.xsd  |  108 +
 .../schemas/inspire/common/1.0/enums/enum_por.xsd  |  108 +
 .../schemas/inspire/common/1.0/enums/enum_rum.xsd  |  108 +
 .../schemas/inspire/common/1.0/enums/enum_slo.xsd  |  108 +
 .../schemas/inspire/common/1.0/enums/enum_slv.xsd  |  108 +
 .../schemas/inspire/common/1.0/enums/enum_spa.xsd  |  108 +
 .../schemas/inspire/common/1.0/enums/enum_swe.xsd  |  108 +
 .../1.0/examples/inspireresourcemddataset.xml      |  128 ++
 .../1.0/examples/inspireresourcemdseries.xml       |  114 +
 .../1.0/examples/inspireresourcemdservice.xml      |   91 +
 .../test/schemas/inspire/common/1.0/network.xsd    |  521 +++++
 .../WMS_Image2000GetCapabilities_InspireSchema.xml |  271 +++
 .../inspire/inspire_vs/1.0/examples/wms_at.xml     |  358 +++
 .../inspire_vs/1.0/examples/wms_geoimage.xml       |  346 +++
 .../schemas/inspire/inspire_vs/1.0/inspire_vs.xsd  |   19 +
 mapproxy/test/schemas/kml/2.2.0/ReadMe.txt         |   14 +
 .../test/schemas/kml/2.2.0/atom-author-link.xsd    |   66 +
 mapproxy/test/schemas/kml/2.2.0/ogckml22.xsd       | 1646 ++++++++++++++
 mapproxy/test/schemas/kml/2.2.0/xAL.xsd            | 1680 +++++++++++++++
 mapproxy/test/schemas/ows/1.1.0/ReadMe.txt         |   87 +
 mapproxy/test/schemas/ows/1.1.0/ows19115subset.xsd |  235 ++
 mapproxy/test/schemas/ows/1.1.0/owsAll.xsd         |   23 +
 mapproxy/test/schemas/ows/1.1.0/owsCommon.xsd      |  157 ++
 mapproxy/test/schemas/ows/1.1.0/owsContents.xsd    |   86 +
 .../schemas/ows/1.1.0/owsDataIdentification.xsd    |  127 ++
 mapproxy/test/schemas/ows/1.1.0/owsDomainType.xsd  |  279 +++
 .../test/schemas/ows/1.1.0/owsExceptionReport.xsd  |   76 +
 .../test/schemas/ows/1.1.0/owsGetCapabilities.xsd  |  112 +
 .../test/schemas/ows/1.1.0/owsGetResourceByID.xsd  |   51 +
 .../test/schemas/ows/1.1.0/owsInputOutputData.xsd  |   59 +
 mapproxy/test/schemas/ows/1.1.0/owsManifest.xsd    |  125 ++
 .../schemas/ows/1.1.0/owsOperationsMetadata.xsd    |  140 ++
 .../schemas/ows/1.1.0/owsServiceIdentification.xsd |   60 +
 .../test/schemas/ows/1.1.0/owsServiceProvider.xsd  |   47 +
 .../test/schemas/sld/1.1.0/sld_capabilities.xsd    |   27 +
 .../test/schemas/wms/1.0.0/capabilities_1_0_0.dtd  |  353 +++
 .../test/schemas/wms/1.0.0/capabilities_1_0_0.xml  |  188 ++
 .../test/schemas/wms/1.0.7/capabilities_1_0_7.dtd  |  524 +++++
 .../test/schemas/wms/1.0.7/capabilities_1_0_7.xml  |  260 +++
 .../test/schemas/wms/1.1.0/capabilities_1_1_0.dtd  |  273 +++
 .../test/schemas/wms/1.1.0/capabilities_1_1_0.xml  |  303 +++
 .../test/schemas/wms/1.1.0/exception_1_1_0.dtd     |    6 +
 .../test/schemas/wms/1.1.0/exception_1_1_0.xml     |   33 +
 mapproxy/test/schemas/wms/1.1.1/OGC-exception.xsd  |   68 +
 .../wms/1.1.1/WMS_DescribeLayerResponse.dtd        |   22 +
 .../test/schemas/wms/1.1.1/WMS_MS_Capabilities.dtd |  274 +++
 .../test/schemas/wms/1.1.1/WMS_exception_1_1_1.dtd |    5 +
 .../test/schemas/wms/1.1.1/capabilities_1_1_1.dtd  |  276 +++
 .../test/schemas/wms/1.1.1/capabilities_1_1_1.xml  |  303 +++
 .../test/schemas/wms/1.1.1/exception_1_1_1.dtd     |    6 +
 .../test/schemas/wms/1.1.1/exception_1_1_1.xml     |   33 +
 mapproxy/test/schemas/wms/1.3.0/ReadMe.txt         |    8 +
 .../test/schemas/wms/1.3.0/capabilities_1_3_0.xml  |  277 +++
 .../test/schemas/wms/1.3.0/capabilities_1_3_0.xsd  |  611 ++++++
 .../test/schemas/wms/1.3.0/exceptions_1_3_0.xml    |   34 +
 .../test/schemas/wms/1.3.0/exceptions_1_3_0.xsd    |   28 +
 mapproxy/test/schemas/wmsc/1.1.1/OGC-exception.xsd |   68 +
 .../wmsc/1.1.1/WMS_DescribeLayerResponse.dtd       |   22 +
 .../schemas/wmsc/1.1.1/WMS_MS_Capabilities.dtd     |  283 +++
 .../schemas/wmsc/1.1.1/WMS_exception_1_1_1.dtd     |    5 +
 .../test/schemas/wmsc/1.1.1/capabilities_1_1_1.dtd |  276 +++
 .../test/schemas/wmsc/1.1.1/capabilities_1_1_1.xml |  303 +++
 .../test/schemas/wmsc/1.1.1/exception_1_1_1.dtd    |    6 +
 .../test/schemas/wmsc/1.1.1/exception_1_1_1.xml    |   33 +
 mapproxy/test/schemas/wmts/1.0/ReadMe.txt          |   32 +
 mapproxy/test/schemas/wmts/1.0/wmts.xsd            |   28 +
 mapproxy/test/schemas/wmts/1.0/wmtsAbstract.wsdl   |  151 ++
 .../wmts/1.0/wmtsGetCapabilities_request.xsd       |   38 +
 .../wmts/1.0/wmtsGetCapabilities_response.xsd      |  564 +++++
 .../wmts/1.0/wmtsGetFeatureInfo_request.xsd        |   57 +
 .../wmts/1.0/wmtsGetFeatureInfo_response.xsd       |   72 +
 .../test/schemas/wmts/1.0/wmtsGetTile_request.xsd  |   91 +
 mapproxy/test/schemas/wmts/1.0/wmtsKVP.xsd         |   76 +
 .../test/schemas/wmts/1.0/wmtsPayload_response.xsd |   70 +
 mapproxy/test/schemas/xlink/1.0.0/ReadMe.txt       |    6 +
 mapproxy/test/schemas/xlink/1.0.0/xlinks.xsd       |  122 ++
 mapproxy/test/schemas/xml.xsd                      |  287 +++
 mapproxy/test/system/__init__.py                   |   91 +
 mapproxy/test/system/fixture/auth.yaml             |   67 +
 mapproxy/test/system/fixture/cache.mbtiles         |  Bin 0 -> 8192 bytes
 .../01/000/000/000/000/000/001.jpeg                |  Bin 0 -> 10214 bytes
 .../01/000/000/000/000/000/001.png                 |  Bin 0 -> 463 bytes
 mapproxy/test/system/fixture/cache_grid_names.yaml |   50 +
 mapproxy/test/system/fixture/cache_mbtiles.yaml    |   28 +
 mapproxy/test/system/fixture/cache_source.yaml     |   81 +
 mapproxy/test/system/fixture/cgi.py                |   16 +
 mapproxy/test/system/fixture/combined_sources.yaml |  130 ++
 mapproxy/test/system/fixture/coverage.yaml         |   79 +
 mapproxy/test/system/fixture/disable_storage.yaml  |   25 +
 mapproxy/test/system/fixture/empty_ogrdata.geojson |    1 +
 mapproxy/test/system/fixture/formats.yaml          |   74 +
 mapproxy/test/system/fixture/inspire.yaml          |  103 +
 mapproxy/test/system/fixture/inspire_full.yaml     |  126 ++
 mapproxy/test/system/fixture/kml_layer.yaml        |   66 +
 mapproxy/test/system/fixture/layer.yaml            |  232 ++
 mapproxy/test/system/fixture/layergroups.yaml      |   57 +
 mapproxy/test/system/fixture/layergroups_root.yaml |  106 +
 mapproxy/test/system/fixture/legendgraphic.yaml    |   95 +
 mapproxy/test/system/fixture/mapnik_source.yaml    |   54 +
 mapproxy/test/system/fixture/mapproxy_export.yaml  |   12 +
 mapproxy/test/system/fixture/mapserver.yaml        |   23 +
 mapproxy/test/system/fixture/mixed_mode.yaml       |   51 +
 mapproxy/test/system/fixture/multiapp1.yaml        |   20 +
 mapproxy/test/system/fixture/multiapp2.yaml        |   19 +
 mapproxy/test/system/fixture/renderd_client.yaml   |   55 +
 mapproxy/test/system/fixture/scalehints.yaml       |   72 +
 mapproxy/test/system/fixture/seed.yaml             |   94 +
 mapproxy/test/system/fixture/seed_mapproxy.yaml    |   36 +
 mapproxy/test/system/fixture/seed_old.yaml         |   12 +
 mapproxy/test/system/fixture/seed_timeouts.yaml    |   12 +
 .../system/fixture/seed_timeouts_mapproxy.yaml     |   27 +
 mapproxy/test/system/fixture/seedonly.yaml         |   53 +
 mapproxy/test/system/fixture/sld.yaml              |   35 +
 mapproxy/test/system/fixture/source_errors.yaml    |   82 +
 .../test/system/fixture/source_errors_raise.yaml   |   82 +
 .../test/system/fixture/tileservice_origin.yaml    |   26 +
 .../test/system/fixture/tilesource_minmax_res.yaml |   22 +
 .../test/system/fixture/util-conf-base-grids.yaml  |    5 +
 .../test/system/fixture/util-conf-overwrite.yaml   |   13 +
 .../test/system/fixture/util-conf-wms-111-cap.xml  |   90 +
 mapproxy/test/system/fixture/util_grids.yaml       |   29 +
 .../system/fixture/util_wms_capabilities111.xml    |  130 ++
 .../system/fixture/util_wms_capabilities130.xml    |  100 +
 .../util_wms_capabilities_service_exception.xml    |    5 +
 mapproxy/test/system/fixture/watermark.yaml        |   50 +
 mapproxy/test/system/fixture/wms_srs_extent.yaml   |   21 +
 mapproxy/test/system/fixture/wms_versions.yaml     |   40 +
 mapproxy/test/system/fixture/wmts.yaml             |  111 +
 mapproxy/test/system/fixture/wmts_dimensions.yaml  |   57 +
 mapproxy/test/system/fixture/xslt_featureinfo.yaml |   53 +
 mapproxy/test/system/test_auth.py                  |  828 +++++++
 mapproxy/test/system/test_behind_proxy.py          |   80 +
 mapproxy/test/system/test_cache_grid_names.py      |   94 +
 mapproxy/test/system/test_cache_mbtiles.py         |   75 +
 mapproxy/test/system/test_cache_source.py          |  112 +
 mapproxy/test/system/test_combined_sources.py      |  251 +++
 mapproxy/test/system/test_coverage.py              |  115 +
 mapproxy/test/system/test_decorate_img.py          |  188 ++
 mapproxy/test/system/test_disable_storage.py       |   54 +
 mapproxy/test/system/test_formats.py               |  164 ++
 mapproxy/test/system/test_inspire_vs.py            |  140 ++
 mapproxy/test/system/test_kml.py                   |  236 ++
 mapproxy/test/system/test_layergroups.py           |  143 ++
 mapproxy/test/system/test_legendgraphic.py         |  206 ++
 mapproxy/test/system/test_mapnik.py                |  155 ++
 mapproxy/test/system/test_mapserver.py             |   69 +
 mapproxy/test/system/test_mixed_mode_format.py     |  153 ++
 mapproxy/test/system/test_multiapp.py              |  103 +
 mapproxy/test/system/test_renderd_client.py        |  269 +++
 mapproxy/test/system/test_scalehints.py            |  112 +
 mapproxy/test/system/test_seed.py                  |  387 ++++
 mapproxy/test/system/test_seed_only.py             |   82 +
 mapproxy/test/system/test_sld.py                   |   88 +
 mapproxy/test/system/test_source_errors.py         |  256 +++
 mapproxy/test/system/test_tilesource_minmax_res.py |   50 +
 mapproxy/test/system/test_tms.py                   |  247 +++
 mapproxy/test/system/test_tms_origin.py            |   52 +
 mapproxy/test/system/test_util_conf.py             |  226 ++
 mapproxy/test/system/test_util_export.py           |  132 ++
 mapproxy/test/system/test_util_grids.py            |   95 +
 mapproxy/test/system/test_util_wms_capabilities.py |  101 +
 mapproxy/test/system/test_watermark.py             |   74 +
 mapproxy/test/system/test_wms.py                   | 1098 ++++++++++
 mapproxy/test/system/test_wms_srs_extent.py        |   98 +
 mapproxy/test/system/test_wms_version.py           |   57 +
 mapproxy/test/system/test_wmsc.py                  |   92 +
 mapproxy/test/system/test_wmts.py                  |  158 ++
 mapproxy/test/system/test_wmts_dimensions.py       |  164 ++
 mapproxy/test/system/test_wmts_restful.py          |  114 +
 mapproxy/test/system/test_xslt_featureinfo.py      |  205 ++
 mapproxy/test/test_http_helper.py                  |  209 ++
 mapproxy/test/unit/__init__.py                     |    0
 mapproxy/test/unit/epsg                            |    2 +
 mapproxy/test/unit/polygons/polygons.dbf           |  Bin 0 -> 833 bytes
 mapproxy/test/unit/polygons/polygons.shp           |  Bin 0 -> 1580 bytes
 mapproxy/test/unit/polygons/polygons.shx           |  Bin 0 -> 124 bytes
 mapproxy/test/unit/test_async.py                   |  340 +++
 mapproxy/test/unit/test_auth.py                    |  400 ++++
 mapproxy/test/unit/test_cache.py                   |  876 ++++++++
 mapproxy/test/unit/test_cache_couchdb.py           |  117 +
 mapproxy/test/unit/test_cache_riak.py              |   71 +
 mapproxy/test/unit/test_cache_tile.py              |  339 +++
 mapproxy/test/unit/test_client.py                  |  365 ++++
 mapproxy/test/unit/test_client_cgi.py              |  133 ++
 mapproxy/test/unit/test_collections.py             |  115 +
 mapproxy/test/unit/test_concat_legends.py          |   37 +
 mapproxy/test/unit/test_conf_loader.py             |  891 ++++++++
 mapproxy/test/unit/test_conf_validator.py          |  331 +++
 mapproxy/test/unit/test_config.py                  |  118 +
 mapproxy/test/unit/test_decorate_img.py            |  171 ++
 mapproxy/test/unit/test_exceptions.py              |  255 +++
 mapproxy/test/unit/test_featureinfo.py             |  179 ++
 mapproxy/test/unit/test_file_lock_load.py          |   42 +
 mapproxy/test/unit/test_geom.py                    |  347 +++
 mapproxy/test/unit/test_grid.py                    | 1228 +++++++++++
 mapproxy/test/unit/test_image.py                   |  562 +++++
 mapproxy/test/unit/test_image_mask.py              |   89 +
 mapproxy/test/unit/test_image_messages.py          |  182 ++
 mapproxy/test/unit/test_image_options.py           |  160 ++
 mapproxy/test/unit/test_multiapp.py                |  172 ++
 mapproxy/test/unit/test_ogr_reader.py              |   41 +
 mapproxy/test/unit/test_request.py                 |  536 +++++
 mapproxy/test/unit/test_request_wmts.py            |   75 +
 mapproxy/test/unit/test_response.py                |   75 +
 mapproxy/test/unit/test_seed.py                    |  265 +++
 mapproxy/test/unit/test_seed_cachelock.py          |   89 +
 mapproxy/test/unit/test_srs.py                     |   94 +
 mapproxy/test/unit/test_tiled_source.py            |   74 +
 mapproxy/test/unit/test_tilefilter.py              |   30 +
 mapproxy/test/unit/test_times.py                   |   13 +
 mapproxy/test/unit/test_timeutils.py               |   50 +
 mapproxy/test/unit/test_util_conf_utils.py         |   69 +
 mapproxy/test/unit/test_utils.py                   |  445 ++++
 mapproxy/test/unit/test_wms_capabilities.py        |   42 +
 mapproxy/test/unit/test_wms_layer.py               |   78 +
 mapproxy/test/unit/test_yaml.py                    |   61 +
 mapproxy/tilefilter.py                             |   59 +
 mapproxy/util/__init__.py                          |    0
 mapproxy/util/async.py                             |  344 +++
 mapproxy/util/collections.py                       |  132 ++
 mapproxy/util/coverage.py                          |  258 +++
 mapproxy/util/ext/__init__.py                      |   14 +
 mapproxy/util/ext/dictspec/__init__.py             |    1 +
 mapproxy/util/ext/dictspec/spec.py                 |  120 ++
 mapproxy/util/ext/dictspec/test/__init__.py        |    0
 mapproxy/util/ext/dictspec/test/test_validator.py  |  274 +++
 mapproxy/util/ext/dictspec/validator.py            |  190 ++
 mapproxy/util/ext/local.py                         |  196 ++
 mapproxy/util/ext/lockfile.py                      |  138 ++
 mapproxy/util/ext/odict.py                         |  330 +++
 mapproxy/util/ext/serving.py                       |  773 +++++++
 mapproxy/util/ext/tempita/__init__.py              | 1172 ++++++++++
 mapproxy/util/ext/tempita/_looper.py               |  163 ++
 mapproxy/util/ext/tempita/compat3.py               |   45 +
 mapproxy/util/ext/wmsparse/__init__.py             |    3 +
 mapproxy/util/ext/wmsparse/parse.py                |  305 +++
 mapproxy/util/ext/wmsparse/test/__init__.py        |    0
 mapproxy/util/ext/wmsparse/test/test_parse.py      |  107 +
 mapproxy/util/ext/wmsparse/test/test_util.py       |   16 +
 mapproxy/util/ext/wmsparse/test/wms-large-111.xml  | 2114 ++++++++++++++++++
 .../util/ext/wmsparse/test/wms-omniscale-111.xml   |   90 +
 .../util/ext/wmsparse/test/wms-omniscale-130.xml   |  120 ++
 mapproxy/util/ext/wmsparse/test/wms_nasa_cap.xml   |  386 ++++
 mapproxy/util/ext/wmsparse/util.py                 |   22 +
 mapproxy/util/fs.py                                |  139 ++
 mapproxy/util/geom.py                              |  219 ++
 mapproxy/util/lib.py                               |  108 +
 mapproxy/util/lock.py                              |  164 ++
 mapproxy/util/ogr.py                               |  228 ++
 mapproxy/util/py.py                                |   81 +
 mapproxy/util/times.py                             |   75 +
 mapproxy/util/wsgi.py                              |   41 +
 mapproxy/util/yaml.py                              |   48 +
 mapproxy/version.py                                |   31 +
 mapproxy/wsgiapp.py                                |  211 ++
 requirements-tests.txt                             |   12 +
 setup.cfg                                          |   14 +
 setup.py                                           |  100 +
 485 files changed, 82071 insertions(+)

diff --git a/CHANGES.txt b/CHANGES.txt
new file mode 100644
index 0000000..e59cac0
--- /dev/null
+++ b/CHANGES.txt
@@ -0,0 +1,721 @@
+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
diff --git a/COPYING.txt b/COPYING.txt
new file mode 100644
index 0000000..4181e2d
--- /dev/null
+++ b/COPYING.txt
@@ -0,0 +1,60 @@
+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.
+
+
+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
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,202 @@
+
+                                 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.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..7a0bc2c
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,26 @@
+include 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 *.xml
\ No newline at end of file
diff --git a/MapProxy.egg-info/PKG-INFO b/MapProxy.egg-info/PKG-INFO
new file mode 100644
index 0000000..a32f6c4
--- /dev/null
+++ b/MapProxy.egg-info/PKG-INFO
@@ -0,0 +1,227 @@
+Metadata-Version: 1.1
+Name: MapProxy
+Version: 1.8.2
+Summary: An accelerating proxy for web map services
+Home-page: http://mapproxy.org
+Author: Oliver Tonnhofer
+Author-email: olt at omniscale.de
+License: Apache Software License 2.0
+Description: 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.
+        
+        .. image:: http://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 <http://omniscale.com>`_, 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 http://mapproxy.org/ for more information.
+        
+        The documentation is available at: http://mapproxy.org/docs/latest/
+        
+        Changes
+        -------
+        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
+        
+        
+        Older changes
+        -------------
+        See https://raw.github.com/mapproxy/mapproxy/master/CHANGES.txt
+        
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Topic :: Internet :: Proxy Servers
+Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
+Classifier: Topic :: Scientific/Engineering :: GIS
diff --git a/MapProxy.egg-info/SOURCES.txt b/MapProxy.egg-info/SOURCES.txt
new file mode 100644
index 0000000..46e2031
--- /dev/null
+++ b/MapProxy.egg-info/SOURCES.txt
@@ -0,0 +1,484 @@
+CHANGES.txt
+COPYING.txt
+LICENSE.txt
+MANIFEST.in
+README.rst
+requirements-tests.txt
+setup.cfg
+setup.py
+MapProxy.egg-info/PKG-INFO
+MapProxy.egg-info/SOURCES.txt
+MapProxy.egg-info/dependency_links.txt
+MapProxy.egg-info/entry_points.txt
+MapProxy.egg-info/namespace_packages.txt
+MapProxy.egg-info/not-zip-safe
+MapProxy.egg-info/pbr.json
+MapProxy.egg-info/requires.txt
+MapProxy.egg-info/top_level.txt
+doc/GM.txt
+doc/Makefile
+doc/auth.rst
+doc/caches.rst
+doc/conf.py
+doc/configuration.rst
+doc/configuration_examples.rst
+doc/coverages.rst
+doc/decorate_img.rst
+doc/deployment.rst
+doc/development.rst
+doc/index.rst
+doc/inspire.rst
+doc/install.rst
+doc/install_osgeo4w.rst
+doc/install_windows.rst
+doc/labeling.rst
+doc/mapproxy_2.rst
+doc/mapproxy_util.rst
+doc/mapproxy_util_autoconfig.rst
+doc/seed.rst
+doc/services.rst
+doc/sources.rst
+doc/tutorial.rst
+doc/tutorial.yaml
+doc/_static/logo.png
+doc/_static/mapproxy.css
+doc/_templates/layout.html
+doc/_templates/navbar.html
+doc/_templates/toctree.html
+doc/imgs/bicubic.png
+doc/imgs/bilinear.png
+doc/imgs/labeling-dynamic.png
+doc/imgs/labeling-meta-buffer.png
+doc/imgs/labeling-metatiling-buffer.png
+doc/imgs/labeling-metatiling.png
+doc/imgs/labeling-no-clip.png
+doc/imgs/labeling-no-placement.png
+doc/imgs/labeling-partial-false.png
+doc/imgs/labeling-repeated.png
+doc/imgs/mapnik-webmerc-hq.png
+doc/imgs/mapnik-webmerc.png
+doc/imgs/mapproxy-demo.png
+doc/imgs/nearest.png
+doc/yaml/cache_conf.yaml
+doc/yaml/grid_conf.yaml
+doc/yaml/merged_conf.yaml
+doc/yaml/meta_conf.yaml
+doc/yaml/seed.yaml
+doc/yaml/simple_conf.yaml
+mapproxy/__init__.py
+mapproxy/exception.py
+mapproxy/featureinfo.py
+mapproxy/grid.py
+mapproxy/layer.py
+mapproxy/multiapp.py
+mapproxy/proj.py
+mapproxy/response.py
+mapproxy/srs.py
+mapproxy/template.py
+mapproxy/tilefilter.py
+mapproxy/version.py
+mapproxy/wsgiapp.py
+mapproxy/cache/__init__.py
+mapproxy/cache/base.py
+mapproxy/cache/couchdb.py
+mapproxy/cache/dummy.py
+mapproxy/cache/file.py
+mapproxy/cache/legend.py
+mapproxy/cache/mbtiles.py
+mapproxy/cache/meta.py
+mapproxy/cache/renderd.py
+mapproxy/cache/riak.py
+mapproxy/cache/sqlite.py
+mapproxy/cache/tile.py
+mapproxy/client/__init__.py
+mapproxy/client/cgi.py
+mapproxy/client/http.py
+mapproxy/client/log.py
+mapproxy/client/tile.py
+mapproxy/client/wms.py
+mapproxy/compat/__init__.py
+mapproxy/compat/image.py
+mapproxy/compat/itertools.py
+mapproxy/compat/modules.py
+mapproxy/config/__init__.py
+mapproxy/config/config.py
+mapproxy/config/coverage.py
+mapproxy/config/defaults.py
+mapproxy/config/loader.py
+mapproxy/config/spec.py
+mapproxy/config/validator.py
+mapproxy/config_template/__init__.py
+mapproxy/config_template/base_config/config.wsgi
+mapproxy/config_template/base_config/full_example.yaml
+mapproxy/config_template/base_config/full_seed_example.yaml
+mapproxy/config_template/base_config/log.ini
+mapproxy/config_template/base_config/mapproxy.yaml
+mapproxy/config_template/base_config/seed.yaml
+mapproxy/config_template/paster/etc/config.ini
+mapproxy/config_template/paster/etc/config.wsgi
+mapproxy/config_template/paster/etc/develop.ini
+mapproxy/config_template/paster/etc/log_deploy.ini
+mapproxy/config_template/paster/etc/mapproxy.yaml
+mapproxy/config_template/paster/etc/seed.yaml
+mapproxy/image/__init__.py
+mapproxy/image/mask.py
+mapproxy/image/merge.py
+mapproxy/image/message.py
+mapproxy/image/opts.py
+mapproxy/image/tile.py
+mapproxy/image/transform.py
+mapproxy/image/fonts/DejaVuSans.ttf
+mapproxy/image/fonts/DejaVuSansMono.ttf
+mapproxy/image/fonts/LICENSE
+mapproxy/image/fonts/__init__.py
+mapproxy/request/__init__.py
+mapproxy/request/base.py
+mapproxy/request/tile.py
+mapproxy/request/wmts.py
+mapproxy/request/wms/__init__.py
+mapproxy/request/wms/exception.py
+mapproxy/script/__init__.py
+mapproxy/script/export.py
+mapproxy/script/grids.py
+mapproxy/script/scales.py
+mapproxy/script/util.py
+mapproxy/script/wms_capabilities.py
+mapproxy/script/conf/__init__.py
+mapproxy/script/conf/app.py
+mapproxy/script/conf/caches.py
+mapproxy/script/conf/layers.py
+mapproxy/script/conf/seeds.py
+mapproxy/script/conf/sources.py
+mapproxy/script/conf/utils.py
+mapproxy/seed/__init__.py
+mapproxy/seed/cachelock.py
+mapproxy/seed/cleanup.py
+mapproxy/seed/config.py
+mapproxy/seed/script.py
+mapproxy/seed/seeder.py
+mapproxy/seed/spec.py
+mapproxy/seed/util.py
+mapproxy/service/__init__.py
+mapproxy/service/base.py
+mapproxy/service/demo.py
+mapproxy/service/kml.py
+mapproxy/service/ows.py
+mapproxy/service/template_helper.py
+mapproxy/service/tile.py
+mapproxy/service/wms.py
+mapproxy/service/wmts.py
+mapproxy/service/templates/tms_capabilities.xml
+mapproxy/service/templates/tms_exception.xml
+mapproxy/service/templates/tms_root_resource.xml
+mapproxy/service/templates/tms_tilemap_capabilities.xml
+mapproxy/service/templates/wms100capabilities.xml
+mapproxy/service/templates/wms100exception.xml
+mapproxy/service/templates/wms110capabilities.xml
+mapproxy/service/templates/wms110exception.xml
+mapproxy/service/templates/wms111capabilities.xml
+mapproxy/service/templates/wms111exception.xml
+mapproxy/service/templates/wms130capabilities.xml
+mapproxy/service/templates/wms130exception.xml
+mapproxy/service/templates/wmts100capabilities.xml
+mapproxy/service/templates/wmts100exception.xml
+mapproxy/service/templates/demo/capabilities_demo.html
+mapproxy/service/templates/demo/demo.html
+mapproxy/service/templates/demo/openlayers-demo.cfg
+mapproxy/service/templates/demo/static.html
+mapproxy/service/templates/demo/tms_demo.html
+mapproxy/service/templates/demo/wms_demo.html
+mapproxy/service/templates/demo/wmts_demo.html
+mapproxy/service/templates/demo/static/OpenLayers.js
+mapproxy/service/templates/demo/static/logo.png
+mapproxy/service/templates/demo/static/site.css
+mapproxy/service/templates/demo/static/img/blank.gif
+mapproxy/service/templates/demo/static/img/east-mini.png
+mapproxy/service/templates/demo/static/img/north-mini.png
+mapproxy/service/templates/demo/static/img/south-mini.png
+mapproxy/service/templates/demo/static/img/west-mini.png
+mapproxy/service/templates/demo/static/img/zoom-minus-mini.png
+mapproxy/service/templates/demo/static/img/zoom-plus-mini.png
+mapproxy/service/templates/demo/static/img/zoom-world-mini.png
+mapproxy/service/templates/demo/static/theme/default/framedCloud.css
+mapproxy/service/templates/demo/static/theme/default/google.css
+mapproxy/service/templates/demo/static/theme/default/ie6-style.css
+mapproxy/service/templates/demo/static/theme/default/style.css
+mapproxy/source/__init__.py
+mapproxy/source/error.py
+mapproxy/source/mapnik.py
+mapproxy/source/tile.py
+mapproxy/source/wms.py
+mapproxy/test/__init__.py
+mapproxy/test/helper.py
+mapproxy/test/http.py
+mapproxy/test/image.py
+mapproxy/test/mocker.py
+mapproxy/test/test_http_helper.py
+mapproxy/test/schemas/xml.xsd
+mapproxy/test/schemas/inspire/common/1.0/common.xsd
+mapproxy/test/schemas/inspire/common/1.0/network.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_bul.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_cze.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_dan.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_dut.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_eng.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_est.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_fin.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_fre.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_ger.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_gle.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_gre.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_hun.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_ita.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_lav.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_lit.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_mlt.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_pol.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_por.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_rum.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_slo.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_slv.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_spa.xsd
+mapproxy/test/schemas/inspire/common/1.0/enums/enum_swe.xsd
+mapproxy/test/schemas/inspire/common/1.0/examples/inspireresourcemddataset.xml
+mapproxy/test/schemas/inspire/common/1.0/examples/inspireresourcemdseries.xml
+mapproxy/test/schemas/inspire/common/1.0/examples/inspireresourcemdservice.xml
+mapproxy/test/schemas/inspire/inspire_vs/1.0/inspire_vs.xsd
+mapproxy/test/schemas/inspire/inspire_vs/1.0/examples/WMS_Image2000GetCapabilities_InspireSchema.xml
+mapproxy/test/schemas/inspire/inspire_vs/1.0/examples/wms_at.xml
+mapproxy/test/schemas/inspire/inspire_vs/1.0/examples/wms_geoimage.xml
+mapproxy/test/schemas/kml/2.2.0/ReadMe.txt
+mapproxy/test/schemas/kml/2.2.0/atom-author-link.xsd
+mapproxy/test/schemas/kml/2.2.0/ogckml22.xsd
+mapproxy/test/schemas/kml/2.2.0/xAL.xsd
+mapproxy/test/schemas/ows/1.1.0/ReadMe.txt
+mapproxy/test/schemas/ows/1.1.0/ows19115subset.xsd
+mapproxy/test/schemas/ows/1.1.0/owsAll.xsd
+mapproxy/test/schemas/ows/1.1.0/owsCommon.xsd
+mapproxy/test/schemas/ows/1.1.0/owsContents.xsd
+mapproxy/test/schemas/ows/1.1.0/owsDataIdentification.xsd
+mapproxy/test/schemas/ows/1.1.0/owsDomainType.xsd
+mapproxy/test/schemas/ows/1.1.0/owsExceptionReport.xsd
+mapproxy/test/schemas/ows/1.1.0/owsGetCapabilities.xsd
+mapproxy/test/schemas/ows/1.1.0/owsGetResourceByID.xsd
+mapproxy/test/schemas/ows/1.1.0/owsInputOutputData.xsd
+mapproxy/test/schemas/ows/1.1.0/owsManifest.xsd
+mapproxy/test/schemas/ows/1.1.0/owsOperationsMetadata.xsd
+mapproxy/test/schemas/ows/1.1.0/owsServiceIdentification.xsd
+mapproxy/test/schemas/ows/1.1.0/owsServiceProvider.xsd
+mapproxy/test/schemas/sld/1.1.0/sld_capabilities.xsd
+mapproxy/test/schemas/wms/1.0.0/capabilities_1_0_0.dtd
+mapproxy/test/schemas/wms/1.0.0/capabilities_1_0_0.xml
+mapproxy/test/schemas/wms/1.0.7/capabilities_1_0_7.dtd
+mapproxy/test/schemas/wms/1.0.7/capabilities_1_0_7.xml
+mapproxy/test/schemas/wms/1.1.0/capabilities_1_1_0.dtd
+mapproxy/test/schemas/wms/1.1.0/capabilities_1_1_0.xml
+mapproxy/test/schemas/wms/1.1.0/exception_1_1_0.dtd
+mapproxy/test/schemas/wms/1.1.0/exception_1_1_0.xml
+mapproxy/test/schemas/wms/1.1.1/OGC-exception.xsd
+mapproxy/test/schemas/wms/1.1.1/WMS_DescribeLayerResponse.dtd
+mapproxy/test/schemas/wms/1.1.1/WMS_MS_Capabilities.dtd
+mapproxy/test/schemas/wms/1.1.1/WMS_exception_1_1_1.dtd
+mapproxy/test/schemas/wms/1.1.1/capabilities_1_1_1.dtd
+mapproxy/test/schemas/wms/1.1.1/capabilities_1_1_1.xml
+mapproxy/test/schemas/wms/1.1.1/exception_1_1_1.dtd
+mapproxy/test/schemas/wms/1.1.1/exception_1_1_1.xml
+mapproxy/test/schemas/wms/1.3.0/ReadMe.txt
+mapproxy/test/schemas/wms/1.3.0/capabilities_1_3_0.xml
+mapproxy/test/schemas/wms/1.3.0/capabilities_1_3_0.xsd
+mapproxy/test/schemas/wms/1.3.0/exceptions_1_3_0.xml
+mapproxy/test/schemas/wms/1.3.0/exceptions_1_3_0.xsd
+mapproxy/test/schemas/wmsc/1.1.1/OGC-exception.xsd
+mapproxy/test/schemas/wmsc/1.1.1/WMS_DescribeLayerResponse.dtd
+mapproxy/test/schemas/wmsc/1.1.1/WMS_MS_Capabilities.dtd
+mapproxy/test/schemas/wmsc/1.1.1/WMS_exception_1_1_1.dtd
+mapproxy/test/schemas/wmsc/1.1.1/capabilities_1_1_1.dtd
+mapproxy/test/schemas/wmsc/1.1.1/capabilities_1_1_1.xml
+mapproxy/test/schemas/wmsc/1.1.1/exception_1_1_1.dtd
+mapproxy/test/schemas/wmsc/1.1.1/exception_1_1_1.xml
+mapproxy/test/schemas/wmts/1.0/ReadMe.txt
+mapproxy/test/schemas/wmts/1.0/wmts.xsd
+mapproxy/test/schemas/wmts/1.0/wmtsAbstract.wsdl
+mapproxy/test/schemas/wmts/1.0/wmtsGetCapabilities_request.xsd
+mapproxy/test/schemas/wmts/1.0/wmtsGetCapabilities_response.xsd
+mapproxy/test/schemas/wmts/1.0/wmtsGetFeatureInfo_request.xsd
+mapproxy/test/schemas/wmts/1.0/wmtsGetFeatureInfo_response.xsd
+mapproxy/test/schemas/wmts/1.0/wmtsGetTile_request.xsd
+mapproxy/test/schemas/wmts/1.0/wmtsKVP.xsd
+mapproxy/test/schemas/wmts/1.0/wmtsPayload_response.xsd
+mapproxy/test/schemas/xlink/1.0.0/ReadMe.txt
+mapproxy/test/schemas/xlink/1.0.0/xlinks.xsd
+mapproxy/test/system/__init__.py
+mapproxy/test/system/test_auth.py
+mapproxy/test/system/test_behind_proxy.py
+mapproxy/test/system/test_cache_grid_names.py
+mapproxy/test/system/test_cache_mbtiles.py
+mapproxy/test/system/test_cache_source.py
+mapproxy/test/system/test_combined_sources.py
+mapproxy/test/system/test_coverage.py
+mapproxy/test/system/test_decorate_img.py
+mapproxy/test/system/test_disable_storage.py
+mapproxy/test/system/test_formats.py
+mapproxy/test/system/test_inspire_vs.py
+mapproxy/test/system/test_kml.py
+mapproxy/test/system/test_layergroups.py
+mapproxy/test/system/test_legendgraphic.py
+mapproxy/test/system/test_mapnik.py
+mapproxy/test/system/test_mapserver.py
+mapproxy/test/system/test_mixed_mode_format.py
+mapproxy/test/system/test_multiapp.py
+mapproxy/test/system/test_renderd_client.py
+mapproxy/test/system/test_scalehints.py
+mapproxy/test/system/test_seed.py
+mapproxy/test/system/test_seed_only.py
+mapproxy/test/system/test_sld.py
+mapproxy/test/system/test_source_errors.py
+mapproxy/test/system/test_tilesource_minmax_res.py
+mapproxy/test/system/test_tms.py
+mapproxy/test/system/test_tms_origin.py
+mapproxy/test/system/test_util_conf.py
+mapproxy/test/system/test_util_export.py
+mapproxy/test/system/test_util_grids.py
+mapproxy/test/system/test_util_wms_capabilities.py
+mapproxy/test/system/test_watermark.py
+mapproxy/test/system/test_wms.py
+mapproxy/test/system/test_wms_srs_extent.py
+mapproxy/test/system/test_wms_version.py
+mapproxy/test/system/test_wmsc.py
+mapproxy/test/system/test_wmts.py
+mapproxy/test/system/test_wmts_dimensions.py
+mapproxy/test/system/test_wmts_restful.py
+mapproxy/test/system/test_xslt_featureinfo.py
+mapproxy/test/system/fixture/auth.yaml
+mapproxy/test/system/fixture/cache.mbtiles
+mapproxy/test/system/fixture/cache_grid_names.yaml
+mapproxy/test/system/fixture/cache_mbtiles.yaml
+mapproxy/test/system/fixture/cache_source.yaml
+mapproxy/test/system/fixture/cgi.py
+mapproxy/test/system/fixture/combined_sources.yaml
+mapproxy/test/system/fixture/coverage.yaml
+mapproxy/test/system/fixture/disable_storage.yaml
+mapproxy/test/system/fixture/empty_ogrdata.geojson
+mapproxy/test/system/fixture/formats.yaml
+mapproxy/test/system/fixture/inspire.yaml
+mapproxy/test/system/fixture/inspire_full.yaml
+mapproxy/test/system/fixture/kml_layer.yaml
+mapproxy/test/system/fixture/layer.yaml
+mapproxy/test/system/fixture/layergroups.yaml
+mapproxy/test/system/fixture/layergroups_root.yaml
+mapproxy/test/system/fixture/legendgraphic.yaml
+mapproxy/test/system/fixture/mapnik_source.yaml
+mapproxy/test/system/fixture/mapproxy_export.yaml
+mapproxy/test/system/fixture/mapserver.yaml
+mapproxy/test/system/fixture/mixed_mode.yaml
+mapproxy/test/system/fixture/multiapp1.yaml
+mapproxy/test/system/fixture/multiapp2.yaml
+mapproxy/test/system/fixture/renderd_client.yaml
+mapproxy/test/system/fixture/scalehints.yaml
+mapproxy/test/system/fixture/seed.yaml
+mapproxy/test/system/fixture/seed_mapproxy.yaml
+mapproxy/test/system/fixture/seed_old.yaml
+mapproxy/test/system/fixture/seed_timeouts.yaml
+mapproxy/test/system/fixture/seed_timeouts_mapproxy.yaml
+mapproxy/test/system/fixture/seedonly.yaml
+mapproxy/test/system/fixture/sld.yaml
+mapproxy/test/system/fixture/source_errors.yaml
+mapproxy/test/system/fixture/source_errors_raise.yaml
+mapproxy/test/system/fixture/tileservice_origin.yaml
+mapproxy/test/system/fixture/tilesource_minmax_res.yaml
+mapproxy/test/system/fixture/util-conf-base-grids.yaml
+mapproxy/test/system/fixture/util-conf-overwrite.yaml
+mapproxy/test/system/fixture/util-conf-wms-111-cap.xml
+mapproxy/test/system/fixture/util_grids.yaml
+mapproxy/test/system/fixture/util_wms_capabilities111.xml
+mapproxy/test/system/fixture/util_wms_capabilities130.xml
+mapproxy/test/system/fixture/util_wms_capabilities_service_exception.xml
+mapproxy/test/system/fixture/watermark.yaml
+mapproxy/test/system/fixture/wms_srs_extent.yaml
+mapproxy/test/system/fixture/wms_versions.yaml
+mapproxy/test/system/fixture/wmts.yaml
+mapproxy/test/system/fixture/wmts_dimensions.yaml
+mapproxy/test/system/fixture/xslt_featureinfo.yaml
+mapproxy/test/system/fixture/cache_data/wms_cache_EPSG900913/01/000/000/000/000/000/001.jpeg
+mapproxy/test/system/fixture/cache_data/wms_cache_transparent_EPSG900913/01/000/000/000/000/000/001.png
+mapproxy/test/unit/__init__.py
+mapproxy/test/unit/epsg
+mapproxy/test/unit/test_async.py
+mapproxy/test/unit/test_auth.py
+mapproxy/test/unit/test_cache.py
+mapproxy/test/unit/test_cache_couchdb.py
+mapproxy/test/unit/test_cache_riak.py
+mapproxy/test/unit/test_cache_tile.py
+mapproxy/test/unit/test_client.py
+mapproxy/test/unit/test_client_cgi.py
+mapproxy/test/unit/test_collections.py
+mapproxy/test/unit/test_concat_legends.py
+mapproxy/test/unit/test_conf_loader.py
+mapproxy/test/unit/test_conf_validator.py
+mapproxy/test/unit/test_config.py
+mapproxy/test/unit/test_decorate_img.py
+mapproxy/test/unit/test_exceptions.py
+mapproxy/test/unit/test_featureinfo.py
+mapproxy/test/unit/test_file_lock_load.py
+mapproxy/test/unit/test_geom.py
+mapproxy/test/unit/test_grid.py
+mapproxy/test/unit/test_image.py
+mapproxy/test/unit/test_image_mask.py
+mapproxy/test/unit/test_image_messages.py
+mapproxy/test/unit/test_image_options.py
+mapproxy/test/unit/test_multiapp.py
+mapproxy/test/unit/test_ogr_reader.py
+mapproxy/test/unit/test_request.py
+mapproxy/test/unit/test_request_wmts.py
+mapproxy/test/unit/test_response.py
+mapproxy/test/unit/test_seed.py
+mapproxy/test/unit/test_seed_cachelock.py
+mapproxy/test/unit/test_srs.py
+mapproxy/test/unit/test_tiled_source.py
+mapproxy/test/unit/test_tilefilter.py
+mapproxy/test/unit/test_times.py
+mapproxy/test/unit/test_timeutils.py
+mapproxy/test/unit/test_util_conf_utils.py
+mapproxy/test/unit/test_utils.py
+mapproxy/test/unit/test_wms_capabilities.py
+mapproxy/test/unit/test_wms_layer.py
+mapproxy/test/unit/test_yaml.py
+mapproxy/test/unit/polygons/polygons.dbf
+mapproxy/test/unit/polygons/polygons.shp
+mapproxy/test/unit/polygons/polygons.shx
+mapproxy/util/__init__.py
+mapproxy/util/async.py
+mapproxy/util/collections.py
+mapproxy/util/coverage.py
+mapproxy/util/fs.py
+mapproxy/util/geom.py
+mapproxy/util/lib.py
+mapproxy/util/lock.py
+mapproxy/util/ogr.py
+mapproxy/util/py.py
+mapproxy/util/times.py
+mapproxy/util/wsgi.py
+mapproxy/util/yaml.py
+mapproxy/util/ext/__init__.py
+mapproxy/util/ext/local.py
+mapproxy/util/ext/lockfile.py
+mapproxy/util/ext/odict.py
+mapproxy/util/ext/serving.py
+mapproxy/util/ext/dictspec/__init__.py
+mapproxy/util/ext/dictspec/spec.py
+mapproxy/util/ext/dictspec/validator.py
+mapproxy/util/ext/dictspec/test/__init__.py
+mapproxy/util/ext/dictspec/test/test_validator.py
+mapproxy/util/ext/tempita/__init__.py
+mapproxy/util/ext/tempita/_looper.py
+mapproxy/util/ext/tempita/compat3.py
+mapproxy/util/ext/wmsparse/__init__.py
+mapproxy/util/ext/wmsparse/parse.py
+mapproxy/util/ext/wmsparse/util.py
+mapproxy/util/ext/wmsparse/test/__init__.py
+mapproxy/util/ext/wmsparse/test/test_parse.py
+mapproxy/util/ext/wmsparse/test/test_util.py
+mapproxy/util/ext/wmsparse/test/wms-large-111.xml
+mapproxy/util/ext/wmsparse/test/wms-omniscale-111.xml
+mapproxy/util/ext/wmsparse/test/wms-omniscale-130.xml
+mapproxy/util/ext/wmsparse/test/wms_nasa_cap.xml
\ No newline at end of file
diff --git a/MapProxy.egg-info/dependency_links.txt b/MapProxy.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/MapProxy.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/MapProxy.egg-info/entry_points.txt b/MapProxy.egg-info/entry_points.txt
new file mode 100644
index 0000000..a5ae7b6
--- /dev/null
+++ b/MapProxy.egg-info/entry_points.txt
@@ -0,0 +1,14 @@
+[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.filter_factory]
+lighttpd_root_fix = mapproxy.util.wsgi:lighttpd_root_fix_filter_factory
+
+[paste.paster_create_template]
+mapproxy_conf = mapproxy.config_template:PasterConfigurationTemplate
+
diff --git a/MapProxy.egg-info/namespace_packages.txt b/MapProxy.egg-info/namespace_packages.txt
new file mode 100644
index 0000000..3fb1968
--- /dev/null
+++ b/MapProxy.egg-info/namespace_packages.txt
@@ -0,0 +1 @@
+mapproxy
diff --git a/MapProxy.egg-info/not-zip-safe b/MapProxy.egg-info/not-zip-safe
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/MapProxy.egg-info/not-zip-safe
@@ -0,0 +1 @@
+
diff --git a/MapProxy.egg-info/pbr.json b/MapProxy.egg-info/pbr.json
new file mode 100644
index 0000000..6759183
--- /dev/null
+++ b/MapProxy.egg-info/pbr.json
@@ -0,0 +1 @@
+{"is_release": false, "git_version": "26a945b"}
\ No newline at end of file
diff --git a/MapProxy.egg-info/requires.txt b/MapProxy.egg-info/requires.txt
new file mode 100644
index 0000000..d85ba0e
--- /dev/null
+++ b/MapProxy.egg-info/requires.txt
@@ -0,0 +1,2 @@
+PyYAML>=3.0,<3.99
+Pillow !=2.4.0
diff --git a/MapProxy.egg-info/top_level.txt b/MapProxy.egg-info/top_level.txt
new file mode 100644
index 0000000..3fb1968
--- /dev/null
+++ b/MapProxy.egg-info/top_level.txt
@@ -0,0 +1 @@
+mapproxy
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..a32f6c4
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,227 @@
+Metadata-Version: 1.1
+Name: MapProxy
+Version: 1.8.2
+Summary: An accelerating proxy for web map services
+Home-page: http://mapproxy.org
+Author: Oliver Tonnhofer
+Author-email: olt at omniscale.de
+License: Apache Software License 2.0
+Description: 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.
+        
+        .. image:: http://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 <http://omniscale.com>`_, 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 http://mapproxy.org/ for more information.
+        
+        The documentation is available at: http://mapproxy.org/docs/latest/
+        
+        Changes
+        -------
+        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
+        
+        
+        Older changes
+        -------------
+        See https://raw.github.com/mapproxy/mapproxy/master/CHANGES.txt
+        
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Topic :: Internet :: Proxy Servers
+Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
+Classifier: Topic :: Scientific/Engineering :: GIS
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..08f6f0f
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,12 @@
+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.
+
+.. image:: http://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 <http://omniscale.com>`_, 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 http://mapproxy.org/ for more information.
+
+The documentation is available at: http://mapproxy.org/docs/latest/
+
diff --git a/doc/GM.txt b/doc/GM.txt
new file mode 100644
index 0000000..5a19899
--- /dev/null
+++ b/doc/GM.txt
@@ -0,0 +1,23 @@
+POLYGON ((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.151279 [...]
+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.403720297 [...]
+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,154656 [...]
+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,14914 [...]
+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,14 [...]
+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.152808 [...]
+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.37458773 [...]
+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.87005 [...]
+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.948686 [...]
+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.378118371 [...]
+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.4686408 [...]
+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.6567517 [...]
+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,126666 [...]
+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.402856360 [...]
+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,129 [...]
+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,122 [...]
+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.36452426 [...]
+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.6410429 [...]
+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.687441 [...]
+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.350382 [...]
diff --git a/doc/Makefile b/doc/Makefile
new file mode 100644
index 0000000..93afe21
--- /dev/null
+++ b/doc/Makefile
@@ -0,0 +1,89 @@
+# 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 <target>' where <target> 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 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."
+
+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."
diff --git a/doc/_static/logo.png b/doc/_static/logo.png
new file mode 100644
index 0000000..0883226
Binary files /dev/null and b/doc/_static/logo.png differ
diff --git a/doc/_static/mapproxy.css b/doc/_static/mapproxy.css
new file mode 100644
index 0000000..21cbedc
--- /dev/null
+++ b/doc/_static/mapproxy.css
@@ -0,0 +1,68 @@
+body {
+  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-brand {
+  padding-top: 0px;
+}
+
+.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;
+}
diff --git a/doc/_templates/layout.html b/doc/_templates/layout.html
new file mode 100644
index 0000000..bd76e01
--- /dev/null
+++ b/doc/_templates/layout.html
@@ -0,0 +1,16 @@
+{% extends "!layout.html" %}
+
+{% set bootswatch_css_custom = ['_static/mapproxy.css'] %}
+
+{%- block content %}
+{{ navBar() }}
+<div class="container">
+  <div class="row">
+    {%- block sidebar1 %}{{ bsidebar() }}{% endblock %}
+    <div class="col-md-8">
+      {% block body %}{% endblock %}
+    </div>
+  </div>
+</div>
+{%- endblock %}
+
diff --git a/doc/_templates/navbar.html b/doc/_templates/navbar.html
new file mode 100644
index 0000000..8ad5e2b
--- /dev/null
+++ b/doc/_templates/navbar.html
@@ -0,0 +1,24 @@
+<div id="navbar" class="{{ theme_navbar_class }} navbar-default {% if theme_navbar_fixed_top == 'true' -%} navbar-fixed-top{%- endif -%}">
+  <div class="container">
+    <div class="navbar-header">
+      <!-- .btn-navbar is used as the toggle for collapsed navbar content -->
+      <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".nav-collapse">
+        <span class="icon-bar"></span>
+        <span class="icon-bar"></span>
+        <span class="icon-bar"></span>
+      </button>
+      <a class="navbar-brand" href="{{ pathto(master_doc) }}">
+        {%- block sidebarlogo %}
+        {%- if logo %}<img src="{{ pathto('_static/' + logo, 1) }}" height="50">{%- endif %}
+        {%- endblock %}
+        {% if theme_navbar_title -%}{{ theme_navbar_title|e }}{%- else -%}{{ project|e }}{%- endif -%}
+        {{ release|e }}
+      </a>
+    </div>
+    <div class="collapse navbar-collapse nav-collapse">
+      {% block navbarsearch %}
+        {% include "navbarsearchbox.html" %}
+      {% endblock %}
+    </div>
+  </div>
+</div>
diff --git a/doc/_templates/toctree.html b/doc/_templates/toctree.html
new file mode 100755
index 0000000..53cce44
--- /dev/null
+++ b/doc/_templates/toctree.html
@@ -0,0 +1 @@
+{{ toctree() }}
\ No newline at end of file
diff --git a/doc/auth.rst b/doc/auth.rst
new file mode 100644
index 0000000..50acc4e
--- /dev/null
+++ b/doc/auth.rst
@@ -0,0 +1,531 @@
+Authentication 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 <http://wsgi.org>`_ 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_reponse):
+          if random.randint(0, 1) == 1:
+            return self.app(environ, start_reponse)
+          else:
+            start_reponse('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_reponse):
+          # put authorize callback function into environment
+          environ['mapproxy.authorize'] = self.authorize
+          return self.app(environ, start_reponse)
+
+      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 <http://toblerity.github.com/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::
+
+  <Layer queryable="1">
+    <Name>layer1</Name>
+    <Layer queryable="1"><Name>layer1a</Name></Layer>
+    <Layer><Name>layer1b</Name></Layer>
+  </Layer>
+
+
+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 <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_reponse):
+          environ['mapproxy.authorize'] = self.authorize
+          return self.app(environ, start_reponse)
+
+      def authorize(self, service, layers=[]):
+          instance_name = environ.get('mapproxy.instance_name', '')
+          if instance_name.startswith('secure'):
+              return {'authorized': 'none'}
+          else:
+              return {'authorized': 'full'}
+
+
diff --git a/doc/caches.rst b/doc/caches.rst
new file mode 100644
index 0000000..7519844
--- /dev/null
+++ b/doc/caches.rst
@@ -0,0 +1,290 @@
+Caches
+######
+
+.. 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.
+
+``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``). ``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);
+
+  .. note::
+    ``tms`` and ``quadkey`` 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
+
+
+``mbtiles``
+===========
+
+Use a single SQLite file for this cache. It uses the `MBTile specification <http://mbtiles.org/>`_.
+
+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.
+
+``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 databse. 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 direcotry 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
+
+
+``couchdb``
+===========
+
+.. versionadded:: 1.3.0
+
+Store tiles inside a `CouchDB <http://couchdb.apache.org/>`_. 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 <http://python-requests.org/>`_. 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``.
+
+
+``riak``
+========
+
+.. versionadded:: 1.6.0
+
+Store tiles in a `Riak <http://basho.com/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 <https://pypi.python.org/pypi/riak>`_ version 2.0 or newer. You can install it in the usual way, for example with ``pip install riak``. Environments with older version must be upgraded with ``pip install -U riak``.
+
+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. A single localhost node is used if you don't configure any nodes.
+
+``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 needs to 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]
+        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
diff --git a/doc/conf.py b/doc/conf.py
new file mode 100644
index 0000000..cc01455
--- /dev/null
+++ b/doc/conf.py
@@ -0,0 +1,204 @@
+# -*- 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.8'
+# The full version, including alpha/beta/rc tags.
+release = '1.8.2'
+
+# 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
+# "<project> v<release> 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 <link> 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
diff --git a/doc/configuration.rst b/doc/configuration.rst
new file mode 100644
index 0000000..cc79bbd
--- /dev/null
+++ b/doc/configuration.rst
@@ -0,0 +1,1055 @@
+Configuration
+#############
+
+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 <seed>` for more information.
+
+.. index:: mapproxy.yaml
+
+mapproxy.yaml
+-------------
+
+The configuration uses the YAML format. The Wikipedia contains a `good introduction to YAML <http://en.wikipedia.org/wiki/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: [mysoruce]
+
+should become
+
+::
+
+  layers:
+    - name: mylayer
+      title: My Layer
+      source: [mysoruce]
+
+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 <layers_name>`) 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`.
+
+``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 <scale_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.
+
+WMS and Mapserver sources also support tagged names (``wms:lyr1,lyr2``). See :ref:`tagged_source_names`.
+
+Cache souces
+^^^^^^^^^^^^
+.. 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 <meta_size>` for more details.
+
+``image``
+"""""""""
+
+:ref:`See below <image_options>` 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 <wms_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<cache_file_directory>` 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.
+::
+
+  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 <image_options>` for all image format options.
+
+
+.. _globals_cache:
+
+``cache``
+"""""""""
+
+.. versionadded:: 1.6.0 ``tile_lock_dir``
+
+
+.. _meta_size:
+
+``meta_size``
+  MapProxy does not make a single request for every tile but 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 WMS.
+
+``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 WMS. This limit is per request 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 HTTPS Connections (HTTPS)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. note:: You need Python 2.6 or the `SSL module <http://pypi.python.org/pypi/ssl>`_ for this feature.
+
+MapProxy supports access to HTTPS servers. Just use ``https`` instead of ``http`` when
+defining the URL of a source. MapProxy needs a file that contains the root and CA
+certificates. If the server certificate is signed by a "standard" root certificate (i.e. your browser does not warn you), then you can use a cert file that is distributed with your system. On Debian based systems you can use ``/etc/ssl/certs/ca-certificates.crt``.
+See the `Python SSL documentation <http://docs.python.org/dev/library/ssl.html#ssl-certificates>`_ for more information about the format.
+
+::
+
+  http:
+    ssl_ca_certs: /etc/ssl/certs/ca-certificates.crt
+
+If you want to use SSL 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, see :ref:`WMS source options <wms_source-ssl_no_cert_checks>`.
+::
+
+  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 <http://en.wikipedia.org/wiki/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<mapserver_label>`.
+
+``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 <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 <http://mapproxy.org/blog/improving-the-performance-for-png-requests/>`_ that is only available in Pillow >=2.0. See :ref:`installation of PIL<dependencies_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_util_scales>`.
+
+
+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.
diff --git a/doc/configuration_examples.rst b/doc/configuration_examples.rst
new file mode 100644
index 0000000..3ed2d32
--- /dev/null
+++ b/doc/configuration_examples.rst
@@ -0,0 +1,831 @@
+.. _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 <tiles_label>` 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 <grids>`.
+
+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 <http://wiki.openstreetmap.org/wiki/Tile_usage_policy>`_.
+
+.. _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.::
+
+  <script src="http://openlayers.org/api/OpenLayers.js"></script>
+  <script type="text/javascript">
+    var map;
+    function init(){
+        map = new OpenLayers.Map('map', {
+            projection: new OpenLayers.Projection("EPSG:900913")
+        });
+
+        var base_layer = new OpenLayers.Layer.OSM();
+
+        var overlay_layer = new OpenLayers.Layer.TMS(
+            'TMS street_layer',
+            'http://127.0.0.1:8080/tiles/',
+            {layername: 'street_layer_EPSG900913',
+             type: 'png', isBaseLayer: false}
+        );
+
+        map.addLayer(base_layer);
+        map.addLayer(overlay_layer);
+        map.zoomToMaxExtent();
+    };
+  </script>
+
+Note that we used the ``/tiles`` service instead of ``/tms`` here. See :ref:`the tile service documentation <open_layers_label>` 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<osm_tile_conf>` 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<google_maps_label>` 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: [new_grid]
+        sources: [existing_cache]
+
+      existing_cache:
+        grids: [old_grid]
+        sources: [my_source]
+
+    grids:
+      utm32n:
+        srs: 'EPSG:25832'
+        bbox: [4, 46, 16, 56]
+        bbox_srs: 'EPSG:4326'
+        origin: 'nw'
+        min_res: 5700
+
+      osm_grid:
+        base: GLOBAL_MERCATOR
+        origin: nw
+
+
+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: [osm_grid]
+        disable_storage: true
+        sources: [osm_source]
+
+    sources:
+      osm_source:
+        type: tile
+        grid: osm_grid
+        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
+
+      osm_grid:
+        base: GLOBAL_MERCATOR
+        origin: nw
+
+
+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 <jpeg_quality>`.
+
+::
+
+  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 <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 [...]
+
+
+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:
+          <sld:StyledLayerDescriptor version="1.0.0"
+          [snip]
+          </sld:StyledLayerDescriptor>
+
+
+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 <lxml_install>`.
+
+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 <lxml_install>` 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.
+::
+
+  <p>FI1</p>
+
+and
+::
+
+  <p>FI2</p>
+
+will result in::
+
+  <html>
+    <body>
+      <p>FI1</p>
+      <p>FI2</p>
+   </body>
+  </html>
+
+
+XML
+~~~
+
+Multiple XML documents are put in the root of the first document.
+
+::
+
+  <root>
+    <a>FI1</a>
+  </root>
+
+and
+::
+
+  <other_root>
+    <b>FI2</b>
+  </other_root>
+
+will result in::
+
+  <root>
+    <a>FI1</a>
+    <b>FI2</b>
+  </root>
+
+
+XSL Transformations
+-------------------
+
+MapProxy supports XSL transformations for more control over feature information. This also requires :ref:`lxml <lxml_install>`. 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 <http_ssl>`.
+::
+
+  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 <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: [osm_grid]
+      sources: [osm_wms]
+      cache:
+        type: file
+        directory_layout: quadkey
+
+  grids:
+    osm_grid:
+      base: GLOBAL_MERCATOR
+      origin: nw
+
+
+.. _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
diff --git a/doc/coverages.rst b/doc/coverages.rst
new file mode 100644
index 0000000..3cf124b
--- /dev/null
+++ b/doc/coverages.rst
@@ -0,0 +1,133 @@
+.. _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 <sources>` and in the :doc:`mapproxy-seed tool <seed>`. Refer to the corresponding section in the documentation.
+
+
+There are three different ways to describe a coverage.
+
+- a simple rectangular bounding box,
+- a text file with one or more (multi)polygons in WKT format,
+- (multi)polygons from any data source readable with OGR (e.g. Shapefile, GeoJSON, PostGIS)
+
+
+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, 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 <http://mapproxy.org/static/polygons/>`_. Read `the index <http://mapproxy.org/static/polygons/0-fips-codes.txt>`_ to find your country.
+
+``datasource``:
+ The path to the polygon 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
+  <http://www.gdal.org/ogr/ogr_formats.html>`_ 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 <http://www.gdal.org/ogr/ogr_sql.html>`_. If this
+  option is unset, the first layer from the datasource will be used.
+
+
+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'
+
+
+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 <http://www.gdal.org/ogr/ogr_formats.html>`_ for all supported formats.
diff --git a/doc/decorate_img.rst b/doc/decorate_img.rst
new file mode 100644
index 0000000..3ab87b2
--- /dev/null
+++ b/doc/decorate_img.rst
@@ -0,0 +1,106 @@
+Decorate 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 <http://wsgi.org>`_ 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 <auth>` 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.
+
diff --git a/doc/deployment.rst b/doc/deployment.rst
new file mode 100644
index 0000000..c366229
--- /dev/null
+++ b/doc/deployment.rst
@@ -0,0 +1,341 @@
+Deployment
+==========
+
+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_production>`.
+
+
+.. _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 <server_script>`.
+
+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
+
+  <Directory /path/to/mapproxy/>
+    Order deny,allow
+    Allow from all
+  </Directory>
+
+
+``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 <http://code.google.com/p/modwsgi/wiki/ConfigurationDirectives>`_. 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 <https://code.google.com/p/modwsgi/wiki/VirtualEnvironments>`_ 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}
+
+  <Directory /path/to/mapproxy/>
+    Order deny,allow
+    Require all granted  # for Apache 2.4
+    # Allow from all     # for Apache 2.2
+  </Directory>
+
+
+.. _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 <server_script>`). 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
+
+
+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 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/``
+
+::
+
+  <IfModule mod_proxy.c>
+    <IfModule mod_headers.c>
+          <Location /mapproxy>
+                  ProxyPass http://localhost:8181
+                  ProxyPassReverse  http://localhost:8181
+                  RequestHeader add X-Script-Name "/mapproxy"
+          </Location>
+    </IfModule>
+  </IfModule>
+
+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 <http://mapproxy.org/docs/1.5.0/deployment.html>`_.
+
+
+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 [...]
+
+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<lock_dir>` and :ref:`globals.cache.tile_lock_dir<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 <http://docs.python.org/howto/logging.html#configuring-logging>`_.
+
+
+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.
+
+
+Enabling logging
+~~~~~~~~~~~~~~~~
+
+The :ref:`test server <deployment_testing>` 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 <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)
+
diff --git a/doc/development.rst b/doc/development.rst
new file mode 100644
index 0000000..e6bf07f
--- /dev/null
+++ b/doc/development.rst
@@ -0,0 +1,104 @@
+Development
+===========
+
+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 <http://pypi.python.org/pypi/MapProxy>`_. There is also `an archive of all releases <http://pypi.python.org/packages/source/M/MapProxy/>`_.
+
+MapProxy uses `Git`_ as a source control management tool. If you are new to distributed SCMs or Git we recommend to read `Pro Git <http://git-scm.com/book>`_.
+
+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 <https://github.com/mapproxy/mapproxy/issues>`_ to manage all bug reports, enhancements and new feature requests for MapProxy. Go ahead and `create new tickets <https://github.com/mapproxy/mapproxy/issues/new>`_. 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 <http://docs.python.org/library/doctest.html>`_ 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 at lists.osgeo.org You must `subscribe <http://lists.osgeo.org/mailman/listinfo/mapproxy>`_ to the list before you can write. The archive is `available here <http://lists.osgeo.org/pipermail/mapproxy/>`_.
+
+IRC
+"""
+There is also a channel on `Freenode <http://freenode.net/>`_: ``#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 sou [...]
+
+.. 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/
\ No newline at end of file
diff --git a/doc/imgs/bicubic.png b/doc/imgs/bicubic.png
new file mode 100644
index 0000000..49018bb
Binary files /dev/null and b/doc/imgs/bicubic.png differ
diff --git a/doc/imgs/bilinear.png b/doc/imgs/bilinear.png
new file mode 100644
index 0000000..46a2b61
Binary files /dev/null and b/doc/imgs/bilinear.png differ
diff --git a/doc/imgs/labeling-dynamic.png b/doc/imgs/labeling-dynamic.png
new file mode 100644
index 0000000..365d604
Binary files /dev/null and b/doc/imgs/labeling-dynamic.png differ
diff --git a/doc/imgs/labeling-meta-buffer.png b/doc/imgs/labeling-meta-buffer.png
new file mode 100644
index 0000000..4663a51
Binary files /dev/null and b/doc/imgs/labeling-meta-buffer.png differ
diff --git a/doc/imgs/labeling-metatiling-buffer.png b/doc/imgs/labeling-metatiling-buffer.png
new file mode 100644
index 0000000..9d7bc07
Binary files /dev/null and b/doc/imgs/labeling-metatiling-buffer.png differ
diff --git a/doc/imgs/labeling-metatiling.png b/doc/imgs/labeling-metatiling.png
new file mode 100644
index 0000000..4e6a978
Binary files /dev/null and b/doc/imgs/labeling-metatiling.png differ
diff --git a/doc/imgs/labeling-no-clip.png b/doc/imgs/labeling-no-clip.png
new file mode 100644
index 0000000..dbb36b8
Binary files /dev/null and b/doc/imgs/labeling-no-clip.png differ
diff --git a/doc/imgs/labeling-no-placement.png b/doc/imgs/labeling-no-placement.png
new file mode 100644
index 0000000..8b9e869
Binary files /dev/null and b/doc/imgs/labeling-no-placement.png differ
diff --git a/doc/imgs/labeling-partial-false.png b/doc/imgs/labeling-partial-false.png
new file mode 100644
index 0000000..eea19ee
Binary files /dev/null and b/doc/imgs/labeling-partial-false.png differ
diff --git a/doc/imgs/labeling-repeated.png b/doc/imgs/labeling-repeated.png
new file mode 100644
index 0000000..53b0271
Binary files /dev/null and b/doc/imgs/labeling-repeated.png differ
diff --git a/doc/imgs/mapnik-webmerc-hq.png b/doc/imgs/mapnik-webmerc-hq.png
new file mode 100644
index 0000000..ef35fc1
Binary files /dev/null and b/doc/imgs/mapnik-webmerc-hq.png differ
diff --git a/doc/imgs/mapnik-webmerc.png b/doc/imgs/mapnik-webmerc.png
new file mode 100644
index 0000000..d3c2a81
Binary files /dev/null and b/doc/imgs/mapnik-webmerc.png differ
diff --git a/doc/imgs/mapproxy-demo.png b/doc/imgs/mapproxy-demo.png
new file mode 100644
index 0000000..d7bf96f
Binary files /dev/null and b/doc/imgs/mapproxy-demo.png differ
diff --git a/doc/imgs/nearest.png b/doc/imgs/nearest.png
new file mode 100644
index 0000000..36c6cd8
Binary files /dev/null and b/doc/imgs/nearest.png differ
diff --git a/doc/index.rst b/doc/index.rst
new file mode 100644
index 0000000..29be41b
--- /dev/null
+++ b/doc/index.rst
@@ -0,0 +1,38 @@
+MapProxy 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`
+
diff --git a/doc/inspire.rst b/doc/inspire.rst
new file mode 100644
index 0000000..7d54146
--- /dev/null
+++ b/doc/inspire.rst
@@ -0,0 +1,157 @@
+.. _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<layer_metadata>`.
+
+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 at 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 at 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<layer_metadata>` 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 <https://www.loc.gov/standards/iso639-2/php/code_list.php>`_ code:
+
+::
+
+    inspire_md:
+      languages:
+        default: eng
+      ....
+
diff --git a/doc/install.rst b/doc/install.rst
new file mode 100644
index 0000000..48d26ef
--- /dev/null
+++ b/doc/install.rst
@@ -0,0 +1,191 @@
+Installation
+============
+
+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 <http://pypi.python.org/pypi/MapProxy>`_ (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 <http://virtualenv.openplans.org/#what-it-does>`_ 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 <coverages>` 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 <coverages>` allows you to read geometries from OGR datasources (Shapefiles, PostGIS, etc.). This package is optional and only required for OGR datasource support. 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 <http://pypi.python.org>`_ 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<configuration>` 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 <http://mapproxy.org/docs/1.5.0/migrate.html>`_.
diff --git a/doc/install_osgeo4w.rst b/doc/install_osgeo4w.rst
new file mode 100644
index 0000000..c2d5319
--- /dev/null
+++ b/doc/install_osgeo4w.rst
@@ -0,0 +1,67 @@
+Installation 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 <http://trac.osgeo.org/osgeo4w/wiki/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*.
+
+As happens with the standard Windows installation, you need to `install the distribute package <http://pypi.python.org/pypi/distribute#distribute-setup-py>`_ to get the ``easy_install`` command. Run this in your administrator OSGeo4W shell, e.g.::
+
+ C:\OSGeo4W> python C:\Users\MyUsername\Downloads\distribute-setup.py
+
+Once ``easy_install`` is working within the OSGeo4W python environment, run::
+
+ C:\OSGeo4W> easy_install mapproxy
+
+and
+
+::
+
+ C:\OSGeo4W> easy_install pyproj
+
+If these three last 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.
+
+
+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 <create_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
+
+
+
+
+
+
+
+
+
diff --git a/doc/install_windows.rst b/doc/install_windows.rst
new file mode 100644
index 0000000..00acbaa
--- /dev/null
+++ b/doc/install_windows.rst
@@ -0,0 +1,113 @@
+Installation on Windows
+=======================
+
+.. note:: You can also :doc:`install MapProxy inside an existing OSGeo4W installation<install_osgeo4w>`.
+
+At frist you need a working Python installation. You can download Python from: http://www.python.org/download/. MapProxy requires Python 2.7, 3.3 or 3.4. Python 2.6 should still work, but it is no longer officially supported.
+
+
+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 <http://virtualenv.openplans.org/#what-it-does>`_ 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\<command>``.
+
+.. 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 ``easy_install``.
+To install MapProxy with most dependencies call::
+
+  easy_install MapProxy
+
+This might take a minute. You can skip the next step.
+
+
+Setuptools
+----------
+
+MapProxy and most dependencies can be installed with the ``easy_install`` command.
+You need to `install the setuptool package <http://pypi.python.org/pypi/setuptools>`_ to get the ``easy_install`` command.
+
+After that you can install MapProxy with::
+
+    c:\Python27\Scripts\easy_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 ``easy_install``.
+
+PyProj
+~~~~~~
+
+Since libproj4 is generally not available on a Windows system, you will also need to install the Python package ``pyproj``.
+
+::
+
+  easy_install pyproj
+
+
+Shapely and GEOS *(optional)*
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Shapely can be installed with ``easy_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 <http://www.gisinternals.com/sdk/>`_ (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 <http://www.computerhope.com/issues/ch000549.htm>`_.
+
+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
+
+
+Platform dependent packages
+---------------------------
+
+All Python packages are downloaded from http://pypi.python.org/, but not all platform combinations might be available as a binary package, especially if you run a 64bit version of Windows.
+
+If you run into troubles 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 <http://www.lfd.uci.edu/~gohlke/pythonlibs/>`_.
+
+You can install the ``.exe`` packages with ``easy_install``::
+
+  easy_install path\to\package-xxx.exe
+
+
+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 <create_configuration>` from the installation documentation.
+
+
diff --git a/doc/labeling.rst b/doc/labeling.rst
new file mode 100644
index 0000000..2647256
--- /dev/null
+++ b/doc/labeling.rst
@@ -0,0 +1,300 @@
+WMS 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 <http://lists.osgeo.org/mailman/listinfo/mapproxy>`_ or use our `issue tracker <https://github.com/mapproxy/mapproxy/issues/>`_.
diff --git a/doc/mapproxy_2.rst b/doc/mapproxy_2.rst
new file mode 100644
index 0000000..d9db0ef
--- /dev/null
+++ b/doc/mapproxy_2.rst
@@ -0,0 +1,47 @@
+MapProxy 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.
diff --git a/doc/mapproxy_util.rst b/doc/mapproxy_util.rst
new file mode 100644
index 0000000..1d91de2
--- /dev/null
+++ b/doc/mapproxy_util.rst
@@ -0,0 +1,538 @@
+.. _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`
+- ``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 <name>, --template <name>
+
+  Create a configuration with the named template.
+
+.. cmdoption:: -f <mapproxy.yaml>, --mapproxy-conf <mapproxy.yaml>
+
+  The path to 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<mapproxy-util create -f>`) . 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 <address>, --bind <address>
+
+  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 <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 <address>, --bind <address>
+
+  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 <m|d>
+
+  Return resolutions in this unit per pixel (default meter per pixel).
+
+.. cmdoption:: -l <n>, --levels <n>
+
+  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>, --dpi <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 <URL>
+
+  Display all available Layers for this service. Each new layer will be marked with a hyphen and all sublayers are indented.
+
+.. cmdoption:: --version <versionnumber>
+
+  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 <path/to/config>, --mapproxy-config <path/to/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_name>, --grid <grid_name>
+
+  Display information only for a single grid.
+  The tool will exit, if the grid name is not found.
+
+.. cmdoption:: -c <coverage name>, --coverage <coverage name>
+
+  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.yaml>, --seed-conf <seed.yaml>
+
+  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 to 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 <grids>`. 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``:
+    Exports tiles into a MBTile file.
+
+
+
+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 \
+
diff --git a/doc/mapproxy_util_autoconfig.rst b/doc/mapproxy_util_autoconfig.rst
new file mode 100644
index 0000000..e71f6b7
--- /dev/null
+++ b/doc/mapproxy_util_autoconfig.rst
@@ -0,0 +1,165 @@
+.. _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|filename>
+
+  URL or filename of the WMS capabilities document. The tool will add `REQUEST` and `SERVICE` parameters to the URL as necessary.
+
+.. cmdoption:: --output <filename>
+
+  Filename for the created MapProxy configuration.
+
+.. cmdoption:: --output-seed <filename>
+
+  Filename for the created MapProxy-seeding configuration.
+
+.. cmdoption:: --force
+
+  Overwrite any existing configuration with the same output filename.
+
+
+
+.. cmdoption:: --base <filename>
+
+  Base configuration that should be included in the ``--output`` file with the ``base`` option.
+
+.. cmdoption:: --overwrite <filename>
+.. cmdoption:: --overwrite-seed <filename>
+
+  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
+
diff --git a/doc/seed.rst b/doc/seed.rst
new file mode 100644
index 0000000..6ce38e0
--- /dev/null
+++ b/doc/seed.rst
@@ -0,0 +1,487 @@
+Seeding
+=======
+
+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.yaml>, --seed-conf==<seed.yaml>
+
+  The seed configuration. You can also pass the configration as the last argument to ``mapproxy-seed``
+
+.. option:: -f <mapproxy.yaml>, --proxy-conf=<mapproxy.yaml>
+
+  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=<task1,task2,..>
+
+  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=<task1,task2,..>
+
+  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:: --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
+
+
+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 <seed_old_configuration>` 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 <coverages>` 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 <coverages>`.
+
+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'
+
+
+.. _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 <coverages>`.
+
+
+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 <coverages>` 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:
+      ogr_datasource: 'shps/world_boundaries_m.shp'
+      ogr_where: 'CNTRY_NAME = "Germany"'
+      ogr_srs: 'EPSG:900913'
+      level: [0, 14]
+      srs: ['EPSG:900913', 'EPSG:4326']
+    switzerland:
+      polygons: 'polygons/SZ.txt'
+      polygons_srs: EPSG:900913
+      level: [0, 14]
+      srs: ['EPSG:900913']
+    austria:
+      bbox: [9.36, 46.33, 17.28, 49.09]
+      bbox_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
diff --git a/doc/services.rst b/doc/services.rst
new file mode 100644
index 0000000..ae094cd
--- /dev/null
+++ b/doc/services.rst
@@ -0,0 +1,390 @@
+.. _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 <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<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.
+
+``featureinfo_types``
+"""""""""""""""""""""
+
+A list of feature info types the server should offer. Available types are ``text``, ``html`` and ``xml``. The types then are advertised in the capabilities with the correct mime type.
+
+``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 <fi_xslt>`.
+
+``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 <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
+          country: Germany
+          phone: +49(0)000-000000-0
+          fax: +49(0)000-000000-0
+          email: you at 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 <http://wiki.osgeo.org/wiki/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 <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<overlay_tiles_osm_openlayers>`, 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 <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 <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 <wms_md>`.
+
+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:
diff --git a/doc/sources.rst b/doc/sources.rst
new file mode 100644
index 0000000..0888248
--- /dev/null
+++ b/doc/sources.rst
@@ -0,0 +1,497 @@
+.. _sources:
+
+Sources
+#######
+
+MapProxy supports the following sources:
+
+- :ref:`wms_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 <sld_example>` 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 <fi_xslt>`.
+
+``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 <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 <scale_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 <direct_source>`.
+
+``supported_formats``
+^^^^^^^^^^^^^^^^^^^^^
+
+Use this option to specify which image formats you 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 below)
+
+See :ref:`HTTP Options <http_ssl>` for detailed documentation.
+
+.. _wms_source-ssl_no_cert_checks:
+
+``ssl_no_cert_checks``
+
+  MapProxy checks the SSL server certificates for any ``req.url`` that use HTTPS. You need to supply a file (see) that includes that certificate, otherwise MapProxy will fail to establish the connection. You can set the ``http.ssl_no_cert_checks`` options to ``true`` to disable this verification.
+
+.. _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
+
+
+.. _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 <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`` (:ref:`see above <wms_source-ssl_no_cert_checks>`)
+
+See :ref:`HTTP Options <http_ssl>` for detailed documentation.
+
+
+``seed_only``
+^^^^^^^^^^^^^
+See :ref:`seed_only <wms_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 <wms_label>` 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.
+
+
+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 <tagged_source_names>`.
+
+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 <https://github.com/mapnik/mapnik/wiki/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
+
diff --git a/doc/tutorial.rst b/doc/tutorial.rst
new file mode 100644
index 0000000..360c49d
--- /dev/null
+++ b/doc/tutorial.rst
@@ -0,0 +1,429 @@
+Tutorial
+########
+
+This tutorial should give you a quick introduction to the MapProxy configuration.
+
+You should have a :doc:`working MapProxy installation <install>`, 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 <services>`. 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 <sources>` 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 <layers_section>` 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>`::
+
+
+  mapproxy-util serve-develop mapproxy.yaml
+
+You can :download:`download the configuration <yaml/simple_conf.yaml>`.
+
+
+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 <caches>` 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 <yaml/cache_conf.yaml>`.
+
+
+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 <grids>`. 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 <mapproxy_util_scales>`
+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 <cache_resolutions>`.
+
+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 <grids>`.
+
+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 <supported_srs>` 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 <yaml/grid_conf.yaml>`.
+
+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 <yaml/merged_conf.yaml>`.
+
+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 <coverages>`.
+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 <meta_tiles>` and :ref:`Meta Buffering <meta_buffer>`.
+
+There is a :doc:`chapter on WMS labeling issues <labeling>` 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 <yaml/seed.yaml>` and the :download:`polygon file <GM.txt>`.
+
+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 <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 <http://mapproxy.org/support.html>`_
+where you can get support.
+
diff --git a/doc/tutorial.yaml b/doc/tutorial.yaml
new file mode 100644
index 0000000..9032bfd
--- /dev/null
+++ b/doc/tutorial.yaml
@@ -0,0 +1,109 @@
+services:
+  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 at 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 meta
\ No newline at end of file
diff --git a/doc/yaml/cache_conf.yaml b/doc/yaml/cache_conf.yaml
new file mode 100644
index 0000000..436a334
--- /dev/null
+++ b/doc/yaml/cache_conf.yaml
@@ -0,0 +1,41 @@
+services:
+  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 at 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'
+
+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]
diff --git a/doc/yaml/grid_conf.yaml b/doc/yaml/grid_conf.yaml
new file mode 100644
index 0000000..24e70b2
--- /dev/null
+++ b/doc/yaml/grid_conf.yaml
@@ -0,0 +1,48 @@
+services:
+  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 at 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'
+
+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'
diff --git a/doc/yaml/merged_conf.yaml b/doc/yaml/merged_conf.yaml
new file mode 100644
index 0000000..571f2ad
--- /dev/null
+++ b/doc/yaml/merged_conf.yaml
@@ -0,0 +1,47 @@
+services:
+  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 at 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'
+
+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]
diff --git a/doc/yaml/meta_conf.yaml b/doc/yaml/meta_conf.yaml
new file mode 100644
index 0000000..c5605c0
--- /dev/null
+++ b/doc/yaml/meta_conf.yaml
@@ -0,0 +1,49 @@
+services:
+  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 at 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'
+
+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]
diff --git a/doc/yaml/seed.yaml b/doc/yaml/seed.yaml
new file mode 100644
index 0000000..d11f4c8
--- /dev/null
+++ b/doc/yaml/seed.yaml
@@ -0,0 +1,12 @@
+seeds:
+  test_cache_seed:
+    caches: [test_wms_cache]
+    levels:
+      from: 6
+      to: 16
+    coverages: [germany]
+
+coverages:
+  germany:
+    polygons: ./GM.txt
+    polygons_srs: EPSG:900913
diff --git a/doc/yaml/simple_conf.yaml b/doc/yaml/simple_conf.yaml
new file mode 100644
index 0000000..286cd36
--- /dev/null
+++ b/doc/yaml/simple_conf.yaml
@@ -0,0 +1,36 @@
+services:
+  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 at 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'
+
+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]
diff --git a/mapproxy/__init__.py b/mapproxy/__init__.py
new file mode 100644
index 0000000..b0d6433
--- /dev/null
+++ b/mapproxy/__init__.py
@@ -0,0 +1 @@
+__import__('pkg_resources').declare_namespace(__name__)
\ No newline at end of file
diff --git a/mapproxy/cache/__init__.py b/mapproxy/cache/__init__.py
new file mode 100644
index 0000000..70487bb
--- /dev/null
+++ b/mapproxy/cache/__init__.py
@@ -0,0 +1,36 @@
+# 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.
+
+"""
+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="<mapproxy.layer.CacheMapLayer>"]
+    tm  [label="TileManager",  href="<mapproxy.cache.tile.TileManager>"];
+    fc      [label="FileCache", href="<mapproxy.cache.file.FileCache>"];
+    s       [label="Source", href="<mapproxy.source.Source>"];
+
+    {
+        cl -> tm [label="load_tile_coords"];
+        tm -> fc [label="load\\nstore\\nis_cached"];
+        tm -> s  [label="get_map"]
+    }
+    
+
+"""
diff --git a/mapproxy/cache/base.py b/mapproxy/cache/base.py
new file mode 100644
index 0000000..3509140
--- /dev/null
+++ b/mapproxy/cache/base.py
@@ -0,0 +1,111 @@
+# 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 sys
+import time
+
+from contextlib import contextmanager
+
+from mapproxy.util.lock import FileLock, cleanup_lockdir, DummyLock
+
+class CacheBackendError(Exception):
+    pass
+
+ at 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)
diff --git a/mapproxy/cache/couchdb.py b/mapproxy/cache/couchdb.py
new file mode 100644
index 0000000..659f384
--- /dev/null
+++ b/mapproxy/cache/couchdb.py
@@ -0,0 +1,305 @@
+# 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 __future__ import with_statement
+
+import codecs
+import datetime
+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
+
+try:
+    import simplejson as json
+except ImportError:
+    try:
+        import json
+    except ImportError:
+        json = 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.")
+
+        if json is None:
+            raise ImportError("CouchDB backend requires 'simplejson' package or Python 2.6+.")
+
+        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
diff --git a/mapproxy/cache/dummy.py b/mapproxy/cache/dummy.py
new file mode 100644
index 0000000..4d0235d
--- /dev/null
+++ b/mapproxy/cache/dummy.py
@@ -0,0 +1,34 @@
+# 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.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()
diff --git a/mapproxy/cache/file.py b/mapproxy/cache/file.py
new file mode 100644
index 0000000..839e759
--- /dev/null
+++ b/mapproxy/cache/file.py
@@ -0,0 +1,275 @@
+# 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 with_statement
+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.base import TileCacheBase, tile_buffer
+from mapproxy.compat import string_type
+
+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, lock_timeout=60.0):
+        """
+        :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
+
+        if directory_layout == 'tc':
+            self.tile_location = self._tile_location_tc
+        elif directory_layout == 'tms':
+            self.tile_location = self._tile_location_tms
+        elif directory_layout == 'quadkey':
+            self.tile_location = self._tile_location_quadkey
+        else:
+            raise ValueError('unknown directory_layout "%s"' % directory_layout)
+
+    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'
+        """
+        if isinstance(level, string_type):
+            return os.path.join(self.cache_dir, level)
+        else:
+            return os.path.join(self.cache_dir, "%02d" % level)
+
+    def _tile_location_tc(self, tile, 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
+        >>> c = FileCache(cache_dir='/tmp/cache/', file_ext='png')
+        >>> c.tile_location(Tile((3, 4, 2))).replace('\\\\', '/')
+        '/tmp/cache/02/000/000/003/000/000/004.png'
+        """
+        if tile.location is None:
+            x, y, z = tile.coord
+            parts = (self.level_location(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, self.file_ext))
+            tile.location = os.path.join(*parts)
+        if create_dir:
+            ensure_directory(tile.location)
+        return tile.location
+
+    def _tile_location_tms(self, tile, 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
+        >>> c = FileCache(cache_dir='/tmp/cache/', file_ext='png', directory_layout='tms')
+        >>> c.tile_location(Tile((3, 4, 2))).replace('\\\\', '/')
+        '/tmp/cache/2/3/4.png'
+        """
+        if tile.location is None:
+            x, y, z = tile.coord
+            tile.location = os.path.join(
+                self.level_location(str(z)),
+                str(x), str(y) + '.' + self.file_ext
+            )
+        if create_dir:
+            ensure_directory(tile.location)
+        return tile.location
+
+    def _tile_location_quadkey(self, tile, 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
+        >>> from mapproxy.cache.file import FileCache
+        >>> c = FileCache(cache_dir='/tmp/cache/', file_ext='png', directory_layout='quadkey')
+        >>> c.tile_location(Tile((3, 4, 2))).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(
+                self.cache_dir, quadKey + '.' + self.file_ext
+            )
+        if create_dir:
+            ensure_directory(tile.location)
+        return tile.location
+
+    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 if os.path.relpath is available
+        # (only supported with >= Python 2.6)
+        if hasattr(os.path, 'relpath'):
+            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)
+
diff --git a/mapproxy/cache/legend.py b/mapproxy/cache/legend.py
new file mode 100644
index 0000000..7322b2d
--- /dev/null
+++ b/mapproxy/cache/legend.py
@@ -0,0 +1,84 @@
+# 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 with_statement
+
+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
diff --git a/mapproxy/cache/mbtiles.py b/mapproxy/cache/mbtiles.py
new file mode 100644
index 0000000..492179d
--- /dev/null
+++ b/mapproxy/cache/mbtiles.py
@@ -0,0 +1,346 @@
+# This file is part of the MapProxy project.
+# Copyright (C) 2011-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 with_statement
+import hashlib
+import os
+import sqlite3
+import threading
+import time
+
+from mapproxy.image import ImageSource
+from mapproxy.cache.base import TileCacheBase, tile_buffer, CacheBackendError
+from mapproxy.util.fs import ensure_directory
+from mapproxy.util.lock import FileLock
+from mapproxy.compat import BytesIO, PY2
+
+import logging
+log = logging.getLogger(__name__)
+
+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):
+        self.lock_cache_id = 'mbtiles-' + hashlib.md5(mbtile_file.encode('utf-8')).hexdigest()
+        self.mbtile_file = mbtile_file
+        self.supports_timestamp = with_timestamps
+        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)
+        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(os.path.join(os.path.dirname(self.mbtile_file), 'init.lck'),
+                remove_on_unlock=True):
+                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)
+        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
+        with tile_buffer(tile) as buf:
+            if PY2:
+                content = buffer(buf.read())
+            else:
+                content = buf.read()
+            x, y, level = tile.coord
+            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.execute(stmt, (level, x, y, content, time.time()))
+                else:
+                    stmt = "INSERT OR REPLACE INTO tiles (zoom_level, tile_column, tile_row, tile_data) VALUES (?,?,?,?)"
+                    cursor.execute(stmt, (level, x, y, content))
+                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 len(coords) > 1000:
+            # SQLite is limited to 1000 args
+            raise CacheBackendError('cannot query SQLite for more than 333 tiles')
+
+        if self.supports_timestamp:
+            stmt = "SELECT tile_column, tile_row, tile_data, last_modified FROM tiles WHERE "
+        else:
+            stmt = "SELECT tile_column, tile_row, tile_data FROM tiles WHERE "
+        stmt += ' OR '.join(['(tile_column = ? AND tile_row = ? AND zoom_level = ?)'] * (len(coords)//3))
+
+        cursor = self.db.cursor()
+        cursor.execute(stmt, coords)
+
+        loaded_tiles = 0
+        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()
+        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):
+        self.lock_cache_id = 'sqlite-' + hashlib.md5(mbtiles_dir.encode('utf-8')).hexdigest()
+        self.cache_dir = mbtiles_dir
+        self._mbtiles = {}
+        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,
+                )
+
+        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 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)
+            return True
+        else:
+            return level_cache.remove_level_tiles_before(level, timestamp)
+
diff --git a/mapproxy/cache/meta.py b/mapproxy/cache/meta.py
new file mode 100644
index 0000000..9e664b7
--- /dev/null
+++ b/mapproxy/cache/meta.py
@@ -0,0 +1,78 @@
+from __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));
\ No newline at end of file
diff --git a/mapproxy/cache/renderd.py b/mapproxy/cache/renderd.py
new file mode 100644
index 0000000..155ac3c
--- /dev/null
+++ b/mapproxy/cache/renderd.py
@@ -0,0 +1,92 @@
+# This file is part of the MapProxy project.
+# Copyright (C) 2012, 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 with_statement
+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
+
+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'))
+
+        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)
\ No newline at end of file
diff --git a/mapproxy/cache/riak.py b/mapproxy/cache/riak.py
new file mode 100644
index 0000000..eaec17c
--- /dev/null
+++ b/mapproxy/cache/riak.py
@@ -0,0 +1,196 @@
+# 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 with_statement, 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):
+        if riak is None:
+            raise ImportError("Riak backend requires 'riak' package.")
+
+        self.nodes = nodes
+        self.protocol = protocol
+        self.lock_cache_id = 'riak-' + hashlib.md5(nodes[0]['host'] + bucket).hexdigest()
+        self.request_timeout = 10000 # 10s, TODO make configurable
+        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):
+        return self.connection.bucket(self.bucket_name)
+
+    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):
+        if tile.source:
+            return True
+        return self.load_tile(tile)
+
+    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(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.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()
diff --git a/mapproxy/cache/sqlite.py b/mapproxy/cache/sqlite.py
new file mode 100644
index 0000000..d41f5cd
--- /dev/null
+++ b/mapproxy/cache/sqlite.py
@@ -0,0 +1,413 @@
+from __future__ import print_function, division
+
+import errno
+import os
+import sqlite3
+import time
+from contextlib import contextmanager
+from functools import partial
+from mapproxy.compat.itertools import groupby
+from six.moves import map
+from six.moves import zip
+
+
+from mapproxy.compat import BytesIO
+from mapproxy.image import ImageSource, is_single_color_image
+from mapproxy.cache.base import tile_buffer
+from mapproxy.cache.base import TileCacheBase
+
+
+class Metadata(object):
+    def __init__(self, db, layer_name, tile_grid, file_ext):
+        self.layer_id = layer_name
+        self.grid = tile_grid
+        self.file_ext = file_ext
+        self.db = db
+        self.table_name = "metadata"
+
+    def _create_metadata_table(self):
+        cursor = self.db.cursor()
+        stmt = """CREATE TABLE IF NOT EXISTS %s (
+            layer_id TEXT NOT NULL,
+            matrix_id TEXT NOT NULL,
+            matrix_set_id TEXT NOT NULL,
+            table_name TEXT NOT NULL,
+            bbox TEXT,
+            srs TEXT,
+            format TEXT,
+            min_tile_col INTEGER,
+            max_tile_col INTEGER,
+            min_tile_row INTEGER,
+            max_tile_row INTEGER,
+            tile_width INTEGER,
+            tile_height INTEGER,
+            matrix_width INTEGER,
+            matrix_height INTEGER,
+            CONSTRAINT unique_rows UNIQUE (layer_id, matrix_id, matrix_set_id, table_name)
+            )""" % (self.table_name)
+        cursor.execute(stmt)
+        self.db.commit()
+
+    def store(self, tile_set):
+        self.tile_set_table_name(tile_set)
+        params = self._tile_set_params_dict(tile_set)
+        cursor = self.db.cursor()
+        stmt = """CREATE TABLE IF NOT EXISTS %s (
+            x INTEGER,
+            y INTEGER,
+            data BLOB,
+            date_added INTEGER,
+            unique_tile TEXT
+            )""" % (tile_set.table_name)
+        cursor.execute(stmt)
+        stmt = """CREATE UNIQUE INDEX IF NOT EXISTS idx_%s_xy ON %s (x, y)""" % (tile_set.table_name, tile_set.table_name)
+        cursor.execute(stmt)
+        stmt = """INSERT INTO %s (layer_id, bbox, srs, format, min_tile_col, max_tile_col, min_tile_row,
+            max_tile_row, tile_width, tile_height, matrix_width, matrix_height, matrix_id, matrix_set_id, table_name)
+            VALUES (:layer_id, :bbox, :srs, :format, :min_tile_col, :max_tile_col, :min_tile_row, :max_tile_row,
+                    :tile_width, :tile_height, :matrix_width, :matrix_height, :matrix_id,
+                    :matrix_set_name, :table_name)""" % (self.table_name)
+        try:
+            cursor.execute(stmt, params)
+        except sqlite3.IntegrityError as e:
+            pass
+        self.db.commit()
+
+    def _tile_set_params_dict(self, tile_set):
+        level = tile_set.level
+        tile_width, tile_height = self.grid.tile_size
+        matrix_width, matrix_height = self.grid.grid_sizes[level]
+        params = {
+        'layer_id' : self.layer_id,
+        'bbox' : ', '.join(map(str, [v for v in self.grid.bbox])),
+        'srs' : self.grid.srs.srs_code,
+        'format' : self.file_ext,
+        'min_tile_col' : tile_set.grid[0],
+        'max_tile_col' : tile_set.grid[2],
+        'min_tile_row' : tile_set.grid[1],
+        'max_tile_row' : tile_set.grid[3],
+        'tile_width' : tile_width,
+        'tile_height' : tile_height,
+        'matrix_width' : matrix_width,
+        'matrix_height' : matrix_height,
+        'matrix_id' : level,
+        'matrix_set_name' : self.grid.name,
+        'table_name' : tile_set.table_name
+        }
+        return params
+
+    def tile_set_table_name(self, tile_set):
+        if tile_set.table_name:
+            return tile_set.table_name
+
+        level = tile_set.level
+        grid = tile_set.grid
+        query = " AND ".join(["layer_id = ?",
+                "min_tile_col = ?",
+                "min_tile_row = ?",
+                "max_tile_col = ?",
+                "max_tile_row = ?",
+                "matrix_id = ?",
+                "matrix_set_id = ?"])
+        cursor = self.db.cursor()
+        stmt = "SELECT table_name FROM %s WHERE %s" % (self.table_name, query)
+        cursor.execute(stmt, (self.layer_id, grid[0], grid[1], grid[2], grid[3], level, self.grid.name))
+        result = cursor.fetchone()
+        if result is not None:
+            table_name = result['table_name']
+        else:
+            x0, y0, x1, y1 = tile_set.grid
+            table_name = "tileset_%s_%s_%d_%d_%d_%d_%d" % (self.layer_id, self.grid.name, tile_set.level, x0, y0, x1, y1)
+
+        tile_set.table_name = table_name
+        return table_name
+
+
+class TileSet(object):
+
+    def __init__(self, db, level, file_ext, grid, unique_tiles):
+        self.db = db
+        self.table_name = None
+        self.file_ext = file_ext
+        self._metadata_stored = False
+        self.grid = grid
+        self.level = level
+        self.unique_tiles = unique_tiles
+
+    def get_tiles(self, tiles):
+        stmt = "SELECT x, y, data, date_added, unique_tile FROM %s WHERE " % (self.table_name)
+        stmt += ' OR '.join(['(x = ? AND y = ?)'] * len(tiles))
+
+        coords = []
+        for tile in tiles:
+            x, y, level = tile.coord
+            coords.append(x)
+            coords.append(y)
+
+        cursor = self.db.cursor()
+        try:
+            cursor.execute(stmt, coords)
+        except sqlite3.OperationalError as e:
+            print(e)
+
+        #associate the right tiles with the cursor
+        tile_dict = {}
+        for tile in tiles:
+            x, y, level = tile.coord
+            tile_dict[(x, y)] = tile
+
+        for row in cursor:
+            tile = tile_dict[(row['x'], row['y'])]
+            #TODO get unique tiles if row['data'] == null
+            data = row['data'] if row['data'] is not None else self.unique_tiles.get_data(row['unique_tile'])
+            tile.timestamp = row['date_added']
+            tile.size = len(data)
+            tile.source = ImageSource(BytesIO(data), size=tile.size)
+        cursor.close()
+        return tiles
+
+    def is_cached(self, tile):
+        x, y, level = tile.coord
+        cursor = self.db.cursor()
+        stmt = "SELECT date_added from %s WHERE x = ? AND y = ?" % (self.table_name)
+        try:
+            cursor.execute(stmt, (x, y))
+        except sqlite3.OperationalError as e:
+            #table does not exist
+            #print e
+            pass
+        result = cursor.fetchone()
+        if result is not None:
+            return True
+        return False
+
+    def set_tile(self, tile):
+        x, y, z = tile.coord
+        assert self.grid[0] <= x < self.grid[2]
+        assert self.grid[1] <= y < self.grid[3]
+
+
+        color = is_single_color_image(tile.source.as_image())
+
+        with tile_buffer(tile) as buf:
+            _data = buffer(buf.read())
+
+        if color:
+            data = None
+            _color = ''.join('%02x' % v for v in color)
+            self.unique_tiles.set_data(_data, _color)
+        else:
+            #get value of cStringIO-Object and store it to a buffer
+            data = _data
+            _color = None
+
+        timestamp = int(time.time())
+        cursor = self.db.cursor()
+        stmt = "INSERT INTO %s (x, y, data, date_added, unique_tile) VALUES (?,?,?,?,?)" % (self.table_name)
+        try:
+            cursor.execute(stmt, (x, y, data, timestamp, _color))
+        except (sqlite3.IntegrityError, sqlite3.OperationalError) as e:
+            #tile is already present, updating data
+            stmt = "UPDATE %s SET data = ?, date_added = ?, unique_tile = ? WHERE x = ? AND y = ?" % (self.table_name)
+            try:
+                cursor.execute(stmt, (data, timestamp, _color, x, y))
+            except sqlite3.OperationalError as e:
+                #database is locked
+                print(e)
+                return False
+        return True
+
+    def set_tiles(self, tiles):
+        result = all([self.set_tile(t) for t in tiles])
+        self.db.commit()
+        return result
+
+    def remove_tiles(self, tiles):
+        cursor = self.db.cursor()
+        stmt = "DELETE FROM %s WHERE" % (self.table_name)
+        stmt += ' OR '.join(['(x = ? AND y = ?)'] * len(tiles))
+        coords = []
+        for t in tiles:
+            x, y, level = t.coord
+            coords.append(x)
+            coords.append(y)
+        try:
+            cursor.execute(stmt, coords)
+            self.db.commit()
+        except sqlite3.OperationalError as e:
+            #no such table
+            #print e
+            pass
+        if cursor.rowcount < 1:
+            return False
+        return True
+
+
+class TileSetManager(object):
+    def __init__(self, db, subgrid_size, file_ext, metadata, unique_tiles):
+        self.db = db
+        self.subgrid_size = subgrid_size
+        self.file_ext = file_ext
+        self.md = metadata
+        self.unique_tiles = unique_tiles
+        self._tile_sets = {}
+
+    def store(self, tiles):
+        return all(tile_set.set_tiles(list(tile_set_tiles))
+            for tile_set, tile_set_tiles
+                in groupby(tiles, partial(self._get_tile_set, create_db_entry=True)))
+
+    def load(self, tiles):
+        for tile_set, tile_set_tiles in groupby(tiles, self._get_tile_set):
+            tile_set.get_tiles(list(tile_set_tiles))
+
+        for tile in tiles:
+            if tile.source is None:
+                return False
+        return True
+
+    def remove(self, tiles):
+        return all(tile_set.remove_tiles(list(tile_set_tiles))
+            for tile_set, tile_set_tiles in groupby(tiles, self._get_tile_set))
+
+    def is_cached(self, tile):
+        tile_set = self._get_tile_set(tile)
+        return tile_set.is_cached(tile)
+
+    def _get_tile_set(self, tile, create_db_entry=False):
+        x, y, level = tile.coord
+        x0, y0 = self.subgrid_size
+        x_pos = x//x0
+        y_pos = y//y0
+        key = (x0 * x_pos, y0 * y_pos)
+        if key not in self._tile_sets:
+            grid = [x0 * x_pos, y0 * y_pos, x0 * x_pos + x0, y0 * y_pos + y0]
+            tile_set = TileSet(self.db, level, self.file_ext, grid, self.unique_tiles)
+            tile_set.table_name = self.md.tile_set_table_name(tile_set)
+            self._tile_sets[key] = tile_set
+        else:
+            tile_set = self._tile_sets[key]
+        if create_db_entry and not tile_set._metadata_stored:
+            self.md.store(tile_set)
+            tile_set._metadata_stored = True
+        return tile_set
+
+class UniqueTiles(object):
+    def __init__(self, db):
+        self.db = db
+        self.table_name = "unique_tiles"
+
+    def _create_unique_tiles_table(self):
+        cursor = self.db.cursor()
+        stmt = """CREATE TABLE IF NOT EXISTS %s (
+            id TEXT PRIMARY KEY,
+            data BLOB
+            ) """ % (self.table_name)
+        cursor.execute(stmt)
+        self.db.commit()
+
+    def set_data(self, data, color):
+        cursor = self.db.cursor()
+        stmt = "INSERT INTO %s (id, data) VALUES (?, ?)" % (self.table_name)
+        try:
+            cursor.execute(stmt, (color, data))
+        except sqlite3.IntegrityError as e:
+            #data already present
+            pass
+        #TODO check entry point for commit()
+        self.db.commit()
+
+    def get_data(self, color):
+        #_color = tile.unique_tile
+        cursor = self.db.cursor()
+        stmt = "SELECT data FROM %s WHERE id = ?" % (self.table_name)
+        try:
+            cursor.execute(stmt, (color,))
+        except sqlite3.OperationalError as e:
+            #tile not present
+            pass
+        row = cursor.fetchone()
+        #returning raw data here - an ImageSource-Object will be created within the TileSet-Class
+        return row['data']
+
+class DatabaseStore(TileCacheBase):
+    def __init__(self, path, layer_name, grid, sub_grid, file_ext):
+        self.cache_path = path
+        self.layer_name = layer_name
+        self._subgrid_size = sub_grid
+        self.grid = grid
+        self.file_ext = file_ext
+        self._tile_set_mgr = {}
+        self._db = sqlite3.connect(self.cache_path, timeout=3)
+        self._db.row_factory = sqlite3.Row
+        self._metadata = Metadata(self._db, self.layer_name, self.grid, self.file_ext)
+        self._metadata._create_metadata_table()
+        self._unique_tiles = UniqueTiles(self._db)
+        self._unique_tiles._create_unique_tiles_table()
+
+    @property
+    def db(self):
+        return self._db
+
+    def _get_tile_set_mgr(self, tile):
+        x, y, level = tile.coord
+        if level not in self._tile_set_mgr:
+            size = self.grid.grid_sizes[level]
+            size = min(size[0], self._subgrid_size[0]), min(size[1], self._subgrid_size[1])
+            self._tile_set_mgr[level] = TileSetManager(self.db, size, self.file_ext, self._metadata, self._unique_tiles)
+        return self._tile_set_mgr[level]
+
+    def store_tile(self, tile):
+        mgr = self._get_tile_set_mgr(tile)
+        return mgr.store([tile])
+
+    def store_tiles(self, tiles):
+        first_tile = None
+        for t in tiles:
+            if t.coord is not None:
+                first_tile = t
+                break
+        else:
+            return True
+        mgr = self._get_tile_set_mgr(first_tile)
+        return mgr.store(tiles)
+
+    def load_tile(self, tile, with_metadata=False):
+        if tile.coord is None:
+            return True
+
+        mgr = self._get_tile_set_mgr(tile)
+        return mgr.load([tile])
+
+    #TODO implement metadata query
+    def load_tiles(self, tiles, with_metadata=False):
+        first_tile = None
+        for t in tiles:
+            if t.coord is not None:
+                first_tile = t
+                break
+        else:
+            return True
+        mgr = self._get_tile_set_mgr(first_tile)
+        return mgr.load([t for t in tiles if t.source is None and t.coord is not None])
+
+    def remove_tile(self, tile):
+        mgr = self._get_tile_set_mgr(tile)
+        return mgr.remove([tile])
+
+    def remove_tiles(self, tiles):
+        first_tile = None
+        for t in tiles:
+            if t.coord is not None:
+                first_tile = t
+                break
+        else:
+            return True
+        mgr = self._get_tile_set_mgr(first_tile)
+        return mgr.remove(tiles)
+
+    def is_cached(self, tile):
+        if tile.coord is None:
+            return True
+        mgr = self._get_tile_set_mgr(tile)
+        return mgr.is_cached(tile)
diff --git a/mapproxy/cache/tile.py b/mapproxy/cache/tile.py
new file mode 100644
index 0000000..9f28118
--- /dev/null
+++ b/mapproxy/cache/tile.py
@@ -0,0 +1,482 @@
+# 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.
+
+"""
+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="<mapproxy.core.layer.CacheMapLayer>"]
+    tm  [label="TileManager",  href="<TileManager>"];
+    fc      [label="FileCache", href="<FileCache>"];
+    s       [label="Source", href="<mapproxy.core.source.Source>"];
+
+    {
+        cl -> tm [label="load_tile_coords"];
+        tm -> fc [label="load\\nstore\\nis_cached"];
+        tm -> s  [label="get_map"]
+    }
+
+
+"""
+
+from __future__ import with_statement
+
+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
+
+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):
+        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.transparent = self.sources[0].transparent
+        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')
+
+    @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):
+        self.cache = tile_mgr.cache
+        self.sources = tile_mgr.sources
+        self.grid = tile_mgr.grid
+        self.meta_grid = tile_mgr.meta_grid
+        self.tile_mgr = tile_mgr
+        self.dimensions = dimensions
+
+    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 []
+                # 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.
+        """
+        if len(self.sources) == 1:
+            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
+            else:
+                return img
+
+        imgs = []
+        for img in async.imap(get_map_from_source, self.sources):
+            if img is not None:
+                imgs.append(img)
+
+        if not imgs: return None
+        return merge_images(imgs, size=query.size, image_opts=self.tile_mgr.image_opts)
+
+    def _create_meta_tiles(self, meta_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):
+        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
+
+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
diff --git a/mapproxy/client/__init__.py b/mapproxy/client/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/mapproxy/client/cgi.py b/mapproxy/client/cgi.py
new file mode 100644
index 0000000..00491a8
--- /dev/null
+++ b/mapproxy/client/cgi.py
@@ -0,0 +1,140 @@
+# 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.
+
+"""
+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)
+
diff --git a/mapproxy/client/http.py b/mapproxy/client/http.py
new file mode 100644
index 0000000..7bc7202
--- /dev/null
+++ b/mapproxy/client/http.py
@@ -0,0 +1,272 @@
+# 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.
+
+"""
+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
+
+class HTTPClientError(Exception):
+    def __init__(self, arg, response_code=None):
+        Exception.__init__(self, arg)
+        self.response_code = response_code
+
+if sys.version_info >= (2, 6):
+    _urllib2_has_timeout = True
+else:
+    _urllib2_has_timeout = False
+
+_max_set_timeout = None
+
+try:
+    import ssl
+    ssl # prevent pyflakes warnings
+except ImportError:
+    ssl = None
+
+
+def _set_global_socket_timeout(timeout):
+    global _max_set_timeout
+    if _max_set_timeout is None:
+        _max_set_timeout = timeout
+    elif _max_set_timeout != timeout:
+        _max_set_timeout = max(_max_set_timeout, timeout)
+        warnings.warn("Python >=2.6 required for individual HTTP timeouts. Setting global timeout to %.1f." %
+                     _max_set_timeout)
+    socket.setdefaulttimeout(_max_set_timeout)
+
+
+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):
+        if ssl_ca_certs not in self._opener:
+            handlers = []
+            if ssl_ca_certs:
+                connection_class = verified_https_connection_with_ca_certs(ssl_ca_certs)
+                https_handler = VerifiedHTTPSHandler(connection_class=connection_class)
+                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[ssl_ca_certs] = (opener, passman)
+        else:
+            opener, passman = self._opener[ssl_ca_certs]
+
+        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):
+        if _urllib2_has_timeout:
+            self._timeout = timeout
+        else:
+            self._timeout = None
+            _set_global_socket_timeout(timeout)
+        if url and url.startswith('https'):
+            if insecure:
+                ssl_ca_certs = None
+            else:
+                if ssl is None:
+                    raise ImportError('No ssl module found. SSL certificate '
+                        'verification requires Python 2.6 or ssl module. Upgrade '
+                        'or disable verification with http.ssl_no_cert_checks option.')
+                if ssl_ca_certs is None:
+                    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)
+        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 ssl and 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 at bar:b:az@@localhost/bar')
+    ('http://localhost/bar', ('bar foo; foo at bar', 'b:az@'))
+    """
+    username = password = None
+    if '@' in url:
+        scheme, host, path, query, frag = urlparse.urlsplit(url)
+        if '@' in host:
+            auth_data, host = host.rsplit('@', 1)
+            url = url.replace(auth_data+'@', '', 1)
+            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)
+
+
+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)
\ No newline at end of file
diff --git a/mapproxy/client/log.py b/mapproxy/client/log.py
new file mode 100644
index 0000000..fe26e50
--- /dev/null
+++ b/mapproxy/client/log.py
@@ -0,0 +1,33 @@
+# 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 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)
+    
\ No newline at end of file
diff --git a/mapproxy/client/tile.py b/mapproxy/client/tile.py
new file mode 100644
index 0000000..9c22a40
--- /dev/null
+++ b/mapproxy/client/tile.py
@@ -0,0 +1,167 @@
+# 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 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)
diff --git a/mapproxy/client/wms.py b/mapproxy/client/wms.py
new file mode 100644
index 0000000..5d096a9
--- /dev/null
+++ b/mapproxy/client/wms.py
@@ -0,0 +1,229 @@
+# 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.
+
+"""
+WMS clients for maps and information.
+"""
+from __future__ import with_statement
+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
+        info_srs = self._best_supported_srs(req_srs)
+        info_bbox = req_srs.transform_bbox_to(info_srs, req_bbox)
+
+        req_coord = make_lin_transf((0, query.size[1], query.size[0], 0), req_bbox)(query.pos)
+
+        info_coord = req_srs.transform_to(info_srs, req_coord)
+        info_pos = make_lin_transf((info_bbox), (0, query.size[1], query.size[0], 0))(info_coord)
+        info_pos = int(round(info_pos[0])), int(round(info_pos[1]))
+
+        info_query = InfoQuery(
+            bbox=info_bbox,
+            size=query.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)
+
diff --git a/mapproxy/compat/__init__.py b/mapproxy/compat/__init__.py
new file mode 100644
index 0000000..fdb594f
--- /dev/null
+++ b/mapproxy/compat/__init__.py
@@ -0,0 +1,41 @@
+import 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
\ No newline at end of file
diff --git a/mapproxy/compat/image.py b/mapproxy/compat/image.py
new file mode 100644
index 0000000..d6378ec
--- /dev/null
+++ b/mapproxy/compat/image.py
@@ -0,0 +1,69 @@
+# 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 warnings
+
+__all__ = ['Image', 'ImageColor', 'ImageDraw', 'ImageFont', 'ImagePalette',
+           'ImageChops', 'quantize']
+
+try:
+    from PIL import Image, ImageColor, ImageDraw, ImageFont, ImagePalette, ImageChops
+    # prevent pyflakes warnings
+    Image, ImageColor, ImageDraw, ImageFont, ImagePalette, ImageChops
+except ImportError:
+    try:
+        import Image, ImageColor, ImageDraw, ImageFont, ImagePalette, ImageChops
+        # prevent pyflakes warnings
+        Image, ImageColor, ImageDraw, ImageFont, ImagePalette, ImageChops
+    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 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_pil
\ No newline at end of file
diff --git a/mapproxy/compat/itertools.py b/mapproxy/compat/itertools.py
new file mode 100644
index 0000000..5e26a64
--- /dev/null
+++ b/mapproxy/compat/itertools.py
@@ -0,0 +1,27 @@
+from __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,
+    )
+
+else:
+    izip = zip
+    imap = map
+    from itertools import (
+        zip_longest as izip_longest,
+        islice,
+        chain,
+        groupby,
+    )
+
diff --git a/mapproxy/compat/modules.py b/mapproxy/compat/modules.py
new file mode 100644
index 0000000..6c64227
--- /dev/null
+++ b/mapproxy/compat/modules.py
@@ -0,0 +1,9 @@
+import sys
+PY2 = sys.version_info[0] == 2
+
+__all__ = ['urlparse']
+
+if PY2:
+    import urlparse; urlparse
+else:
+    from urllib import parse as urlparse
\ No newline at end of file
diff --git a/mapproxy/config/__init__.py b/mapproxy/config/__init__.py
new file mode 100644
index 0000000..72ef6cf
--- /dev/null
+++ b/mapproxy/config/__init__.py
@@ -0,0 +1,22 @@
+# 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 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']
\ No newline at end of file
diff --git a/mapproxy/config/config.py b/mapproxy/config/config.py
new file mode 100644
index 0000000..620e145
--- /dev/null
+++ b/mapproxy/config/config.py
@@ -0,0 +1,204 @@
+# 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.
+
+"""
+System-wide configuration.
+"""
+from __future__ import with_statement
+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
+
+ at 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
diff --git a/mapproxy/config/coverage.py b/mapproxy/config/coverage.py
new file mode 100644
index 0000000..7f8985d
--- /dev/null
+++ b/mapproxy/config/coverage.py
@@ -0,0 +1,75 @@
+# 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 re
+
+from mapproxy.srs import SRS
+from mapproxy.config import abspath
+from mapproxy.util.geom import (
+    load_datasource,
+    load_ogr_datasource,
+    load_polygons,
+    require_geom_support,
+    build_multipolygon,
+)
+from mapproxy.util.coverage import 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):
+    if '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)
+    else:
+        return None
+    return coverage(geom or bbox, SRS(srs))
diff --git a/mapproxy/config/defaults.py b/mapproxy/config/defaults.py
new file mode 100644
index 0000000..badbed8
--- /dev/null
+++ b/mapproxy/config/defaults.py
@@ -0,0 +1,95 @@
+# 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.
+
+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,
+)
+
+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 = '*',
+)
diff --git a/mapproxy/config/loader.py b/mapproxy/config/loader.py
new file mode 100644
index 0000000..c6e6fad
--- /dev/null
+++ b/mapproxy/config/loader.py
@@ -0,0 +1,1758 @@
+# 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.
+
+"""
+Configuration loading and system initializing.
+"""
+from __future__ import with_statement, 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.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]):
+                # 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 'sources' in layers_conf and 'name' 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 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 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'] = 'http://localhost' + 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,
+    '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')
+
+    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'):
+            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
+
+        lock_timeout = self.context.globals.get_value('http.client_timeout', {})
+
+        return FileCache(
+            cache_dir,
+            file_ext=file_ext,
+            directory_layout=directory_layout,
+            lock_timeout=lock_timeout,
+            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)
+
+        return MBTilesCache(
+            mbfile_path,
+        )
+
+    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
+            )
+
+        return MBTilesLevelCache(
+            cache_dir,
+        )
+
+    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)
+
+        return RiakCache(nodes=nodes, protocol=protocol, bucket=bucket,
+            tile_grid=grid_conf.tile_grid(),
+            use_secondary_index=use_secondary_index,
+        )
+
+    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
+
+    @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')
+        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)
+
+        for grid_name, grid_conf in self.grid_confs():
+            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 self.conf['sources']:
+                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 self.conf['sources']:
+                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)
+            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 Python >=2.6 and requests")
+                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')
+                tile_creator_class = partial(RenderdTileCreator, renderd_address,
+                    priority=priority, tile_locker=locker)
+
+            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)
+            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, caches[0][0].transparent, 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:
+                layers.append(WMSLayerConfiguration(layer_conf, self.context).wms_layer())
+
+        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:
+            raise ValueError('wms layer requires sources and/or layers')
+
+        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):
+        from mapproxy.service.tile import TileLayer
+        from mapproxy.cache.dummy import DummyCache
+
+        sources = []
+        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['name_path'] = (self.conf['name'], grid.srs.srs_code.replace(':', '').upper())
+                md['grid_name'] = grid.name
+                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 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():
+                if not tile_layer: continue
+                if use_grid_names:
+                    # new style layer names are tuples
+                    tile_layer.md['name_path'] = (tile_layer.md['name'], tile_layer.md['grid_name'])
+                    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)
+
+        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.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',
+                    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):
+            layers[layer_name] = layer_conf.wms_layer()
+        tile_layers = self.tile_layers(conf)
+        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')
+
+        # 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
+
+
diff --git a/mapproxy/config/spec.py b/mapproxy/config/spec.py
new file mode 100644
index 0000000..26b1e2d
--- /dev/null
+++ b/mapproxy/config/spec.py
@@ -0,0 +1,510 @@
+# 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 __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 = {
+    '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(),
+}
+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(),
+        'tile_lock_dir': str(),
+    },
+    'mbtiles': {
+        'filename': str(),
+        'tile_lock_dir': str(),
+    },
+    'couchdb': {
+        'url': str(),
+        'db_name': str(),
+        'tile_metadata': {
+            anything(): anything()
+        },
+        'tile_id': 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(),
+    }
+}
+
+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],
+        }
+    ],
+}
+
+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(),
+            'max_tile_limit': number(),
+            'minimize_meta_requests': bool(),
+            'concurrent_tile_creators': int(),
+            'link_single_color_images': bool(),
+        },
+        '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'): [string_type],
+            'name': str(),
+            'grids': [str()],
+            'cache_dir': str(),
+            'meta_size': [number()],
+            'meta_buffer': number(),
+            '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(),
+            }),
+            '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],
+            '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))
diff --git a/mapproxy/config/validator.py b/mapproxy/config/validator.py
new file mode 100644
index 0000000..da3ac92
--- /dev/null
+++ b/mapproxy/config/validator.py
@@ -0,0 +1,206 @@
+# This file is part of the MapProxy project.
+# Copyright (C) 2015 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.path
+from mapproxy.compat import string_type
+
+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', [])
+        child_layers = layer.get('layers', [])
+
+        if not layer_sources and not child_layers:
+            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']
+                )
+            )
+
+    def _split_tagged_source(self, source_name):
+        layers = None
+        if ':' in str(source_name):
+            source_name, layers = str(source_name).split(':')
+            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):
+        for cache_source in cache.get('sources', []):
+            cache_source, layers = self._split_tagged_source(cache_source)
+            if self.sources_conf and cache_source in self.sources_conf:
+                source = self.sources_conf.get(cache_source)
+                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" % (
+                            cache_source,
+                            name,
+                            ', '.join(TAGGED_SOURCE_TYPES)
+                        )
+                    )
+                    continue
+                self._validate_source(cache_source, source, layers)
+                continue
+            if self.caches_conf and cache_source in self.caches_conf:
+                self._validate_cache(cache_source, self.caches_conf[cache_source])
+                continue
+            self.errors.append(
+                "Source '%s' for cache '%s' not found in config" % (
+                    cache_source,
+                    name
+                )
+            )
+
+        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
+                    )
+                )
diff --git a/mapproxy/config_template/__init__.py b/mapproxy/config_template/__init__.py
new file mode 100644
index 0000000..7c993b0
--- /dev/null
+++ b/mapproxy/config_template/__init__.py
@@ -0,0 +1,14 @@
+try:
+    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:
+    pass
\ No newline at end of file
diff --git a/mapproxy/config_template/base_config/config.wsgi b/mapproxy/config_template/base_config/config.wsgi
new file mode 100644
index 0000000..a75c5be
--- /dev/null
+++ b/mapproxy/config_template/base_config/config.wsgi
@@ -0,0 +1,10 @@
+# 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')
diff --git a/mapproxy/config_template/base_config/full_example.yaml b/mapproxy/config_template/base_config/full_example.yaml
new file mode 100644
index 0000000..60c029b
--- /dev/null
+++ b/mapproxy/config_template/base_config/full_example.yaml
@@ -0,0 +1,566 @@
+# #####################################################################
+#                 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 at omniscale.de
+      # multiline strings are possible with the right indention
+      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'
+
+  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 at omniscale.de
+      # multiline strings are possible with the right indention
+      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'
+
+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]
+
+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
diff --git a/mapproxy/config_template/base_config/full_seed_example.yaml b/mapproxy/config_template/base_config/full_seed_example.yaml
new file mode 100644
index 0000000..1352c3b
--- /dev/null
+++ b/mapproxy/config_template/base_config/full_seed_example.yaml
@@ -0,0 +1,79 @@
+# #####################################################################
+#               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'
diff --git a/mapproxy/config_template/base_config/log.ini b/mapproxy/config_template/base_config/log.ini
new file mode 100644
index 0000000..06e0860
--- /dev/null
+++ b/mapproxy/config_template/base_config/log.ini
@@ -0,0 +1,35 @@
+[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
diff --git a/mapproxy/config_template/base_config/mapproxy.yaml b/mapproxy/config_template/base_config/mapproxy.yaml
new file mode 100644
index 0000000..ff2f18c
--- /dev/null
+++ b/mapproxy/config_template/base_config/mapproxy.yaml
@@ -0,0 +1,61 @@
+# -------------------------------
+# 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:
diff --git a/mapproxy/config_template/base_config/seed.yaml b/mapproxy/config_template/base_config/seed.yaml
new file mode 100644
index 0000000..bec7649
--- /dev/null
+++ b/mapproxy/config_template/base_config/seed.yaml
@@ -0,0 +1,27 @@
+# ---------------------------------------
+# 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:
diff --git a/mapproxy/config_template/paster/etc/config.ini b/mapproxy/config_template/paster/etc/config.ini
new file mode 100644
index 0000000..4abca21
--- /dev/null
+++ b/mapproxy/config_template/paster/etc/config.ini
@@ -0,0 +1,21 @@
+[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
diff --git a/mapproxy/config_template/paster/etc/config.wsgi b/mapproxy/config_template/paster/etc/config.wsgi
new file mode 100644
index 0000000..26c6f02
--- /dev/null
+++ b/mapproxy/config_template/paster/etc/config.wsgi
@@ -0,0 +1,6 @@
+# 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__))
\ No newline at end of file
diff --git a/mapproxy/config_template/paster/etc/develop.ini b/mapproxy/config_template/paster/etc/develop.ini
new file mode 100644
index 0000000..1f38d36
--- /dev/null
+++ b/mapproxy/config_template/paster/etc/develop.ini
@@ -0,0 +1,39 @@
+[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
diff --git a/mapproxy/config_template/paster/etc/log_deploy.ini b/mapproxy/config_template/paster/etc/log_deploy.ini
new file mode 100644
index 0000000..849dfa1
--- /dev/null
+++ b/mapproxy/config_template/paster/etc/log_deploy.ini
@@ -0,0 +1,45 @@
+[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
diff --git a/mapproxy/config_template/paster/etc/mapproxy.yaml b/mapproxy/config_template/paster/etc/mapproxy.yaml
new file mode 100644
index 0000000..59445d3
--- /dev/null
+++ b/mapproxy/config_template/paster/etc/mapproxy.yaml
@@ -0,0 +1,140 @@
+services:
+  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 at 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'
+
+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
diff --git a/mapproxy/config_template/paster/etc/seed.yaml b/mapproxy/config_template/paster/etc/seed.yaml
new file mode 100644
index 0000000..19dad15
--- /dev/null
+++ b/mapproxy/config_template/paster/etc/seed.yaml
@@ -0,0 +1,50 @@
+seeds:
+  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:900913
\ No newline at end of file
diff --git a/mapproxy/exception.py b/mapproxy/exception.py
new file mode 100644
index 0000000..e2f9fc9
--- /dev/null
+++ b/mapproxy/exception.py
@@ -0,0 +1,140 @@
+# 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.
+
+"""
+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)
diff --git a/mapproxy/featureinfo.py b/mapproxy/featureinfo.py
new file mode 100644
index 0000000..e78d7ef
--- /dev/null
+++ b/mapproxy/featureinfo.py
@@ -0,0 +1,162 @@
+# 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 copy
+from io import StringIO
+from mapproxy.compat import string_type, PY2, BytesIO
+
+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)
+
+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)
+
+    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
diff --git a/mapproxy/grid.py b/mapproxy/grid.py
new file mode 100644
index 0000000..4608d60
--- /dev/null
+++ b/mapproxy/grid.py
@@ -0,0 +1,1163 @@
+# 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.
+
+"""
+(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:
+                return threshold_result
+
+            if l_res <= res*self.stretch_factor:
+                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, xxx_todo_changeme):
+        """
+        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) = xxx_todo_changeme
+        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),\
+          <generator object ...>)
+        """
+        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),\
+          <generator object ...>)
+        """
+        # 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),\
+          <generator object ...>)
+        """
+
+        # 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_PIXLE_SIZE = 0.00028 #m/px
+
+def ogc_scale_to_res(scale):
+    return scale * OGC_PIXLE_SIZE
+def res_to_ogc_scale(res):
+    return res / OGC_PIXLE_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 '<ResolutionRange(min_res=%.3f, max_res=%.3f)>' % (
+            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
diff --git a/mapproxy/image/__init__.py b/mapproxy/image/__init__.py
new file mode 100644
index 0000000..c599bf4
--- /dev/null
+++ b/mapproxy/image/__init__.py
@@ -0,0 +1,451 @@
+# 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.
+
+"""
+Image and tile manipulation (transforming, merging, etc).
+"""
+from __future__ import with_statement
+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=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()
+    if image_opts.mode and img.mode[0] == 'I' 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
+
+    if image_opts.colors:
+        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
+    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')
+        img = img.quantize(colors, Image.FASTOCTREE)
+    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)
diff --git a/mapproxy/image/fonts/DejaVuSans.ttf b/mapproxy/image/fonts/DejaVuSans.ttf
new file mode 100644
index 0000000..2f1d69e
Binary files /dev/null and b/mapproxy/image/fonts/DejaVuSans.ttf differ
diff --git a/mapproxy/image/fonts/DejaVuSansMono.ttf b/mapproxy/image/fonts/DejaVuSansMono.ttf
new file mode 100644
index 0000000..ea0bfd8
Binary files /dev/null and b/mapproxy/image/fonts/DejaVuSansMono.ttf differ
diff --git a/mapproxy/image/fonts/LICENSE b/mapproxy/image/fonts/LICENSE
new file mode 100644
index 0000000..254e2cc
--- /dev/null
+++ b/mapproxy/image/fonts/LICENSE
@@ -0,0 +1,99 @@
+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 $
diff --git a/mapproxy/image/fonts/__init__.py b/mapproxy/image/fonts/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/mapproxy/image/mask.py b/mapproxy/image/mask.py
new file mode 100644
index 0000000..5d48ffe
--- /dev/null
+++ b/mapproxy/image/mask.py
@@ -0,0 +1,55 @@
+# 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, 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, 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(img, bbox, polygons):
+    transf = make_lin_transf(bbox, (0, 0) + img.size)
+
+    mask = Image.new('L', img.size, 255)
+    draw = ImageDraw.Draw(mask)
+
+    for p in polygons:
+        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)
+
+    return mask
diff --git a/mapproxy/image/merge.py b/mapproxy/image/merge.py
new file mode 100644
index 0000000..20aa58b
--- /dev/null
+++ b/mapproxy/image/merge.py
@@ -0,0 +1,183 @@
+# 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.
+
+"""
+Image and tile manipulation (transforming, merging, etc).
+"""
+from __future__ import with_statement
+
+from mapproxy.compat.image import Image, ImageColor, ImageChops
+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, layer_img, layer=None):
+        """
+        Add one layer image to merge. Bottom-layers first.
+        """
+        if layer_img is not None:
+            self.layers.append((layer_img, layer))
+
+    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 = 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 or 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 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 and 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 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, 255 * opacity)
+                    )
+                    img.putalpha(alpha)
+                if img.mode == 'RGB':
+                    result.paste(img, (0, 0))
+                else:
+                    # assume paletted images have transparency
+                    if img.mode == 'P':
+                        img = img.convert('RGBA')
+                    result = Image.alpha_composite(result, img)
+            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 == 'RGBA' or img.mode == '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)
+
+def merge_images(images, image_opts, size=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
+    :rtype: `ImageSource`
+    """
+    merger = LayerMerger()
+    for img in images:
+        merger.add(img)
+    return merger.merge(image_opts=image_opts, size=size)
+
+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))
diff --git a/mapproxy/image/message.py b/mapproxy/image/message.py
new file mode 100644
index 0000000..6208556
--- /dev/null
+++ b/mapproxy/image/message.py
@@ -0,0 +1,347 @@
+# 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 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
diff --git a/mapproxy/image/opts.py b/mapproxy/image/opts.py
new file mode 100644
index 0000000..3489d54
--- /dev/null
+++ b/mapproxy/image/opts.py
@@ -0,0 +1,167 @@
+# -:- encoding: utf-8 -:-
+# 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 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 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 options
\ No newline at end of file
diff --git a/mapproxy/image/tile.py b/mapproxy/image/tile.py
new file mode 100644
index 0000000..8612b4e
--- /dev/null
+++ b/mapproxy/image/tile.py
@@ -0,0 +1,167 @@
+# 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.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)
diff --git a/mapproxy/image/transform.py b/mapproxy/image/transform.py
new file mode 100644
index 0000000..eb117d3
--- /dev/null
+++ b/mapproxy/image/transform.py
@@ -0,0 +1,195 @@
+# 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
+
+from mapproxy.compat.image import Image
+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).
+
+           This method will perform good transformation results if the number of
+           quads is high enough (even transformations with strong distortions).
+           Tests on images up to 1500x1500 have shown that meshes beyond 8x8
+           will not improve the results.
+
+           .. _PIL Image.transform:
+              http://www.pythonware.com/library/pil/handbook/image.htm#Image.transform
+
+           ::
+
+                    src quad                   dst quad
+                    .----.   <- coord-           .----.
+                   /    /       transformation   |    |
+                  /    /                         |    |
+                 .----.   img-transformation ->  .----.----
+                           |                     |    |
+            ---------------.
+            large src image                   large dst image
+    """
+    def __init__(self, src_srs, dst_srs, mesh_div=8):
+        """
+        :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
+        :param mesh_div: the number of quads in each direction to use
+                         for transformation (totals to ``mesh_div**2`` quads)
+
+        """
+        self.src_srs = src_srs
+        self.dst_srs = dst_srs
+        self.mesh_div = mesh_div
+        self.dst_bbox = self.dst_size = None
+
+    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:
+            result = src_img.as_image().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).
+        """
+        src_bbox = self.src_srs.align_bbox(src_bbox)
+        dst_bbox = self.dst_srs.align_bbox(dst_bbox)
+        src_size = src_img.size
+        src_quad = (0, 0, src_size[0], src_size[1])
+        dst_quad = (0, 0, dst_size[0], dst_size[1])
+        to_src_px = make_lin_transf(src_bbox, src_quad)
+        to_dst_w = make_lin_transf(dst_quad, dst_bbox)
+        meshes = []
+        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]+0.5, dst_px[1]+0.5))
+                src_w = self.dst_srs.transform_to(self.src_srs, dst_w)
+                src_px = to_src_px(src_w)
+                src_quad.extend(src_px)
+            return quad, src_quad
+
+        mesh_div = self.mesh_div
+        while mesh_div > 1 and (dst_size[0] / mesh_div < 10 or dst_size[1] / mesh_div < 10):
+            mesh_div -= 1
+        for quad in griddify(dst_quad, mesh_div):
+            meshes.append(dst_quad_to_src(quad))
+
+        result = src_img.as_image().transform(dst_size, Image.MESH, meshes,
+                                              image_filter[image_opts.resampling])
+        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 griddify(quad, steps):
+    """
+    Divides a box (`quad`) into multiple boxes (``steps x steps``).
+
+    >>> list(griddify((0, 0, 500, 500), 2))
+    [(0, 0, 250, 250), (250, 0, 500, 250), (0, 250, 250, 500), (250, 250, 500, 500)]
+    """
+    w = quad[2]-quad[0]
+    h = quad[3]-quad[1]
+    x_step = w / float(steps)
+    y_step = h / float(steps)
+
+    y = quad[1]
+    for _ in range(steps):
+        x = quad[0]
+        for _ in range(steps):
+            yield (int(x), int(y), int(x+x_step), int(y+y_step))
+            x += x_step
+        y += y_step
diff --git a/mapproxy/layer.py b/mapproxy/layer.py
new file mode 100644
index 0000000..d404955
--- /dev/null
+++ b/mapproxy/layer.py
@@ -0,0 +1,483 @@
+# -:- 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.
+
+"""
+Layers that can get maps/infos from different sources/caches.
+"""
+
+from __future__ import division, with_statement
+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_transparent(self):
+        return self.image_opts.transparent
+
+    def _set_transparent(self, value):
+        self.image_opts.transparent = value
+
+    transparent = property(_get_transparent, _set_transparent)
+
+    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):
+        if self.opacity is None:
+            return not self.transparent
+        return self.opacity >= 0.99
+
+    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, self.size[1], self.size[0], 0), 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
+
+        #TODO
+        self.transparent = self.one.transparent
+        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, transparent=False, opacity=None):
+        MapLayer.__init__(self)
+        self.transparent = transparent
+        # 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.transparent = tile_manager.transparent
+        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)
+
+
diff --git a/mapproxy/multiapp.py b/mapproxy/multiapp.py
new file mode 100644
index 0000000..a50763d
--- /dev/null
+++ b/mapproxy/multiapp.py
@@ -0,0 +1,228 @@
+# -:- 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.
+
+from __future__ import with_statement
+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 = "<html><body><h1>Welcome to MapProxy %s</h1>" % mapproxy.version.version
+
+        url = req.script_url
+        if self.list_apps:
+            html += "<h2>available instances:</h2><ul>"
+            html += '\n'.join('<li><a href="%(url)s/%(name)s/">%(name)s</a></li>' % {'url': url, 'name': app}
+                              for app in self.loader.available_apps())
+            html += '</ul>'
+        html += '</body></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}
diff --git a/mapproxy/proj.py b/mapproxy/proj.py
new file mode 100644
index 0000000..4e22c95
--- /dev/null
+++ b/mapproxy/proj.py
@@ -0,0 +1,275 @@
+# 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.
+
+"""
+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)
diff --git a/mapproxy/request/__init__.py b/mapproxy/request/__init__.py
new file mode 100644
index 0000000..fe1fefe
--- /dev/null
+++ b/mapproxy/request/__init__.py
@@ -0,0 +1,18 @@
+# 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 mapproxy.request.base import Request, url_decode
+
+__all__ = ['Request', 'url_decode']
\ No newline at end of file
diff --git a/mapproxy/request/base.py b/mapproxy/request/base.py
new file mode 100644
index 0000000..8d32aa7
--- /dev/null
+++ b/mapproxy/request/base.py
@@ -0,0 +1,467 @@
+# 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.
+
+"""
+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
+
diff --git a/mapproxy/request/tile.py b/mapproxy/request/tile.py
new file mode 100644
index 0000000..478863d
--- /dev/null
+++ b/mapproxy/request/tile.py
@@ -0,0 +1,128 @@
+# 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 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<begin>/[^/]+)/
+            ((?P<version>1\.0\.0)/)?
+            (?P<layer>[^/]+)/
+            ((?P<layer_spec>[^/]+)/)?
+            (?P<z>-?\d+)/
+            (?P<x>-?\d+)/
+            (?P<y>-?\d+)\.(?P<format>\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<layer>[^/]+))?
+        (/(?P<layer_spec>[^/]+))?
+        $''', 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)
\ No newline at end of file
diff --git a/mapproxy/request/wms/__init__.py b/mapproxy/request/wms/__init__.py
new file mode 100644
index 0000000..47e8489
--- /dev/null
+++ b/mapproxy/request/wms/__init__.py
@@ -0,0 +1,761 @@
+# 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.
+
+"""
+Service requests (parsing, handling, etc).
+"""
+from __future__ import with_statement
+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'),
+                      ),
+    None: (('text', 'text/plain'),
+           ('html', 'text/html'),
+           ('xml', 'application/vnd.ogc.gml'),
+          )
+}
+
+
+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'
+
diff --git a/mapproxy/request/wms/exception.py b/mapproxy/request/wms/exception.py
new file mode 100644
index 0000000..e94d20d
--- /dev/null
+++ b/mapproxy/request/wms/exception.py
@@ -0,0 +1,99 @@
+# 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.
+
+"""
+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)
diff --git a/mapproxy/request/wmts.py b/mapproxy/request/wmts.py
new file mode 100644
index 0000000..97300cb
--- /dev/null
+++ b/mapproxy/request/wmts.py
@@ -0,0 +1,387 @@
+# 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.
+
+"""
+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]), self.params.coord[2]) # TODO
+        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
diff --git a/mapproxy/response.py b/mapproxy/response.py
new file mode 100644
index 0000000..098aa93
--- /dev/null
+++ b/mapproxy/response.py
@@ -0,0 +1,228 @@
+# 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.
+
+"""
+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'] = 'max-age=%d public' % 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]
diff --git a/mapproxy/script/__init__.py b/mapproxy/script/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/mapproxy/script/conf/__init__.py b/mapproxy/script/conf/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/mapproxy/script/conf/app.py b/mapproxy/script/conf/app.py
new file mode 100644
index 0000000..f2d7937
--- /dev/null
+++ b/mapproxy/script/conf/app.py
@@ -0,0 +1,192 @@
+# -:- 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 __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)
+
+
+ at 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 0
\ No newline at end of file
diff --git a/mapproxy/script/conf/caches.py b/mapproxy/script/conf/caches.py
new file mode 100644
index 0000000..bb011ca
--- /dev/null
+++ b/mapproxy/script/conf/caches.py
@@ -0,0 +1,45 @@
+# -:- 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.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
+
diff --git a/mapproxy/script/conf/layers.py b/mapproxy/script/conf/layers.py
new file mode 100644
index 0000000..193ed16
--- /dev/null
+++ b/mapproxy/script/conf/layers.py
@@ -0,0 +1,54 @@
+# -:- 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.
+
+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
+
diff --git a/mapproxy/script/conf/seeds.py b/mapproxy/script/conf/seeds.py
new file mode 100644
index 0000000..43118fa
--- /dev/null
+++ b/mapproxy/script/conf/seeds.py
@@ -0,0 +1,37 @@
+# -:- 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.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, cleanups
\ No newline at end of file
diff --git a/mapproxy/script/conf/sources.py b/mapproxy/script/conf/sources.py
new file mode 100644
index 0000000..e814ec1
--- /dev/null
+++ b/mapproxy/script/conf/sources.py
@@ -0,0 +1,86 @@
+# -:- 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.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
+
+
diff --git a/mapproxy/script/conf/utils.py b/mapproxy/script/conf/utils.py
new file mode 100644
index 0000000..3a96880
--- /dev/null
+++ b/mapproxy/script/conf/utils.py
@@ -0,0 +1,143 @@
+# -:- 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 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)
+
diff --git a/mapproxy/script/export.py b/mapproxy/script/export.py
new file mode 100644
index 0000000..c1effd4
--- /dev/null
+++ b/mapproxy/script/export.py
@@ -0,0 +1,261 @@
+# 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 __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("--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 in ('tc', 'mapproxy'):
+        cache_conf['cache'] = {
+            'type': 'file',
+            '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, None, seed_coverage)
+
+    print(format_export_task(task, custom_grid=custom_grid))
+
+    logger = ProgressLog(verbose=True, silent=False)
+    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)
+
diff --git a/mapproxy/script/grids.py b/mapproxy/script/grids.py
new file mode 100644
index 0000000..43d0c96
--- /dev/null
+++ b/mapproxy/script/grids.py
@@ -0,0 +1,191 @@
+# 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 __future__ import print_function, division
+
+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 = %8s (%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 = %8s" % (level, res, ' '*spaces, tiles_in_x, tiles_in_y, human_readable_number(total_tiles)))
+
+def human_readable_number(num):
+    if num > 10**12:
+        return '%.3fT' % (num/10**12)
+    if num > 10**9:
+        return '%.3fG' % (num/10**9)
+    if num > 10**6:
+        return '%.3fM' % (num/10**6)
+    if num > 10**3:
+        return '%.3fK' % (num/10**3)
+    return '%d' % 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)
+
+    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:
+        with local_base_config(proxy_configuration.base_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)
+
+
+
diff --git a/mapproxy/script/scales.py b/mapproxy/script/scales.py
new file mode 100644
index 0000000..854a379
--- /dev/null
+++ b/mapproxy/script/scales.py
@@ -0,0 +1,126 @@
+# 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 __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 = map(eval, args)
+    else:
+        values = map(float, 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('    ]')
diff --git a/mapproxy/script/util.py b/mapproxy/script/util.py
new file mode 100755
index 0000000..585f51f
--- /dev/null
+++ b/mapproxy/script/util.py
@@ -0,0 +1,368 @@
+# 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 __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.version import version
+from mapproxy.script.scales import scales_command
+from mapproxy.script.wms_capabilities import wms_capabilities_command
+from mapproxy.script.grids import grids_command
+from mapproxy.script.export import export_command
+from mapproxy.script.conf.app import config_command
+
+
+
+def setup_logging(level=logging.INFO):
+    mapproxy_log = logging.getLogger('mapproxy')
+    mapproxy_log.setLevel(level)
+
+    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 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)
+
+    run_simple(host, port, app, use_reloader=True, processes=1,
+        threaded=True, passthrough_errors=True,
+        extra_files=app.config_files.keys())
+
+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.'
+    }
+}
+
+
+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()
\ No newline at end of file
diff --git a/mapproxy/script/wms_capabilities.py b/mapproxy/script/wms_capabilities.py
new file mode 100644
index 0000000..1066030
--- /dev/null
+++ b/mapproxy/script/wms_capabilities.py
@@ -0,0 +1,152 @@
+# 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 __future__ import print_function
+
+import sys
+import optparse
+
+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 Exception as ex:
+        # catch all, etree.ParseError only avail since Python 2.7
+        # 2.5 and 2.6 raises exc from underlying implementation like expat
+        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)
diff --git a/mapproxy/seed/__init__.py b/mapproxy/seed/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/mapproxy/seed/cachelock.py b/mapproxy/seed/cachelock.py
new file mode 100644
index 0000000..212a749
--- /dev/null
+++ b/mapproxy/seed/cachelock.py
@@ -0,0 +1,122 @@
+# 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 __future__ import with_statement
+
+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'):
+        pass
\ No newline at end of file
diff --git a/mapproxy/seed/cleanup.py b/mapproxy/seed/cleanup.py
new file mode 100644
index 0000000..058a338
--- /dev/null
+++ b/mapproxy/seed/cleanup.py
@@ -0,0 +1,98 @@
+# 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
+
+import os
+from mapproxy.seed.util import format_cleanup_task
+from mapproxy.util.fs import cleanup_directory
+from mapproxy.seed.seeder import TileWorkerPool, TileWalker, TileCleanupWorker
+
+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
+
+        if task.complete_extent:
+            if hasattr(task.tile_manager.cache, 'level_location'):
+                simple_cleanup(task, dry_run=dry_run, progress_logger=progress_logger)
+                continue
+            elif hasattr(task.tile_manager.cache, 'remove_level_tiles_before'):
+                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)
+
+def simple_cleanup(task, dry_run, progress_logger=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))
+        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
+
+    # only supported with >= Python 2.6
+    if hasattr(os.path, 'relpath'):
+        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):
+    """
+    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)
+    try:
+        tile_walker.walk()
+    except KeyboardInterrupt:
+        tile_worker_pool.stop(force=True)
+        raise
+    finally:
+        tile_worker_pool.stop()
diff --git a/mapproxy/seed/config.py b/mapproxy/seed/config.py
new file mode 100644
index 0000000..9b1628d
--- /dev/null
+++ b/mapproxy/seed/config.py
@@ -0,0 +1,462 @@
+# 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 __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)
+
diff --git a/mapproxy/seed/script.py b/mapproxy/seed/script.py
new file mode 100644
index 0000000..9644fd2
--- /dev/null
+++ b/mapproxy/seed/script.py
@@ -0,0 +1,268 @@
+# 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
+
+import sys
+import logging
+from logging.config import fileConfig
+
+from optparse import OptionParser
+
+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
+
+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)
+
+class SeedScript(object):
+    usage = "usage: %prog [options] seed_conf"
+    parser = OptionParser(usage)
+    parser.add_option("-q", "--quiet",
+                      action="count", dest="quiet", default=0,
+                      help="don't print status messages to stdout")
+    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("--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)
+
+        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
+
+        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
+
+            progress = None
+            if options.continue_seed or options.progress_file:
+                if options.progress_file:
+                    progress_file = options.progress_file
+                else:
+                    progress_file = '.mapproxy_seed_progress'
+                progress = ProgressStore(progress_file,
+                    continue_seed=options.continue_seed)
+
+            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)
+                    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 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()
diff --git a/mapproxy/seed/seeder.py b/mapproxy/seed/seeder.py
new file mode 100644
index 0000000..7769221
--- /dev/null
+++ b/mapproxy/seed/seeder.py
@@ -0,0 +1,492 @@
+# 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 __future__ import print_function, division
+
+import sys
+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, ETA, 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 TileProcessor(object):
+    def __init__(self, dry_run=False):
+        self._lastlog = time.time()
+        self.dry_run = dry_run
+
+    def log_progress(self, progress):
+        if (self._lastlog + .1) < time.time():
+            # log progress at most every 100ms
+            print('[%s] %6.2f%% %s \tETA: %s\r' % (
+                timestamp(), progress[1]*100, progress[0],
+                progress[2]
+            ), end=' ')
+            sys.stdout.flush()
+            self._lastlog = time.time()
+
+    def process(self, tiles, progress):
+        if not self.dry_run:
+            self.process_tiles(tiles)
+
+        self.log_progress(progress)
+
+    def stop(self):
+        raise NotImplementedError()
+
+    def process_tiles(self, tiles):
+        raise NotImplementedError()
+
+
+class TileWorkerPool(TileProcessor):
+    """
+    Manages multiple TileWorker.
+    """
+    def __init__(self, task, worker_class, size=2, dry_run=False, progress_logger=None):
+        TileProcessor.__init__(self, dry_run=dry_run)
+        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.eta = ETA()
+        self.level_progress_percentages = [1.0]
+        self.level_progresses = []
+        self.progress_str_parts = []
+        self.old_level_progresses = None
+        if old_progress_identifier is not None:
+            self.old_level_progresses = old_progress_identifier
+
+    def step_forward(self, subtiles=1):
+        self.progress += self.level_progress_percentages[-1] / subtiles
+        self.eta.update(self.progress)
+
+    @property
+    def progress_str(self):
+        return ''.join(self.progress_str_parts)
+
+    @contextmanager
+    def step_down(self, i, subtiles):
+        self.level_progresses.append((i, subtiles))
+        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.pop()
+
+    def already_processed(self):
+        if self.old_level_progresses == []:
+            return True
+
+        if self.old_level_progresses is None:
+            return False
+
+        if self.progress_is_behind(self.old_level_progresses, self.level_progresses):
+            return True
+        else:
+            return False
+
+    def current_progress_identifier(self):
+        return self.level_progresses
+
+    @staticmethod
+    def progress_is_behind(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.progress_is_behind([], [(0, 1)])
+        True
+        >>> SeedProgress.progress_is_behind([(0, 1), (1, 4)], [(0, 1)])
+        False
+        >>> SeedProgress.progress_is_behind([(0, 1), (1, 4)], [(0, 1), (0, 4)])
+        True
+        >>> SeedProgress.progress_is_behind([(0, 1), (1, 4)], [(0, 1), (1, 4)])
+        True
+        >>> SeedProgress.progress_is_behind([(0, 1), (1, 4)], [(0, 1), (3, 4)])
+        False
+
+        """
+        for old, current in izip_longest(old_progress, current_progress, fillvalue=(9e15, 9e15)):
+            if old < current:
+                return False
+            if old > current:
+                return True
+        return True
+
+    def running(self):
+        return True
+
+class StopProcess(Exception):
+    pass
+
+class SeedInterrupted(Exception):
+    pass
+
+
+class TileWalker(object):
+    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)
+        self.report_till_level = task.levels[int(num_seed_levels * 0.8)]
+        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()
+
+    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
+        current_level += 1
+
+        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,
+                            all_subtiles=all_subtiles)
+
+            if not process:
+                continue
+
+            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
+
+    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()
+
+
diff --git a/mapproxy/seed/spec.py b/mapproxy/seed/spec.py
new file mode 100644
index 0000000..470e8f1
--- /dev/null
+++ b/mapproxy/seed/spec.py
@@ -0,0 +1,74 @@
+# 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.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),
+        }
+    },
+}
diff --git a/mapproxy/seed/util.py b/mapproxy/seed/util.py
new file mode 100644
index 0000000..4898998
--- /dev/null
+++ b/mapproxy/seed/util.py
@@ -0,0 +1,294 @@
+# 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 __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
+
+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 ETA(object):
+    def __init__(self):
+        self.avgs = []
+        self.last_tick_start = time.time()
+        self.progress = 0.0
+        self.ticks = 10000
+        self.tick_duration_sums = 0.0
+        self.tick_duration_divisor = 0.0
+        self.tick_count = 0
+
+    def update(self, progress):
+        self.progress = progress
+        missing_ticks = (self.progress * self.ticks) - self.tick_count
+        if missing_ticks:
+            tick_duration = (time.time() - self.last_tick_start) / missing_ticks
+
+            while missing_ticks > 0:
+
+                # reduce the influence of older messurements
+                self.tick_duration_sums *= 0.999
+                self.tick_duration_divisor *= 0.999
+
+                self.tick_count += 1
+
+                self.tick_duration_sums += tick_duration
+                self.tick_duration_divisor += 1
+
+                missing_ticks -= 1
+
+            self.last_tick_start = time.time()
+
+    def eta_string(self):
+        timestamp = self.eta()
+        if timestamp is None:
+            return 'N/A'
+        try:
+            return time.strftime('%Y-%m-%d-%H:%M:%S', time.localtime(timestamp))
+        except ValueError:
+            # raised when time is out of range (e.g. year >2038)
+            return 'N/A'
+
+    def eta(self):
+        if not self.tick_count: return
+        return (self.last_tick_start +
+                ((self.tick_duration_sums/self.tick_duration_divisor)
+                 * (self.ticks - self.tick_count)))
+
+    def __str__(self):
+        return self.eta_string()
+
+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:
+            with open(self.filename + '.tmp', 'wb') as f:
+                f.write(pickle.dumps(self.status))
+                f.flush()
+                os.fsync(f.fileno())
+            os.rename(self.filename + '.tmp', self.filename)
+        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.lastlog = time.time()
+        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.lastlog + .1) < time.time():
+            # log progress at most every 100ms
+            self.out.write('[%s] %6.2f%%\t%-20s ETA: %s\r' % (
+                timestamp(), progress.progress*100, progress.progress_str,
+                progress.eta
+            ))
+            self.out.flush()
+            self.lastlog = time.time()
+
+    def log_progress(self, progress, level, bbox, tiles):
+        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
+        self.out.write('[%s] %2s %6.2f%% %s (%d tiles) ETA: %s\n' % (
+            timestamp(), level, progress.progress*100,
+            format_bbox(bbox), tiles, progress.eta))
+        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)
diff --git a/mapproxy/service/__init__.py b/mapproxy/service/__init__.py
new file mode 100644
index 0000000..3809c9f
--- /dev/null
+++ b/mapproxy/service/__init__.py
@@ -0,0 +1,14 @@
+# 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.
diff --git a/mapproxy/service/base.py b/mapproxy/service/base.py
new file mode 100644
index 0000000..61b4794
--- /dev/null
+++ b/mapproxy/service/base.py
@@ -0,0 +1,46 @@
+# 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.
+
+"""
+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
+
+
diff --git a/mapproxy/service/demo.py b/mapproxy/service/demo.py
new file mode 100644
index 0000000..d83c5dc
--- /dev/null
+++ b/mapproxy/service/demo.py
@@ -0,0 +1,247 @@
+# 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.
+
+"""
+Demo service handler
+"""
+from __future__ import division
+
+import os
+import pkg_resources
+import mimetypes
+from collections import defaultdict
+
+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:
+                url = '%s/tms/1.0.0/%s/%s'%(req.script_url, req.args['layer'], req.args['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 = 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=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")
+        tile_layer = self.tile_layers['_'.join([req.args['tms_layer'], req.args['srs'].replace(':','')])]
+        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=req.args['srs'],
+                                   format=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")
+        wmts_layer = self.tile_layers['_'.join([req.args['wmts_layer'], req.args['srs'].replace(':','')])]
+
+        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=req.args['format'],
+                                   srs=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
diff --git a/mapproxy/service/kml.py b/mapproxy/service/kml.py
new file mode 100644
index 0000000..8115cd6
--- /dev/null
+++ b/mapproxy/service/kml.py
@@ -0,0 +1,326 @@
+# 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 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<begin>/kml)/
+            (?P<layer>[^/]+)/
+            ((?P<layer_spec>[^/]+)/)?
+            (?P<z>-?\d+)/
+            (?P<x>-?\d+)/
+            (?P<y>-?\d+)\.(?P<format>\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<begin>/kml)/
+            (?P<layer>[^/]+)
+            (/(?P<layer_spec>[^/]+))?
+            /?$
+    ''', 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 = """<?xml version="1.0"?>
+<kml xmlns="http://www.opengis.net/kml/2.2">
+  <Document>
+    <name>%(layer_name)s</name>
+    <Region>
+      <LatLonAltBox>
+        <north>%(north)f</north><south>%(south)f</south>
+        <east>%(east)f</east><west>%(west)f</west>
+      </LatLonAltBox>
+    </Region>
+    """
+
+    network_link = """<NetworkLink>
+      <name>%(layer_name)s - %(coord)s</name>
+      <Region>
+        <LatLonAltBox>
+          <north>%(north)f</north><south>%(south)f</south>
+          <east>%(east)f</east><west>%(west)f</west>
+        </LatLonAltBox>
+        <Lod>
+          <minLodPixels>%(min_lod)d</minLodPixels>
+          <maxLodPixels>-1</maxLodPixels>
+        </Lod>
+      </Region>
+      <Link>
+        <href>%(href)s</href>
+        <viewRefreshMode>onRegion</viewRefreshMode>
+        <viewFormat/>
+      </Link>
+    </NetworkLink>
+    """
+    ground_overlay = """<GroundOverlay>
+      <name>%(coord)s</name>
+      <Region>
+        <LatLonAltBox>
+          <north>%(north)f</north><south>%(south)f</south>
+          <east>%(east)f</east><west>%(west)f</west>
+        </LatLonAltBox>
+        <Lod>
+          <minLodPixels>%(min_lod)d</minLodPixels>
+          <maxLodPixels>%(max_lod)d</maxLodPixels>
+          <minFadeExtent>8</minFadeExtent>
+          <maxFadeExtent>8</maxFadeExtent>
+        </Lod>
+      </Region>
+      <drawOrder>%(level)d</drawOrder>
+      <Icon>
+        <href>%(href)s</href>
+      </Icon>
+      <LatLonBox>
+        <north>%(north)f</north><south>%(south)f</south>
+        <east>%(east)f</east><west>%(west)f</west>
+      </LatLonBox>
+    </GroundOverlay>
+    """
+    footer = """</Document>
+</kml>
+"""
+    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)
\ No newline at end of file
diff --git a/mapproxy/service/ows.py b/mapproxy/service/ows.py
new file mode 100644
index 0000000..7b20fbf
--- /dev/null
+++ b/mapproxy/service/ows.py
@@ -0,0 +1,38 @@
+# 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.
+
+"""
+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)
diff --git a/mapproxy/service/template_helper.py b/mapproxy/service/template_helper.py
new file mode 100644
index 0000000..371b46e
--- /dev/null
+++ b/mapproxy/service/template_helper.py
@@ -0,0 +1,73 @@
+# 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 cgi import escape
+from mapproxy.template import bunch
+
+__all__ = ['escape', 'indent', 'bunch', 'wms100format', 'wms100info_format',
+    'wms111metadatatype', 'limit_llbbox']
+
+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'
+
+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
\ No newline at end of file
diff --git a/mapproxy/service/templates/demo/capabilities_demo.html b/mapproxy/service/templates/demo/capabilities_demo.html
new file mode 100644
index 0000000..c1ab1e8
--- /dev/null
+++ b/mapproxy/service/templates/demo/capabilities_demo.html
@@ -0,0 +1,18 @@
+{{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
+}}
+            <h2>{{service}} GetCapabilities</h2>
+            <a href="{{url}}">{{url}}</a>
+            <pre>
+{{for line in capabilities}}
+{{cgi.escape(wrapper.fill(line.decode('utf8')))}}
+{{endfor}}
+            </pre>
+
diff --git a/mapproxy/service/templates/demo/demo.html b/mapproxy/service/templates/demo/demo.html
new file mode 100644
index 0000000..1a6a4cb
--- /dev/null
+++ b/mapproxy/service/templates/demo/demo.html
@@ -0,0 +1,179 @@
+{{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}}
+<script type="text/javascript">
+    /**
+    * Getting the closest parent with the given tag name.
+    */
+    function NeighborFormSubmit(obj)
+    {
+        var obj_parent = obj.parentNode;
+        if (!obj_parent) return false;
+        if (obj_parent.tagName.toLowerCase() != 'tr') return NeighborFormSubmit(obj_parent);
+        var forms = obj_parent.getElementsByTagName('form');
+        if (!forms) return false;
+        forms[0].submit()
+    }
+</script>
+{{enddef}}
+            <h2>About</h2>
+            <p>MapProxy Version {{version}}</p>
+            <h2>WMS</h2>
+            {{if 'wms' in services}}
+            <div class="capabilities">
+                <span>Capabilities document</span>
+                <span><a href="../service?REQUEST=GetCapabilities">(download as xml)</a></span>
+                <span><a href="../demo/?wms_capabilities">(view as html)</a></span>
+            </div>
+            {{if 'wms_111' in services }}
+                <table class="code">
+                    <tr>
+                      <th>Layer</th>
+                      <th>Coordinate-System</th>
+                      <th>Image-Format</th>
+                    </tr>
+                    {{for layer in layers.values()}}
+                    <tr>
+                        <td rowspan="{{len(formats)}}">{{layer.name}}</td>
+                        {{for loop, format in looper(formats)}}
+                        <td class="value epsg-codes">
+                                <form action="" method="GET">
+                                <select name="srs" onchange="this.form.submit()">
+                                    {{for srs_name, srs_code in layer_srs(layer)}}
+                                    <option value="{{srs_code}}">{{srs_name}}</option>
+                                    {{endfor}}
+                                </select>
+                                <input type="hidden" name="format" value="{{format}}">
+                                <input type="hidden" name="wms_layer" value="{{layer.name}}">
+                                </form>
+                        </td>
+                        <td class="value">
+                            <a href="#" onclick="NeighborFormSubmit(this)">{{format | strip}}</a>
+                        </td>
+                    </tr>
+                        {{endfor}}
+                    {{endfor}}
+                </table>
+                <p>Coordinate systems marked with * are supported without reprojection.</p>
+                {{else}}
+                <div class="capabilities">
+                    <span>The demo service only supports WMS 1.1.1. Enable 1.1.1 to see a list of your configured layers.</span>
+                </div>
+                {{endif}}
+            {{else}}
+            <div class="capabilities">
+                <span>This service is not available with the current configuration.</span>
+            </div>
+            {{endif}}
+            <h2>WMS-C</h2>
+            {{if 'wms' in services}}
+            <div class="capabilities">
+                <span>Capabilities document</span>
+                <span><a href="../service?REQUEST=GetCapabilities&tiled=true">(download as xml)</a></span>
+                <span><a href="../demo/?wmsc_capabilities">(view as html)</a></span>
+            </div>
+            {{else}}
+            <div class="capabilities">
+                <span>This service is not available with the current configuration.</span>
+            </div>
+            {{endif}}
+            <h2>WMTS</h2>
+            {{if 'wmts' in services}}
+                {{if 'wmts_kvp' in services}}
+                <div class="capabilities">
+                    <span>KVP capabilities document</span>
+                    <span><a href="../service?REQUEST=GetCapabilities&SERVICE=WMTS">(download as xml)</a></span>
+                    <span><a href="../demo/?wmts_capabilities_kvp">(view as html)</a></span>
+                </div>
+                {{endif}}
+                {{if 'wmts_restful' in services}}
+                <div class="capabilities">
+                    <span>RESTFul capabilities document</span>
+                    <span><a href="../wmts/1.0.0/WMTSCapabilities.xml">(download as xml)</a></span>
+                    <span><a href="../demo/?wmts_capabilities">(view as html)</a></span>
+                </div>
+                {{endif}}
+            <table class="code">
+                <tr>
+                    <th>Layer</th>
+                    <th>Coordinate-System</th>
+                    <th>Image-Format</th>
+                </tr>
+            {{for wmts_layer_name, wmts_layers in wmts_layers.items()}}
+            <tr><td rowspan="{{len(wmts_layers)}}">{{wmts_layer_name}}</td>
+                {{for loop, layer in looper(wmts_layers)}}
+                    {{if not loop.first}}
+                        <tr>
+                    {{endif}}
+                    {{if layer.grid.supports_access_with_origin('nw')}}
+                    <td class="value"><a href="../demo/?wmts_layer={{layer.name}}&format={{layer.format}}&srs={{layer.grid.srs.srs_code | quote}}">{{layer.grid.srs.srs_code}}</a></td>
+                    <td class="value"><a href="../demo/?wmts_layer={{layer.name}}&format={{layer.format}}&srs={{layer.grid.srs.srs_code | quote}}">{{layer.format}}</a>
+                    {{else}}
+                    <td class="value" colspan="2">
+                        {{layer.grid.name}} not compatible with WMTS
+                    </td>
+                    {{endif}}
+                    </tr>
+                {{endfor}}
+            {{endfor}}
+            </table>
+            {{else}}
+            <div class="capabilities">
+                <span>This service is not available with the current configuration.</span>
+            </div>
+            {{endif}}
+            <h2>TMS</h2>
+            {{if 'tms' in services}}
+            <div class="capabilities">
+                <span>Capabilities document</span>
+                <span><a href="../tms/1.0.0/">(download as xml)</a></span>
+                <span><a href="../demo/?tms_capabilities">(view as html)</a></span>
+            </div>
+            <table class="code">
+                <tr>
+                    <th>Layer</th>
+                    <th>Coordinate-System</th>
+                    <th>Image-Format</th>
+                    <th>Layer Capabilities</th>
+                </tr>
+            {{for tms_layer_name, tms_layers in tms_layers.items()}}
+            <tr><td rowspan="{{len(tms_layers)}}">{{tms_layer_name}}</td>
+                {{for loop, layer in looper(tms_layers)}}
+                    {{if not loop.first}}
+                        <tr>
+                    {{endif}}
+                    {{if layer.grid.supports_access_with_origin('sw')}}
+                    <td class="value"><a href="../demo/?tms_layer={{layer.name}}&format={{layer.format}}&srs={{layer.grid.srs.srs_code | quote}}">{{layer.grid.srs.srs_code}}</a></td>
+                    <td class="value"><a href="../demo/?tms_layer={{layer.name}}&format={{layer.format}}&srs={{layer.grid.srs.srs_code | quote}}">{{layer.format}}</a></td>
+                    <td class="value"><a href="../demo/?tms_capabilities&layer={{layer.name}}&srs={{layer.md['name_path'][1]}}">click here</a></td>
+                    {{else}}
+                    <td class="value" colspan="3">
+                        {{layer.grid.name}} not compatible with TMS
+                    </td>
+                    {{endif}}
+                    </tr>
+                {{endfor}}
+            {{endfor}}
+            </table>
+            {{else}}
+            <div class="capabilities">
+                <span>This service is not available with the current configuration.</span>
+            </div>
+            {{endif}}
diff --git a/mapproxy/service/templates/demo/openlayers-demo.cfg b/mapproxy/service/templates/demo/openlayers-demo.cfg
new file mode 100644
index 0000000..1b12895
--- /dev/null
+++ b/mapproxy/service/templates/demo/openlayers-demo.cfg
@@ -0,0 +1,16 @@
+[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]
\ No newline at end of file
diff --git a/mapproxy/service/templates/demo/static.html b/mapproxy/service/templates/demo/static.html
new file mode 100644
index 0000000..4e84d9e
--- /dev/null
+++ b/mapproxy/service/templates/demo/static.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+    <head>
+        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+        <title>MapProxy Demo</title>
+        <link rel="stylesheet" type="text/css" href="static/site.css" />
+
+  </head>
+  {{if jscript_openlayers !=None }}
+    <body onload="init()">
+  {{else}}
+    <body>
+  {{endif}}
+        <div id="box">
+        <div id="header">
+            <img src="static/logo.png" height="55" width="52">
+            <h1>MapProxy</h1>
+        </div>
+        <div id="menu">
+{{if menu_title}}
+            <span class=""><a href="./">Demos</a></span>
+            <span class="current"><a href="">{{menu_title}}</a></span>
+{{else}}
+            <span class="current"><a href="./">Demos</a></span>
+{{endif}}
+        </div>
+        <div id="content">
+{{self.body}}
+        </div>
+        </div>
+{{jscript_openlayers}}
+{{jscript_functions}}
+    </body>
+</html>
diff --git a/mapproxy/service/templates/demo/static/OpenLayers.js b/mapproxy/service/templates/demo/static/OpenLayers.js
new file mode 100644
index 0000000..4269bdd
--- /dev/null
+++ b/mapproxy/service/templates/demo/static/OpenLayers.js
@@ -0,0 +1,619 @@
+/*
+
+  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: <https://github.com/openlayers/openlayers>)
+
+*/
+
+/**
+ * Contains XMLHttpRequest.js <http://code.google.com/p/xmlhttprequest/>
+ * 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;i<len;i++){src=s[i].getAttribute('src');if(src){m=src.match(r);if(m){l=m[1];break;}}}
+return(function(){return l;});})(),ImgPath:''};OpenLayers.Class=function(){var len=arguments.length;var P=arguments[0];var F=arguments[len-1];var C=typeof F.initialize=="function"?F.initialize:function(){P.prototype.initialize.apply(this,arguments);};if(len>1){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;i<l;i++){o=arguments[i];if(typeof o==="function"){o=o.prototype;}
+OpenLayers.Util.extend(C.prototype,o);}};OpenLayers.Util=OpenLayers.Util||{};OpenLayers.Util.extend=function(destination,source){destination=destination||{};if(source){for(var property in source){var value=source[property];if(value!==undefined){destination[property]=value;}}
+var sourceIsEvt=typeof window.Event=="function"&&source instanceof window.Event;if(!sourceIsEvt&&source.hasOwnProperty&&source.hasOwnProperty("toString")){destination.toString=source.toString;}}
+return destination;};OpenLayers.Animation=(function(window){var isNative=!!(window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame);var requestFrame=(function(){var request=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(callback,element){window.setTimeout(callba [...]
+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 [...]
+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;},ease [...]
+return camelizedString;},format:function(template,context,args){if(!context){context=window;}
+var replacer=function(str,match){var replacement;var subs=match.split(/\.+/);for(var i=0;i<subs.length;i++){if(i==0){replacement=context;}
+replacement=replacement[subs[i]];}
+if(typeof replacement=="function"){replacement=args?replacement.apply(null,args):replacement();}
+if(typeof replacement=='undefined'){return'undefined';}else{return replacement;}};return template.replace(OpenLayers.String.tokenRegEx,replacer);},tokenRegEx:/\$\{([\w.]+?)\}/g,numberRegEx:/^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/,isNumeric:function(value){return OpenLayers.String.numberRegEx.test(value);},numericIf:function(value){return OpenLayers.String.isNumeric(value)?parseFloat(value):value;}};OpenLayers.Number={decimalSeparator:".",thousandsSeparator:",",limitSigDigs:func [...]
+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){va [...]
+for(var i=0;i<len;i++){if(i in array){var val=array[i];if(callback.call(caller,val,i,array)){selected.push(val);}}}}
+return selected;}};OpenLayers.Bounds=OpenLayers.Class({left:null,bottom:null,right:null,top:null,centerLonLat:null,initialize:function(left,bottom,right,top){if(OpenLayers.Util.isArray(left)){top=left[3];right=left[2];bottom=left[1];left=left[0];}
+if(left!=null){this.left=OpenLayers.Util.toFloat(left);}
+if(bottom!=null){this.bottom=OpenLayers.Util.toFloat(bottom);}
+if(right!=null){this.right=OpenLayers.Util.toFloat(right);}
+if(top!=null){this.top=OpenLayers.Util.toFloat(top);}},clone:function(){return new OpenLayers.Bounds(this.left,this.bottom,this.right,this.top);},equals:function(bounds){var equals=false;if(bounds!=null){equals=((this.left==bounds.left)&&(this.right==bounds.right)&&(this.top==bounds.top)&&(this.bottom==bounds.bottom));}
+return equals;},toString:function(){return[this.left,this.bottom,this.right,this.top].join(",");},toArray:function(reverseAxisOrder){if(reverseAxisOrder===true){return[this.bottom,this.left,this.top,this.right];}else{return[this.left,this.bottom,this.right,this.top];}},toBBOX:function(decimal,reverseAxisOrder){if(decimal==null){decimal=6;}
+var mult=Math.pow(10,decimal);var xmin=Math.round(this.left*mult)/mult;var ymin=Math.round(this.bottom*mult)/mult;var xmax=Math.round(this.right*mult)/mult;var ymax=Math.round(this.top*mult)/mult;if(reverseAxisOrder===true){return ymin+","+xmin+","+ymax+","+xmax;}else{return xmin+","+ymin+","+xmax+","+ymax;}},toGeometry:function(){return new OpenLayers.Geometry.Polygon([new OpenLayers.Geometry.LinearRing([new OpenLayers.Geometry.Point(this.left,this.bottom),new OpenLayers.Geometry.Point( [...]
+return this.centerLonLat;},scale:function(ratio,origin){if(origin==null){origin=this.getCenterLonLat();}
+var origx,origy;if(origin.CLASS_NAME=="OpenLayers.LonLat"){origx=origin.lon;origy=origin.lat;}else{origx=origin.x;origy=origin.y;}
+var left=(this.left-origx)*ratio+origx;var bottom=(this.bottom-origy)*ratio+origy;var right=(this.right-origx)*ratio+origx;var top=(this.top-origy)*ratio+origy;return new OpenLayers.Bounds(left,bottom,right,top);},add:function(x,y){if((x==null)||(y==null)){throw new TypeError('Bounds.add cannot receive null values');}
+return new OpenLayers.Bounds(this.left+x,this.bottom+y,this.right+x,this.top+y);},extend:function(object){var bounds=null;if(object){switch(object.CLASS_NAME){case"OpenLayers.LonLat":bounds=new OpenLayers.Bounds(object.lon,object.lat,object.lon,object.lat);break;case"OpenLayers.Geometry.Point":bounds=new OpenLayers.Bounds(object.x,object.y,object.x,object.y);break;case"OpenLayers.Bounds":bounds=object;break;}
+if(bounds){this.centerLonLat=null;if((this.left==null)||(bounds.left<this.left)){this.left=bounds.left;}
+if((this.bottom==null)||(bounds.bottom<this.bottom)){this.bottom=bounds.bottom;}
+if((this.right==null)||(bounds.right>this.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)&&(x<this.right)&&(y>this.bottom)&&(y<this.top));}
+return contains;},intersectsBounds:function(bounds,options){if(typeof options==="boolean"){options={inclusive:options};}
+options=options||{};if(options.worldBounds){var self=this.wrapDateLine(options.worldBounds);bounds=bounds.wrapDateLine(options.worldBounds);}else{self=this;}
+if(options.inclusive==null){options.inclusive=true;}
+var intersects=false;var mightTouch=(self.left==bounds.right||self.right==bounds.left||self.top==bounds.bottom||self.bottom==bounds.top);if(options.inclusive||!mightTouch){var inBottom=(((bounds.bottom>=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<bounds.top)));var inLeft=(((bounds.left>=self.left)&&(bounds.left<=self.right))||((self [...]
+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<center.lat)?"b":"t";q [...]
+while(newBounds.left+leftTolerance>=maxExtent.right&&newBounds.right>maxExtent.right){newBounds=newBounds.add(-width,0);}
+var newLeft=newBounds.left+leftTolerance;if(newLeft<maxExtent.right&&newLeft>maxExtent.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) [...]
+return element;},removeClass:function(element,name){var names=element.className;if(names){element.className=OpenLayers.String.trim(names.replace(new RegExp("(^|\\s+)"+name+"(\\s+|$)")," "));}
+return element;},toggleClass:function(element,name){if(OpenLayers.Element.hasClass(element,name)){OpenLayers.Element.removeClass(element,name);}else{OpenLayers.Element.addClass(element,name);}
+return element;},getStyle:function(element,style){element=OpenLayers.Util.getElement(element);var value=null;if(element&&element.style){value=element.style[OpenLayers.String.camelize(style)];if(!value){if(document.defaultView&&document.defaultView.getComputedStyle){var css=document.defaultView.getComputedStyle(element,null);value=css?css.getPropertyValue(style):null;}else if(element.currentStyle){value=element.currentStyle[OpenLayers.String.camelize(style)];}}
+var positions=['left','top','right','bottom'];if(window.opera&&(OpenLayers.Util.indexOf(positions,style)!=-1)&&(OpenLayers.Element.getStyle(element,'position')=='static')){value='auto';}}
+return value=='auto'?null:value;}};OpenLayers.LonLat=OpenLayers.Class({lon:0.0,lat:0.0,initialize:function(lon,lat){if(OpenLayers.Util.isArray(lon)){lat=lon[1];lon=lon[0];}
+this.lon=OpenLayers.Util.toFloat(lon);this.lat=OpenLayers.Util.toFloat(lat);},toString:function(){return("lon="+this.lon+",lat="+this.lat);},toShortString:function(){return(this.lon+", "+this.lat);},clone:function(){return new OpenLayers.LonLat(this.lon,this.lat);},add:function(lon,lat){if((lon==null)||(lat==null)){throw new TypeError('LonLat.add cannot receive null values');}
+return new OpenLayers.LonLat(this.lon+OpenLayers.Util.toFloat(lon),this.lat+OpenLayers.Util.toFloat(lat));},equals:function(ll){var equals=false;if(ll!=null){equals=((this.lon==ll.lon&&this.lat==ll.lat)||(isNaN(this.lon)&&isNaN(this.lat)&&isNaN(ll.lon)&&isNaN(ll.lat)));}
+return equals;},transform:function(source,dest){var point=OpenLayers.Projection.transform({'x':this.lon,'y':this.lat},source,dest);this.lon=point.x;this.lat=point.y;return this;},wrapDateLine:function(maxExtent){var newLonLat=this.clone();if(maxExtent){while(newLonLat.lon<maxExtent.left){newLonLat.lon+=maxExtent.getWidth();}
+while(newLonLat.lon>maxExtent.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 [...]
+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.getElem [...]
+return OpenLayers.Lang.code;},setCode:function(code){var lang;if(!code){code=(OpenLayers.BROWSER_NAME=="msie")?navigator.userLanguage:navigator.language;}
+var parts=code.split('-');parts[0]=parts[0].toLowerCase();if(typeof OpenLayers.Lang[parts[0]]=="object"){lang=parts[0];}
+if(parts[1]){var testLang=parts[0]+'-'+parts[1].toUpperCase();if(typeof OpenLayers.Lang[testLang]=="object"){lang=testLang;}}
+if(!lang){OpenLayers.Console.warn('Failed to find OpenLayers.Lang.'+parts.join("-")+' dictionary, falling back to default language');lang=OpenLayers.Lang.defaultCode;}
+OpenLayers.Lang.code=lang;},translate:function(key,context){var dictionary=OpenLayers.Lang[OpenLayers.Lang.getCode()];var message=dictionary&&dictionary[key];if(!message){message=key;}
+if(context){message=OpenLayers.String.format(message,context);}
+return message;}};OpenLayers.i18n=OpenLayers.Lang.translate;OpenLayers.Util=OpenLayers.Util||{};OpenLayers.Util.getElement=function(){var elements=[];for(var i=0,len=arguments.length;i<len;i++){var element=arguments[i];if(typeof element=='string'){element=document.getElementById(element);}
+if(arguments.length==1){return element;}
+elements.push(element);}
+return elements;};OpenLayers.Util.isElement=function(o){return!!(o&&o.nodeType===1);};OpenLayers.Util.isArray=function(a){return(Object.prototype.toString.call(a)==='[object Array]');};if(typeof window.$==="undefined"){window.$=OpenLayers.Util.getElement;}
+OpenLayers.Util.removeItem=function(array,item){for(var i=array.length-1;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<len;i++){if(array[i]==obj){return i;}}
+return-1;}};OpenLayers.Util.modifyDOMElement=function(element,id,px,sz,position,border,overflow,opacity){if(id){element.id=id;}
+if(px){element.style.left=px.x+"px";element.style.top=px.y+"px";}
+if(sz){element.style.width=sz.w+"px";element.style.height=sz.h+"px";}
+if(position){element.style.position=position;}
+if(border){element.style.border=border;}
+if(overflow){element.style.overflow=overflow;}
+if(parseFloat(opacity)>=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 i [...]
+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;itemIndex<len;itemIndex++){item=value[itemIndex];encodedItemArray.push(encodeURIComponent((item===null||item===undefined)?"":item));}
+encodedValue=encodedItemArray.join(",");}
+else{encodedValue=encodeURIComponent(value);}
+paramsArray.push(encodeURIComponent(key)+"="+encodedValue);}}
+return paramsArray.join("&");};OpenLayers.Util.urlAppend=function(url,paramStr){var newUrl=url;if(paramStr){var parts=(url+" ").split(/[?&]/);newUrl+=(parts.pop()===" "?paramStr:parts.length?"&"+paramStr:"?"+paramStr);}
+return newUrl;};OpenLayers.Util.getImagesLocation=function(){return OpenLayers.ImgPath||(OpenLayers._getScriptLocation()+"img/");};OpenLayers.Util.getImageLocation=function(image){return OpenLayers.Util.getImagesLocation()+image;};OpenLayers.Util.Try=function(){var returnValue=null;for(var i=0,len=arguments.length;i<len;i++){var lambda=arguments[i];try{returnValue=lambda();break;}catch(e){}}
+return returnValue;};OpenLayers.Util.getXmlNodeValue=function(node){var val=null;OpenLayers.Util.Try(function(){val=node.text;if(!val){val=node.textContent;}
+if(!val){val=node.firstChild.nodeValue;}},function(){val=node.textContent;});return val;};OpenLayers.Util.mouseLeft=function(evt,div){var target=(evt.relatedTarget)?evt.relatedTarget:evt.toElement;while(target!=div&&target!=null){target=target.parentNode;}
+return(target!=div);};OpenLayers.Util.DEFAULT_PRECISION=14;OpenLayers.Util.toFloat=function(number,precision){if(precision==null){precision=OpenLayers.Util.DEFAULT_PRECISION;}
+if(typeof number!=="number"){number=parseFloat(number);}
+return precision===0?number:parseFloat(number.toPrecision(precision));};OpenLayers.Util.rad=function(x){return x*Math.PI/180;};OpenLayers.Util.deg=function(x){return x*180/Math.PI;};OpenLayers.Util.VincentyConstants={a:6378137,b:6356752.3142,f:1/298.257223563};OpenLayers.Util.distVincenty=function(p1,p2){var ct=OpenLayers.Util.VincentyConstants;var a=ct.a,b=ct.b,f=ct.f;var L=OpenLayers.Util.rad(p2.lon-p1.lon);var U1=Math.atan((1-f)*Math.tan(OpenLayers.Util.rad(p1.lat)));var U2=Math.atan( [...]
+(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=t [...]
+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.g [...]
+var parameters={};var pairs=paramsString.split(/[&;]/);for(var i=0,len=pairs.length;i<len;++i){var keyValue=pairs[i].split('=');if(keyValue[0]){var key=keyValue[0];try{key=decodeURIComponent(key);}catch(err){key=unescape(key);}
+var value=(keyValue[1]||'').replace(/\+/g," ");try{value=decodeURIComponent(value);}catch(err){value=unescape(value);}
+value=value.split(",");if(value.length==1){value=value[0];}
+parameters[key]=value;}}
+return parameters;};OpenLayers.Util.lastSeqID=0;OpenLayers.Util.createUniqueID=function(prefix){if(prefix==null){prefix="id_";}
+OpenLayers.Util.lastSeqID+=1;return prefix+OpenLayers.Util.lastSeqID;};OpenLayers.INCHES_PER_UNIT={'inches':1.0,'ft':12.0,'mi':63360.0,'m':39.3701,'km':39370.1,'dd':4374754,'yd':36};OpenLayers.INCHES_PER_UNIT["in"]=OpenLayers.INCHES_PER_UNIT.inches;OpenLayers.INCHES_PER_UNIT["degrees"]=OpenLayers.INCHES_PER_UNIT.dd;OpenLayers.INCHES_PER_UNIT["nmi"]=1852*OpenLayers.INCHES_PER_UNIT.m;OpenLayers.METERS_PER_INCH=0.02540005080010160020;OpenLayers.Util.extend(OpenLayers.INCHES_PER_UNIT,{"Inch" [...]
+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=do [...]
+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")!=- [...]
+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 [...]
+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<l;i++){if(!content.childNodes[i].style)continue;content.childNodes[i].style.overflow="visible";}}
+container.appendChild(content);if(superContainer){containerElement.appendChild(superContainer);}else{containerElement.appendChild(container);}
+if(!w){w=parseInt(content.scrollWidth);container.style.width=w+"px";}
+if(!h){h=parseInt(content.scrollHeight);}
+container.removeChild(content);if(superContainer){superContainer.removeChild(container);containerElement.removeChild(superContainer);}else{containerElement.removeChild(container);}
+return new OpenLayers.Size(w,h);};OpenLayers.Util.getScrollbarWidth=function(){var scrollbarWidth=OpenLayers.Util._scrollbarWidth;if(scrollbarWidth==null){var scr=null;var inn=null;var wNoScroll=0;var wScroll=0;scr=document.createElement('div');scr.style.position='absolute';scr.style.top='-1000px';scr.style.left='-1000px';scr.style.width='100px';scr.style.height='50px';scr.style.overflow='hidden';inn=document.createElement('div');inn.style.width='100%';inn.style.height='200px';scr.append [...]
+return scrollbarWidth;};OpenLayers.Util.getFormattedLonLat=function(coordinate,axis,dmsOption){if(!dmsOption){dmsOption='dms';}
+coordinate=(coordinate+540)%360-180;var abscoordinate=Math.abs(coordinate);var coordinatedegrees=Math.floor(abscoordinate);var coordinateminutes=(abscoordinate-coordinatedegrees)/(1/60);var tempcoordinateminutes=coordinateminutes;coordinateminutes=Math.floor(coordinateminutes);var coordinateseconds=(tempcoordinateminutes-coordinateminutes)/(1/60);coordinateseconds=Math.round(coordinateseconds*10);coordinateseconds/=10;if(coordinateseconds>=60){coordinateseconds-=60;coordinateminutes+=1;i [...]
+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== [...]
+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(e [...]
+var foundEntry=false;var elementObservers=OpenLayers.Event.observers[cacheID];if(elementObservers){var i=0;while(!foundEntry&&i<elementObservers.length){var cacheEntry=elementObservers[i];if((cacheEntry.name==name)&&(cacheEntry.observer==observer)&&(cacheEntry.useCapture==useCapture)){elementObservers.splice(i,1);if(elementObservers.length==0){delete OpenLayers.Event.observers[cacheID];}
+foundEntry=true;break;}
+i++;}}
+if(foundEntry){if(element.removeEventListener){element.removeEventListener(name,observer,useCapture);}else if(element&&element.detachEvent){element.detachEvent('on'+name,observer);}}
+return foundEntry;},unloadCache:function(){if(OpenLayers.Event&&OpenLayers.Event.observers){for(var cacheID in OpenLayers.Event.observers){var elementObservers=OpenLayers.Event.observers[cacheID];OpenLayers.Event._removeElementObservers.apply(this,[elementObservers]);}
+OpenLayers.Event.observers=false;}},CLASS_NAME:"OpenLayers.Event"};OpenLayers.Event.observe(window,'unload',OpenLayers.Event.unloadCache,false);OpenLayers.Events=OpenLayers.Class({BROWSER_EVENTS:["mouseover","mouseout","mousedown","mouseup","mousemove","click","dblclick","rightclick","dblrightclick","resize","focus","blur","touchstart","touchmove","touchend","keydown"],listeners:null,object:null,element:null,eventHandler:null,fallThrough:null,includeXY:false,extensions:null,extensionCoun [...]
+this.extensions=null;if(this.element){OpenLayers.Event.stopObservingElement(this.element);if(this.element.hasScrollEvent){OpenLayers.Event.stopObserving(window,"scroll",this.clearMouseListener);}}
+this.element=null;this.listeners=null;this.object=null;this.fallThrough=null;this.eventHandler=null;},addEventType:function(eventName){},attachToElement:function(element){if(this.element){OpenLayers.Event.stopObservingElement(this.element);}else{this.eventHandler=OpenLayers.Function.bindAsEventListener(this.handleBrowserEvent,this);this.clearMouseListener=OpenLayers.Function.bind(this.clearMouseCache,this);}
+this.element=element;for(var i=0,len=this.BROWSER_EVENTS.length;i<len;i++){OpenLayers.Event.observe(element,this.BROWSER_EVENTS[i],this.eventHandler);}
+OpenLayers.Event.observe(element,"dragstart",OpenLayers.Event.stop);},on:function(object){for(var type in object){if(type!="scope"&&object.hasOwnProperty(type)){this.register(type,object.scope,object[type]);}}},register:function(type,obj,func,priority){if(type in OpenLayers.Events&&!this.extensions[type]){this.extensions[type]=new OpenLayers.Events[type](this);}
+if(func!=null){if(obj==null){obj=this.object;}
+var listeners=this.listeners[type];if(!listeners){listeners=[];this.listeners[type]=listeners;this.extensionCount[type]=0;}
+var listener={obj:obj,func:func};if(priority){listeners.splice(this.extensionCount[type],0,listener);if(typeof priority==="object"&&priority.extension){this.extensionCount[type]++;}}else{listeners.push(listener);}}},registerPriority:function(type,obj,func){this.register(type,obj,func,true);},un:function(object){for(var type in object){if(type!="scope"&&object.hasOwnProperty(type)){this.unregister(type,object.scope,object[type]);}}},unregister:function(type,obj,func){if(obj==null){obj=thi [...]
+var listeners=this.listeners[type];if(listeners!=null){for(var i=0,len=listeners.length;i<len;i++){if(listeners[i].obj==obj&&listeners[i].func==func){listeners.splice(i,1);break;}}}},remove:function(type){if(this.listeners[type]!=null){this.listeners[type]=[];}},triggerEvent:function(type,evt){var listeners=this.listeners[type];if(!listeners||listeners.length==0){return undefined;}
+if(evt==null){evt={};}
+evt.object=this.object;evt.element=this.element;if(!evt.type){evt.type=type;}
+listeners=listeners.slice();var continueChain;for(var i=0,len=listeners.length;i<len;i++){var callback=listeners[i];continueChain=callback.func.apply(callback.obj,[evt]);if((continueChain!=undefined)&&(continueChain==false)){break;}}
+if(!this.fallThrough){OpenLayers.Event.stop(evt,true);}
+return continueChain;},handleBrowserEvent:function(evt){var type=evt.type,listeners=this.listeners[type];if(!listeners||listeners.length==0){return;}
+var touches=evt.touches;if(touches&&touches[0]){var x=0;var y=0;var num=touches.length;var touch;for(var i=0;i<num;++i){touch=touches[i];x+=touch.clientX;y+=touch.clientY;}
+evt.clientX=x/num;evt.clientY=y/num;}
+if(this.includeXY){evt.xy=this.getMousePosition(evt);}
+this.triggerEvent(type,evt);},clearMouseCache:function(){this.element.scrolls=null;this.element.lefttop=null;var body=document.body;if(body&&!((body.scrollTop!=0||body.scrollLeft!=0)&&navigator.userAgent.match(/iPhone/i))){this.element.offsets=null;}},getMousePosition:function(evt){if(!this.includeXY){this.clearMouseCache();}else if(!this.element.hasScrollEvent){OpenLayers.Event.observe(window,"scroll",this.clearMouseListener);this.element.hasScrollEvent=true;}
+if(!this.element.scrolls){var viewportElement=OpenLayers.Util.getViewportElement();this.element.scrolls=[viewportElement.scrollLeft,viewportElement.scrollTop];}
+if(!this.element.lefttop){this.element.lefttop=[(document.documentElement.clientLeft||0),(document.documentElement.clientTop||0)];}
+if(!this.element.offsets){this.element.offsets=OpenLayers.Util.pagePosition(this.element);}
+return new OpenLayers.Pixel((evt.clientX+this.element.scrolls[0])-this.element.offsets[0]
+-this.element.lefttop[0],(evt.clientY+this.element.scrolls[1])-this.element.offsets[1]
+-this.element.lefttop[1]);},CLASS_NAME:"OpenLayers.Events"});OpenLayers.Projection=OpenLayers.Class({proj:null,projCode:null,titleRegEx:/\+title=[^\+]*/,initialize:function(projCode,options){OpenLayers.Util.extend(this,options);this.projCode=projCode;if(window.Proj4js){this.proj=new Proj4js.Proj(projCode);}},getCode:function(){return this.proj?this.proj.srsCode:this.projCode;},getUnits:function(){return this.proj?this.proj.units:null;},toString:function(){return this.getCode();},equals:f [...]
+if(window.Proj4js&&this.proj.defData&&p.proj.defData){equals=this.proj.defData.replace(this.titleRegEx,"")==p.proj.defData.replace(this.titleRegEx,"");}else if(p.getCode){var source=this.getCode(),target=p.getCode();equals=source==target||!!OpenLayers.Projection.transforms[source]&&OpenLayers.Projection.transforms[source][target]===OpenLayers.Projection.nullTransform;}}
+return equals;},destroy:function(){delete this.proj;delete this.projCode;},CLASS_NAME:"OpenLayers.Projection"});OpenLayers.Projection.transforms={};OpenLayers.Projection.defaults={"EPSG:4326":{units:"degrees",maxExtent:[-180,-90,180,90],yx:true},"CRS:84":{units:"degrees",maxExtent:[-180,-90,180,90]},"EPSG:900913":{units:"m",maxExtent:[-20037508.34,-20037508.34,20037508.34,20037508.34]}};OpenLayers.Projection.addTransform=function(from,to,method){if(method===OpenLayers.Projection.nullTran [...]
+if(!OpenLayers.Projection.transforms[from]){OpenLayers.Projection.transforms[from]={};}
+OpenLayers.Projection.transforms[from][to]=method;};OpenLayers.Projection.transform=function(point,source,dest){if(source&&dest){if(!(source instanceof OpenLayers.Projection)){source=new OpenLayers.Projection(source);}
+if(!(dest instanceof OpenLayers.Projection)){dest=new OpenLayers.Projection(dest);}
+if(source.proj&&dest.proj){point=Proj4js.transform(source.proj,dest.proj,point);}else{var sourceCode=source.getCode();var destCode=dest.getCode();var transforms=OpenLayers.Projection.transforms;if(transforms[sourceCode]&&transforms[sourceCode][destCode]){transforms[sourceCode][destCode](point);}}}
+return point;};OpenLayers.Projection.nullTransform=function(point){return point;};(function(){var pole=20037508.34;function inverseMercator(xy){xy.x=180*xy.x/pole;xy.y=180/Math.PI*(2*Math.atan(Math.exp((xy.y/pole)*Math.PI))-Math.PI/2);return xy;}
+function forwardMercator(xy){xy.x=xy.x*pole/180;xy.y=Math.log(Math.tan((90+xy.y)*Math.PI/360))/Math.PI*pole;return xy;}
+function map(base,codes){var add=OpenLayers.Projection.addTransform;var same=OpenLayers.Projection.nullTransform;var i,len,code,other,j;for(i=0,len=codes.length;i<len;++i){code=codes[i];add(base,code,forwardMercator);add(code,base,inverseMercator);for(j=i+1;j<len;++j){other=codes[j];add(code,other,same);add(other,code,same);}}}
+var mercator=["EPSG:900913","EPSG:3857","EPSG:102113","EPSG:102100"],geographic=["CRS:84","urn:ogc:def:crs:EPSG:6.6:4326","EPSG:4326"],i;for(i=mercator.length-1;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",uni [...]
+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.maxEx [...]
+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.U [...]
+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<len;++i){if(OpenLayers.Util.isEquivalentUrl(nodes.item(i).href,this.theme)){addNode=false;break;}}
+if(addNode){var cssNode=document.createElement('link');cssNode.setAttribute('rel','stylesheet');cssNode.setAttribute('type','text/css');cssNode.setAttribute('href',this.theme);document.getElementsByTagName('head')[0].appendChild(cssNode);}}
+if(this.controls==null){this.controls=[];if(OpenLayers.Control!=null){if(OpenLayers.Control.Navigation){this.controls.push(new OpenLayers.Control.Navigation());}else if(OpenLayers.Control.TouchNavigation){this.controls.push(new OpenLayers.Control.TouchNavigation());}
+if(OpenLayers.Control.Zoom){this.controls.push(new OpenLayers.Control.Zoom());}else if(OpenLayers.Control.PanZoom){this.controls.push(new OpenLayers.Control.PanZoom());}
+if(OpenLayers.Control.ArgParser){this.controls.push(new OpenLayers.Control.ArgParser());}
+if(OpenLayers.Control.Attribution){this.controls.push(new OpenLayers.Control.Attribution());}}}
+for(var i=0,len=this.controls.length;i<len;i++){this.addControlToMap(this.controls[i]);}
+this.popups=[];this.unloadDestroy=OpenLayers.Function.bind(this.destroy,this);OpenLayers.Event.observe(window,'unload',this.unloadDestroy);if(options&&options.layers){delete this.center;this.addLayers(options.layers);if(options.center&&!this.getCenter()){this.setCenter(options.center,options.zoom);}}},getViewport:function(){return this.viewPortDiv;},render:function(div){this.div=OpenLayers.Util.getElement(div);OpenLayers.Element.addClass(this.div,'olMap');this.viewPortDiv.parentNode.remo [...]
+if(this.panTween){this.panTween.stop();this.panTween=null;}
+OpenLayers.Event.stopObserving(window,'unload',this.unloadDestroy);this.unloadDestroy=null;if(this.updateSizeDestroy){OpenLayers.Event.stopObserving(window,'resize',this.updateSizeDestroy);}else{this.events.unregister("resize",this,this.updateSize);}
+this.paddingForPopups=null;if(this.controls!=null){for(var i=this.controls.length-1;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[prop [...]
+return foundLayer;},setLayerZIndex:function(layer,zIdx){layer.setZIndex(this.Z_INDEX_BASE[layer.isBaseLayer?'BaseLayer':'Overlay']
++zIdx*5);},resetLayersZIndex:function(){for(var i=0,len=this.layers.length;i<len;i++){var layer=this.layers[i];this.setLayerZIndex(layer,i);}},addLayer:function(layer){for(var i=0,len=this.layers.length;i<len;i++){if(this.layers[i]==layer){return false;}}
+if(this.events.triggerEvent("preaddlayer",{layer:layer})===false){return false;}
+if(this.allOverlays){layer.isBaseLayer=false;}
+layer.div.className="olLayerDiv";layer.div.style.overflow="";this.setLayerZIndex(layer,this.layers.length);if(layer.isFixed){this.viewPortDiv.appendChild(layer.div);}else{this.layerContainerDiv.appendChild(layer.div);}
+this.layers.push(layer);layer.setMap(this);if(layer.isBaseLayer||(this.allOverlays&&!this.baseLayer)){if(this.baseLayer==null){this.setBaseLayer(layer);}else{layer.setVisibility(false);}}else{layer.redraw();}
+this.events.triggerEvent("addlayer",{layer:layer});layer.events.triggerEvent("added",{map:this,layer:layer});layer.afterAdd();return true;},addLayers:function(layers){for(var i=0,len=layers.length;i<len;i++){this.addLayer(layers[i]);}},removeLayer:function(layer,setNewBaseLayer){if(this.events.triggerEvent("preremovelayer",{layer:layer})===false){return;}
+if(setNewBaseLayer==null){setNewBaseLayer=true;}
+if(layer.isFixed){this.viewPortDiv.removeChild(layer.div);}else{this.layerContainerDiv.removeChild(layer.div);}
+OpenLayers.Util.removeItem(this.layers,layer);layer.removeMap(this);layer.map=null;if(this.baseLayer==layer){this.baseLayer=null;if(setNewBaseLayer){for(var i=0,len=this.layers.length;i<len;i++){var iLayer=this.layers[i];if(iLayer.isBaseLayer||this.allOverlays){this.setBaseLayer(iLayer);break;}}}}
+this.resetLayersZIndex();this.events.triggerEvent("removelayer",{layer:layer});layer.events.triggerEvent("removed",{map:this,layer:layer});},getNumLayers:function(){return this.layers.length;},getLayerIndex:function(layer){return OpenLayers.Util.indexOf(this.layers,layer);},setLayerIndex:function(layer,idx){var base=this.getLayerIndex(layer);if(idx<0){idx=0;}else if(idx>this.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<len;i++){this.setLayerZIndex(this.layers[i],i);}
+this.events.triggerEvent("changelayer",{layer:layer,property:"order"});if(this.allOverlays){if(idx===0){this.setBaseLayer(layer);}else if(this.baseLayer!==this.layers[0]){this.setBaseLayer(this.layers[0]);}}}},raiseLayer:function(layer,delta){var idx=this.getLayerIndex(layer)+delta;this.setLayerIndex(layer,idx);},setBaseLayer:function(newBaseLayer){if(newBaseLayer!=this.baseLayer){if(OpenLayers.Util.indexOf(this.layers,newBaseLayer)!=-1){var center=this.getCachedCenter();var newResolutio [...]
+this.baseLayer=newBaseLayer;if(!this.allOverlays||this.baseLayer.visibility){this.baseLayer.setVisibility(true);if(this.baseLayer.inRange===false){this.baseLayer.redraw();}}
+if(center!=null){var newZoom=this.getZoomForResolution(newResolution||this.resolution,true);this.setCenter(center,newZoom,false,true);}
+this.events.triggerEvent("changebaselayer",{layer:this.baseLayer});}}},addControl:function(control,px){this.controls.push(control);this.addControlToMap(control,px);},addControls:function(controls,pixels){var pxs=(arguments.length===1)?[]:pixels;for(var i=0,len=controls.length;i<len;i++){var ctrl=controls[i];var px=(pxs[i])?pxs[i]:null;this.addControl(ctrl,px);}},addControlToMap:function(control,px){control.outsideViewport=(control.div!=null);if(this.displayProjection&&!control.displayPro [...]
+control.setMap(this);var div=control.draw(px);if(div){if(!control.outsideViewport){div.style.zIndex=this.Z_INDEX_BASE['Control']+
+this.controls.length;this.viewPortDiv.appendChild(div);}}
+if(control.autoActivate){control.activate();}},getControl:function(id){var returnControl=null;for(var i=0,len=this.controls.length;i<len;i++){var control=this.controls[i];if(control.id==id){returnControl=control;break;}}
+return returnControl;},removeControl:function(control){if((control)&&(control==this.getControl(control.id))){if(control.div&&(control.div.parentNode==this.viewPortDiv)){this.viewPortDiv.removeChild(control.div);}
+OpenLayers.Util.removeItem(this.controls,control);}},addPopup:function(popup,exclusive){if(exclusive){for(var i=this.popups.length-1;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<len;i++){this.layers[i].onMapResize();}
+var center=this.getCachedCenter();if(this.baseLayer!=null&&center!=null){var zoom=this.getZoom();this.zoom=null;this.setCenter(center,zoom);}}}},getCurrentSize:function(){var size=new OpenLayers.Size(this.div.clientWidth,this.div.clientHeight);if(size.w==0&&size.h==0||isNaN(size.w)&&isNaN(size.h)){size.w=this.div.offsetWidth;size.h=this.div.offsetHeight;}
+if(size.w==0&&size.h==0||isNaN(size.w)&&isNaN(size.h)){size.w=parseInt(this.div.style.width);size.h=parseInt(this.div.style.height);}
+return size;},calculateBounds:function(center,resolution){var extent=null;if(center==null){center=this.getCachedCenter();}
+if(resolution==null){resolution=this.getResolution();}
+if((center!=null)&&(resolution!=null)){var halfWDeg=(this.size.w*resolution)/2;var halfHDeg=(this.size.h*resolution)/2;extent=new OpenLayers.Bounds(center.lon-halfWDeg,center.lat-halfHDeg,center.lon+halfWDeg,center.lat+halfHDeg);}
+return extent;},getCenter:function(){var center=null;var cachedCenter=this.getCachedCenter();if(cachedCenter){center=cachedCenter.clone();}
+return center;},getCachedCenter:function(){if(!this.center&&this.size){this.center=this.getLonLatFromViewPortPx({x:this.size.w/2,y:this.size.h/2});}
+return this.center;},getZoom:function(){return this.zoom;},pan:function(dx,dy,options){options=OpenLayers.Util.applyDefaults(options,{animate:true,dragging:false});if(options.dragging){if(dx!=0||dy!=0){this.moveByPx(dx,dy);}}else{var centerPx=this.getViewPortPxFromLonLat(this.getCachedCenter());var newCenterPx=centerPx.add(dx,dy);if(this.dragging||!newCenterPx.equals(centerPx)){var newCenterLonLat=this.getLonLatFromViewPortPx(newCenterPx);if(options.animate){this.panTo(newCenterLonLat);} [...]
+var center=this.getCachedCenter();if(lonlat.equals(center)){return;}
+var from=this.getPixelFromLonLat(center);var to=this.getPixelFromLonLat(lonlat);var vector={x:to.x-from.x,y:to.y-from.y};var last={x:0,y:0};this.panTween.start({x:0,y:0},vector,this.panDuration,{callbacks:{eachStep:OpenLayers.Function.bind(function(px){var x=px.x-last.x,y=px.y-last.y;this.moveByPx(x,y);last.x=Math.round(px.x);last.y=Math.round(px.y);},this),done:OpenLayers.Function.bind(function(px){this.moveTo(lonlat);this.dragging=false;this.events.triggerEvent("moveend");},this)}});}e [...]
+dx=wrapDateLine||x<=this.maxPx.x-xRestriction&&x>=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;i<len;++i){layer=this.layers[i];if(layer.visibility&&(layer===this.baseLayer||layer.inRange)){layer.moveByPx(dx,dy);layer.events.triggerEvent("move");}}
+this.events.triggerEvent("move");}},adjustZoom:function(zoom){var resolution,resolutions=this.baseLayer.resolutions,maxResolution=this.getMaxExtent().getWidth()/this.size.w;if(this.getResolutionForZoom(zoom)>maxResolution){for(var i=zoom|0,ii=resolutions.length;i<ii;++i){if(resolutions[i]<=maxResolution){zoom=i;break;}}}
+return zoom;},moveTo:function(lonlat,zoom,options){if(lonlat!=null&&!(lonlat instanceof OpenLayers.LonLat)){lonlat=new OpenLayers.LonLat(lonlat);}
+if(!options){options={};}
+if(zoom!=null){zoom=parseFloat(zoom);if(!this.fractionalZoom){zoom=Math.round(zoom);}}
+if(this.baseLayer.wrapDateLine){var requestedZoom=zoom;zoom=this.adjustZoom(zoom);if(zoom!==requestedZoom){lonlat=this.getCenter();}}
+var dragging=options.dragging||this.dragging;var forceZoomChange=options.forceZoomChange;if(!this.getCachedCenter()&&!this.isValidLonLat(lonlat)){lonlat=this.maxExtent.getCenterLonLat();this.center=lonlat.clone();}
+if(this.restrictedExtent!=null){if(lonlat==null){lonlat=this.center;}
+if(zoom==null){zoom=this.getZoom();}
+var resolution=this.getResolutionForZoom(zoom);var extent=this.calculateBounds(lonlat,resolution);if(!this.restrictedExtent.containsBounds(extent)){var maxCenter=this.restrictedExtent.getCenterLonLat();if(extent.getWidth()>this.restrictedExtent.getWidth()){lonlat=new OpenLayers.LonLat(maxCenter.lon,lonlat.lat);}else if(extent.left<this.restrictedExtent.left){lonlat=lonlat.add(this.restrictedExtent.left-
+extent.left,0);}else if(extent.right>this.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.bottom<this.restrictedExtent.bottom){lonlat=lonlat.add(0,this.restrictedExtent.bottom-
+extent.bottom);}
+else if(extent.top>this.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 [...]
+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<len;i++){this.popups[i].updatePosition();}
+this.events.triggerEvent("zoomend");}}},centerLayerContainer:function(lonlat){var originPx=this.getViewPortPxFromLonLat(this.layerContainerOrigin);var newPx=this.getViewPortPxFromLonLat(lonlat);if((originPx!=null)&&(newPx!=null)){var oldLeft=parseInt(this.layerContainerDiv.style.left);var oldTop=parseInt(this.layerContainerDiv.style.top);var newLeft=Math.round(originPx.x-newPx.x);var newTop=Math.round(originPx.y-newPx.y);this.layerContainerDiv.style.left=newLeft+"px";this.layerContainerD [...]
+return valid;},getProjection:function(){var projection=this.getProjectionObject();return projection?projection.getCode():null;},getProjectionObject:function(){var projection=null;if(this.baseLayer!=null){projection=this.baseLayer.projection;}
+return projection;},getMaxResolution:function(){var maxResolution=null;if(this.baseLayer!=null){maxResolution=this.baseLayer.maxResolution;}
+return maxResolution;},getMaxExtent:function(options){var maxExtent=null;if(options&&options.restricted&&this.restrictedExtent){maxExtent=this.restrictedExtent;}else if(this.baseLayer!=null){maxExtent=this.baseLayer.maxExtent;}
+return maxExtent;},getNumZoomLevels:function(){var numZoomLevels=null;if(this.baseLayer!=null){numZoomLevels=this.baseLayer.numZoomLevels;}
+return numZoomLevels;},getExtent:function(){var extent=null;if(this.baseLayer!=null){extent=this.baseLayer.getExtent();}
+return extent;},getResolution:function(){var resolution=null;if(this.baseLayer!=null){resolution=this.baseLayer.getResolution();}else if(this.allOverlays===true&&this.layers.length>0){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<bounds.left){bounds.right+=maxExtent.getWidth();}
+center=bounds.getCenterLonLat().wrapDateLine(maxExtent);}
+this.setCenter(center,this.getZoomForExtent(bounds,closest));},zoomToMaxExtent:function(options){var restricted=(options)?options.restricted:true;var maxExtent=this.getMaxExtent({'restricted':restricted});this.zoomToExtent(maxExtent);},zoomToScale:function(scale,closest){var res=OpenLayers.Util.getResolutionFromScale(scale,this.baseLayer.units);var halfWDeg=(this.size.w*res)/2;var halfHDeg=(this.size.h*res)/2;var center=this.getCachedCenter();var extent=new OpenLayers.Bounds(center.lon-h [...]
+return lonlat;},getViewPortPxFromLonLat:function(lonlat){var px=null;if(this.baseLayer!=null){px=this.baseLayer.getViewPortPxFromLonLat(lonlat);}
+return px;},getLonLatFromPixel:function(px){return this.getLonLatFromViewPortPx(px);},getPixelFromLonLat:function(lonlat){var px=this.getViewPortPxFromLonLat(lonlat);px.x=Math.round(px.x);px.y=Math.round(px.y);return px;},getGeodesicPixelSize:function(px){var lonlat=px?this.getLonLatFromPixel(px):(this.getCachedCenter()||new OpenLayers.LonLat(0,0));var res=this.getResolution();var left=lonlat.add(-res/2,0);var right=lonlat.add(res/2,0);var bottom=lonlat.add(0,-res/2);var top=lonlat.add(0 [...]
+return new OpenLayers.Size(OpenLayers.Util.distVincenty(left,right),OpenLayers.Util.distVincenty(bottom,top));},getViewPortPxFromLayerPx:function(layerPx){var viewPortPx=null;if(layerPx!=null){var dX=parseInt(this.layerContainerDiv.style.left);var dY=parseInt(this.layerContainerDiv.style.top);viewPortPx=layerPx.add(dX,dY);}
+return viewPortPx;},getLayerPxFromViewPortPx:function(viewPortPx){var layerPx=null;if(viewPortPx!=null){var dX=-parseInt(this.layerContainerDiv.style.left);var dY=-parseInt(this.layerContainerDiv.style.top);layerPx=viewPortPx.add(dX,dY);if(isNaN(layerPx.x)||isNaN(layerPx.y)){layerPx=null;}}
+return layerPx;},getLonLatFromLayerPx:function(px){px=this.getViewPortPxFromLayerPx(px);return this.getLonLatFromViewPortPx(px);},getLayerPxFromLonLat:function(lonlat){var px=this.getPixelFromLonLat(lonlat);return this.getLayerPxFromViewPortPx(px);},CLASS_NAME:"OpenLayers.Map"});OpenLayers.Map.TILE_WIDTH=256;OpenLayers.Map.TILE_HEIGHT=256;OpenLayers.Layer=OpenLayers.Class({id:null,name:null,div:null,opacity:1,alwaysInRange:null,RESOLUTION_PROPERTIES:['scales','resolutions','maxScale','mi [...]
+if(this.map!=null){this.map.removeLayer(this,setNewBaseLayer);}
+this.projection=null;this.map=null;this.name=null;this.div=null;this.options=null;if(this.events){if(this.eventListeners){this.events.un(this.eventListeners);}
+this.events.destroy();}
+this.eventListeners=null;this.events=null;},clone:function(obj){if(obj==null){obj=new OpenLayers.Layer(this.name,this.getOptions());}
+OpenLayers.Util.applyDefaults(obj,this);obj.map=null;return obj;},getOptions:function(){var options={};for(var o in this.options){options[o]=this[o];}
+return options;},setName:function(newName){if(newName!=this.name){this.name=newName;if(this.map!=null){this.map.events.triggerEvent("changelayer",{layer:this,property:"name"});}}},addOptions:function(newOptions,reinitialize){if(this.options==null){this.options={};}
+if(newOptions){if(typeof newOptions.projection=="string"){newOptions.projection=new OpenLayers.Projection(newOptions.projection);}
+if(newOptions.projection){OpenLayers.Util.applyDefaults(newOptions,OpenLayers.Projection.defaults[newOptions.projection.getCode()]);}
+if(newOptions.maxExtent&&!(newOptions.maxExtent instanceof OpenLayers.Bounds)){newOptions.maxExtent=new OpenLayers.Bounds(newOptions.maxExtent);}
+if(newOptions.minExtent&&!(newOptions.minExtent instanceof OpenLayers.Bounds)){newOptions.minExtent=new OpenLayers.Bounds(newOptions.minExtent);}}
+OpenLayers.Util.extend(this.options,newOptions);OpenLayers.Util.extend(this,newOptions);if(this.projection&&this.projection.getUnits()){this.units=this.projection.getUnits();}
+if(this.map){var resolution=this.map.getResolution();var properties=this.RESOLUTION_PROPERTIES.concat(["projection","units","minExtent","maxExtent"]);for(var o in newOptions){if(newOptions.hasOwnProperty(o)&&OpenLayers.Util.indexOf(properties,o)>=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.visibili [...]
+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<len;i++){p=this.RESOLUTION_PROPERTIES[i];props[p]=this.options[p];if(alwaysInRange&&this.options[p]){alwaysInRange=false;}}
+if(this.alwaysInRange==null){this.alwaysInRange=alwaysInRange;}
+if(props.resolutions==null){props.resolutions=this.resolutionsFromScales(props.scales);}
+if(props.resolutions==null){props.resolutions=this.calculateResolutions(props);}
+if(props.resolutions==null){for(i=0,len=this.RESOLUTION_PROPERTIES.length;i<len;i++){p=this.RESOLUTION_PROPERTIES[i];props[p]=this.options[p]!=null?this.options[p]:this.map[p];}
+if(props.resolutions==null){props.resolutions=this.resolutionsFromScales(props.scales);}
+if(props.resolutions==null){props.resolutions=this.calculateResolutions(props);}}
+var maxResolution;if(this.options.maxResolution&&this.options.maxResolution!=="auto"){maxResolution=this.options.maxResolution;}
+if(this.options.minScale){maxResolution=OpenLayers.Util.getResolutionFromScale(this.options.minScale,this.units);}
+var minResolution;if(this.options.minResolution&&this.options.minResolution!=="auto"){minResolution=this.options.minResolution;}
+if(this.options.maxScale){minResolution=OpenLayers.Util.getResolutionFromScale(this.options.maxScale,this.units);}
+if(props.resolutions){props.resolutions.sort(function(a,b){return(b-a);});if(!maxResolution){maxResolution=props.resolutions[0];}
+if(!minResolution){var lastIdx=props.resolutions.length-1;minResolution=props.resolutions[lastIdx];}}
+this.resolutions=props.resolutions;if(this.resolutions){len=this.resolutions.length;this.scales=new Array(len);for(i=0;i<len;i++){this.scales[i]=OpenLayers.Util.getScaleFromResolution(this.resolutions[i],this.units);}
+this.numZoomLevels=len;}
+this.minResolution=minResolution;if(minResolution){this.maxScale=OpenLayers.Util.getScaleFromResolution(minResolution,this.units);}
+this.maxResolution=maxResolution;if(maxResolution){this.minScale=OpenLayers.Util.getScaleFromResolution(maxResolution,this.units);}},resolutionsFromScales:function(scales){if(scales==null){return;}
+var resolutions,i,len;len=scales.length;resolutions=new Array(len);for(i=0;i<len;i++){resolutions[i]=OpenLayers.Util.getResolutionFromScale(scales[i],this.units);}
+return resolutions;},calculateResolutions:function(props){var viewSize,wRes,hRes;var maxResolution=props.maxResolution;if(props.minScale!=null){maxResolution=OpenLayers.Util.getResolutionFromScale(props.minScale,this.units);}else if(maxResolution=="auto"&&this.maxExtent!=null){viewSize=this.map.getSize();wRes=this.maxExtent.getWidth()/viewSize.w;hRes=this.maxExtent.getHeight()/viewSize.h;maxResolution=Math.max(wRes,hRes);}
+var minResolution=props.minResolution;if(props.maxScale!=null){minResolution=OpenLayers.Util.getResolutionFromScale(props.maxScale,this.units);}else if(props.minResolution=="auto"&&this.minExtent!=null){viewSize=this.map.getSize();wRes=this.minExtent.getWidth()/viewSize.w;hRes=this.minExtent.getHeight()/viewSize.h;minResolution=Math.max(wRes,hRes);}
+if(typeof maxResolution!=="number"&&typeof minResolution!=="number"&&this.maxExtent!=null){var tileSize=this.map.getTileSize();maxResolution=Math.max(this.maxExtent.getWidth()/tileSize.w,this.maxExtent.getHeight()/tileSize.h);}
+var maxZoomLevel=props.maxZoomLevel;var numZoomLevels=props.numZoomLevels;if(typeof minResolution==="number"&&typeof maxResolution==="number"&&numZoomLevels===undefined){var ratio=maxResolution/minResolution;numZoomLevels=Math.floor(Math.log(ratio)/Math.log(2))+1;}else if(numZoomLevels===undefined&&maxZoomLevel!=null){numZoomLevels=maxZoomLevel+1;}
+if(typeof numZoomLevels!=="number"||numZoomLevels<=0||(typeof maxResolution!=="number"&&typeof minResolution!=="number")){return;}
+var resolutions=new Array(numZoomLevels);var base=2;if(typeof minResolution=="number"&&typeof maxResolution=="number"){base=Math.pow((maxResolution/minResolution),(1/(numZoomLevels-1)));}
+var i;if(typeof maxResolution==="number"){for(i=0;i<numZoomLevels;i++){resolutions[i]=maxResolution/Math.pow(base,i);}}else{for(i=0;i<numZoomLevels;i++){resolutions[numZoomLevels-1-i]=minResolution*Math.pow(base,i);}}
+return resolutions;},getResolution:function(){var zoom=this.map.getZoom();return this.getResolutionForZoom(zoom);},getExtent:function(){return this.map.calculateBounds();},getZoomForExtent:function(extent,closest){var viewSize=this.map.getSize();var idealResolution=Math.max(extent.getWidth()/viewSize.w,extent.getHeight()/viewSize.h);return this.getZoomForResolution(idealResolution,closest);},getDataExtent:function(){},getResolutionForZoom:function(zoom){zoom=Math.max(0,Math.min(zoom,this [...]
+((zoom-low)*(this.resolutions[low]-this.resolutions[high]));}else{resolution=this.resolutions[Math.round(zoom)];}
+return resolution;},getZoomForResolution:function(resolution,closest){var zoom,i,len;if(this.map.fractionalZoom){var lowZoom=0;var highZoom=this.resolutions.length-1;var highRes=this.resolutions[lowZoom];var lowRes=this.resolutions[highZoom];var res;for(i=0,len=this.resolutions.length;i<len;++i){res=this.resolutions[i];if(res>=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;i<len;i++){if(closest){diff=Math.abs(this.resolutions[i]-resolution);if(diff>minDiff){break;}
+minDiff=diff;}else{if(this.resolutions[i]<resolution){break;}}}
+zoom=Math.max(0,i-1);}
+return zoom;},getLonLatFromViewPortPx:function(viewPortPx){var lonlat=null;var map=this.map;if(viewPortPx!=null&&map.minPx){var res=map.getResolution();var maxExtent=map.getMaxExtent({restricted:true});var lon=(viewPortPx.x-map.minPx.x)*res+maxExtent.left;var lat=(map.minPx.y-viewPortPx.y)*res+maxExtent.top;lonlat=new OpenLayers.LonLat(lon,lat);if(this.wrapDateLine){lonlat=lonlat.wrapDateLine(this.maxExtent);}}
+return lonlat;},getViewPortPxFromLonLat:function(lonlat,resolution){var px=null;if(lonlat!=null){resolution=resolution||this.map.getResolution();var extent=this.map.calculateBounds(null,resolution);px=new OpenLayers.Pixel((1/resolution*(lonlat.lon-extent.left)),(1/resolution*(extent.top-lonlat.lat)));}
+return px;},setOpacity:function(opacity){if(opacity!=this.opacity){this.opacity=opacity;var childNodes=this.div.childNodes;for(var i=0,len=childNodes.length;i<len;++i){var element=childNodes[i].firstChild||childNodes[i];var lastChild=childNodes[i].lastChild;if(lastChild&&lastChild.nodeName.toLowerCase()==="iframe"){element=lastChild.parentNode;}
+OpenLayers.Util.modifyDOMElement(element,null,null,null,null,null,null,opacity);}
+if(this.map!=null){this.map.events.triggerEvent("changelayer",{layer:this,property:"opacity"});}}},getZIndex:function(){return this.div.style.zIndex;},setZIndex:function(zIndex){this.div.style.zIndex=zIndex;},adjustBounds:function(bounds){if(this.gutter){var mapGutter=this.gutter*this.map.getResolution();bounds=new OpenLayers.Bounds(bounds.left-mapGutter,bounds.bottom-mapGutter,bounds.right+mapGutter,bounds.top+mapGutter);}
+if(this.wrapDateLine){var wrappingOptions={'rightTolerance':this.getResolution(),'leftTolerance':this.getResolution()};bounds=bounds.wrapDateLine(this.maxExtent,wrappingOptions);}
+return bounds;},CLASS_NAME:"OpenLayers.Layer"});OpenLayers.Layer.HTTPRequest=OpenLayers.Class(OpenLayers.Layer,{URL_HASH_FACTOR:(Math.sqrt(5)-1)/2,url:null,params:null,reproject:false,initialize:function(name,url,params,options){OpenLayers.Layer.prototype.initialize.apply(this,[name,options]);this.url=url;if(!this.params){this.params=OpenLayers.Util.extend({},params);}},destroy:function(){this.url=null;this.params=null;OpenLayers.Layer.prototype.destroy.apply(this,arguments);},clone:func [...]
+obj=OpenLayers.Layer.prototype.clone.apply(this,[obj]);return obj;},setUrl:function(newUrl){this.url=newUrl;},mergeNewParams:function(newParams){this.params=OpenLayers.Util.extend(this.params,newParams);var ret=this.redraw();if(this.map!=null){this.map.events.triggerEvent("changelayer",{layer:this,property:"params"});}
+return ret;},redraw:function(force){if(force){return this.mergeNewParams({"_olSalt":Math.random()});}else{return OpenLayers.Layer.prototype.redraw.apply(this,[]);}},selectUrl:function(paramString,urls){var product=1;for(var i=0,len=paramString.length;i<len;i++){product*=paramString.charCodeAt(i)*this.URL_HASH_FACTOR;product-=Math.floor(product);}
+return urls[Math.floor(product*urls.length)];},getFullRequestString:function(newParams,altUrl){var url=altUrl||this.url;var allParams=OpenLayers.Util.extend({},this.params);allParams=OpenLayers.Util.extend(allParams,newParams);var paramsString=OpenLayers.Util.getParameterString(allParams);if(OpenLayers.Util.isArray(url)){url=this.selectUrl(paramsString,url);}
+var urlParams=OpenLayers.Util.upperCaseObject(OpenLayers.Util.getParameters(url));for(var key in allParams){if(key.toUpperCase()in urlParams){delete allParams[key];}}
+paramsString=OpenLayers.Util.getParameterString(allParams);return OpenLayers.Util.urlAppend(url,paramsString);},CLASS_NAME:"OpenLayers.Layer.HTTPRequest"});OpenLayers.Tile=OpenLayers.Class({events:null,eventListeners:null,id:null,layer:null,url:null,bounds:null,size:null,position:null,isLoading:false,initialize:function(layer,position,bounds,url,size,options){this.layer=layer;this.position=position.clone();this.setBounds(bounds);this.url=url;if(size){this.size=size.clone();}
+this.id=OpenLayers.Util.createUniqueID("Tile_");OpenLayers.Util.extend(this,options);this.events=new OpenLayers.Events(this);if(this.eventListeners instanceof Object){this.events.on(this.eventListeners);}},unload:function(){if(this.isLoading){this.isLoading=false;this.events.triggerEvent("unload");}},destroy:function(){this.layer=null;this.bounds=null;this.size=null;this.position=null;if(this.eventListeners){this.events.un(this.eventListeners);}
+this.events.destroy();this.eventListeners=null;this.events=null;},draw:function(deferred){if(!deferred){this.clear();}
+var draw=this.shouldDraw();if(draw&&!deferred){draw=this.events.triggerEvent("beforedraw")!==false;}
+return draw;},shouldDraw:function(){var withinMaxExtent=false,maxExtent=this.layer.maxExtent;if(maxExtent){var map=this.layer.map;var worldBounds=map.baseLayer.wrapDateLine&&map.getMaxExtent();if(this.bounds.intersectsBounds(maxExtent,{inclusive:false,worldBounds:worldBounds})){withinMaxExtent=true;}}
+return withinMaxExtent||this.layer.displayOutsideMaxExtent;},setBounds:function(bounds){bounds=bounds.clone();if(this.layer.map.baseLayer.wrapDateLine){var worldExtent=this.layer.map.getMaxExtent(),tolerance=this.layer.map.getResolution();bounds=bounds.wrapDateLine(worldExtent,{leftTolerance:tolerance,rightTolerance:tolerance});}
+this.bounds=bounds;},moveTo:function(bounds,position,redraw){if(redraw==null){redraw=true;}
+this.setBounds(bounds);this.position=position.clone();if(redraw){this.draw();}},clear:function(draw){},CLASS_NAME:"OpenLayers.Tile"});OpenLayers.Tile.Image=OpenLayers.Class(OpenLayers.Tile,{url:null,imgDiv:null,frame:null,imageReloadAttempts:null,layerAlphaHack:null,asyncRequestId:null,blankImageUrl:"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAQAIBRAA7",maxGetUrlLength:null,canvasContext:null,crossOriginKeyword:null,initialize:function(layer,position,bounds,url, [...]
+if(this.maxGetUrlLength!=null){OpenLayers.Util.extend(this,OpenLayers.Tile.Image.IFrame);}},destroy:function(){if(this.imgDiv){this.clear();this.imgDiv=null;this.frame=null;}
+this.asyncRequestId=null;OpenLayers.Tile.prototype.destroy.apply(this,arguments);},draw:function(){var drawn=OpenLayers.Tile.prototype.draw.apply(this,arguments);if(drawn){if(this.layer!=this.layer.map.baseLayer&&this.layer.reproject){this.bounds=this.getBoundsFromBaseLayer(this.position);}
+if(this.isLoading){this._loadEvent="reload";}else{this.isLoading=true;this._loadEvent="loadstart";}
+this.positionTile();this.renderTile();}else{this.unload();}
+return drawn;},renderTile:function(){this.layer.div.appendChild(this.getTile());if(this.layer.async){var id=this.asyncRequestId=(this.asyncRequestId||0)+1;this.layer.getURLasync(this.bounds,function(url){if(id==this.asyncRequestId){this.url=url;this.initImage();}},this);}else{this.url=this.layer.getURL(this.bounds);this.initImage();}},positionTile:function(){var style=this.getTile().style,size=this.frame?this.size:this.layer.getImageSize(this.bounds);style.left=this.position.x+"%";style. [...]
+this.setImgSrc();if(this.layerAlphaHack===true){img.style.filter="";}
+OpenLayers.Element.removeClass(img,"olImageLoadError");}
+this.canvasContext=null;},getImage:function(){if(!this.imgDiv){this.imgDiv=document.createElement("img");this.imgDiv.className="olTileImage";this.imgDiv.galleryImg="no";var style=this.imgDiv.style;if(this.frame){var left=0,top=0;if(this.layer.gutter){left=this.layer.gutter/this.layer.tileSize.w*100;top=this.layer.gutter/this.layer.tileSize.h*100;}
+style.left=-left+"%";style.top=-top+"%";style.width=(2*left+100)+"%";style.height=(2*top+100)+"%";}
+style.visibility="hidden";style.opacity=0;if(this.layer.opacity<1){style.filter='alpha(opacity='+
+(this.layer.opacity*100)+')';}
+style.position="absolute";if(this.layerAlphaHack){style.paddingTop=style.height;style.height="0";style.width="100%";}
+if(this.frame){this.frame.appendChild(this.imgDiv);}}
+return this.imgDiv;},initImage:function(){this.events.triggerEvent(this._loadEvent);var img=this.getImage();if(this.url&&img.getAttribute("src")==this.url){this.onImageLoad();}else{var load=OpenLayers.Function.bind(function(){OpenLayers.Event.stopObservingElement(img);OpenLayers.Event.observe(img,"load",OpenLayers.Function.bind(this.onImageLoad,this));OpenLayers.Event.observe(img,"error",OpenLayers.Function.bind(this.onImageError,this));this.imageReloadAttempts=0;this.setImgSrc(this.url) [...]
+img.src=this.blankImageUrl;}}},setImgSrc:function(url){var img=this.imgDiv;img.style.visibility='hidden';img.style.opacity=0;if(url){if(this.crossOriginKeyword){if(url.substr(0,5)!=='data:'){img.setAttribute("crossorigin",this.crossOriginKeyword);}else{img.removeAttribute("crossorigin");}}
+img.src=url;}},getTile:function(){return this.frame?this.frame:this.getImage();},createBackBuffer:function(){if(!this.imgDiv||this.isLoading){return;}
+var backBuffer;if(this.frame){backBuffer=this.frame.cloneNode(false);backBuffer.appendChild(this.imgDiv);}else{backBuffer=this.imgDiv;}
+this.imgDiv=null;return backBuffer;},onImageLoad:function(){var img=this.imgDiv;OpenLayers.Event.stopObservingElement(img);img.style.visibility='inherit';img.style.opacity=this.layer.opacity;this.isLoading=false;this.canvasContext=null;this.events.triggerEvent("loadend");if(parseFloat(navigator.appVersion.split("MSIE")[1])<7&&this.layer&&this.layer.div){var span=document.createElement("span");span.style.display="none";var layerDiv=this.layer.div;layerDiv.appendChild(span);window.setTimeo [...]
+if(this.layerAlphaHack===true){img.style.filter="progid:DXImageTransform.Microsoft.AlphaImageLoader(src='"+
+img.src+"', sizingMethod='scale')";}},onImageError:function(){var img=this.imgDiv;if(img.src!=null){this.imageReloadAttempts++;if(this.imageReloadAttempts<=OpenLayers.IMAGE_RELOAD_ATTEMPTS){this.setImgSrc(this.layer.getURL(this.bounds));}else{OpenLayers.Element.addClass(img,"olImageLoadError");this.events.triggerEvent("loaderror");this.onImageLoad();}}},getCanvasContext:function(){if(OpenLayers.CANVAS_SUPPORTED&&this.imgDiv&&!this.isLoading){if(!this.canvasContext){var canvas=document.cr [...]
+return this.canvasContext;}},CLASS_NAME:"OpenLayers.Tile.Image"});OpenLayers.Layer.Grid=OpenLayers.Class(OpenLayers.Layer.HTTPRequest,{tileSize:null,tileOriginCorner:"bl",tileOrigin:null,tileOptions:null,tileClass:OpenLayers.Tile.Image,grid:null,singleTile:false,ratio:1.5,buffer:0,transitionEffect:null,numLoadingTiles:0,tileLoadingDelay:85,serverResolutions:null,moveTimerId:null,deferMoveGriddedTiles:null,tileQueueId:null,tileQueue:null,loading:false,backBuffer:null,gridResolution:null,b [...]
+if(this.className===null){this.className=this.singleTile?'olLayerGridSingleTile':'olLayerGrid';}
+if(!OpenLayers.Animation.isNative){this.deferMoveGriddedTiles=OpenLayers.Function.bind(function(){this.moveGriddedTiles(true);this.moveTimerId=null;},this);}},setMap:function(map){OpenLayers.Layer.HTTPRequest.prototype.setMap.call(this,map);OpenLayers.Element.addClass(this.div,this.className);},removeMap:function(map){if(this.moveTimerId!==null){window.clearTimeout(this.moveTimerId);this.moveTimerId=null;}
+this.clearTileQueue();if(this.backBufferTimerId!==null){window.clearTimeout(this.backBufferTimerId);this.backBufferTimerId=null;}},destroy:function(){this.removeBackBuffer();this.clearGrid();this.grid=null;this.tileSize=null;OpenLayers.Layer.HTTPRequest.prototype.destroy.apply(this,arguments);},clearGrid:function(){this.clearTileQueue();if(this.grid){for(var iRow=0,len=this.grid.length;iRow<len;iRow++){var row=this.grid[iRow];for(var iCol=0,clen=row.length;iCol<clen;iCol++){var tile=row[ [...]
+this.grid=[];this.gridResolution=null;}},clone:function(obj){if(obj==null){obj=new OpenLayers.Layer.Grid(this.name,this.url,this.params,this.getOptions());}
+obj=OpenLayers.Layer.HTTPRequest.prototype.clone.apply(this,[obj]);if(this.tileSize!=null){obj.tileSize=this.tileSize.clone();}
+obj.grid=[];obj.gridResolution=null;obj.backBuffer=null;obj.backBufferTimerId=null;obj.tileQueue=[];obj.tileQueueId=null;obj.loading=false;obj.moveTimerId=null;return obj;},moveTo:function(bounds,zoomChanged,dragging){OpenLayers.Layer.HTTPRequest.prototype.moveTo.apply(this,arguments);bounds=bounds||this.map.getExtent();if(bounds!=null){var forceReTile=!this.grid.length||zoomChanged;var tilesBounds=this.getTilesBounds();var resolution=this.map.getResolution();var serverResolution=this.ge [...]
+if(!zoomChanged||this.transitionEffect==='resize'){this.applyBackBuffer(serverResolution);}
+this.initSingleTile(bounds);}}else{forceReTile=forceReTile||!tilesBounds.intersectsBounds(bounds,{worldBounds:this.map.baseLayer.wrapDateLine&&this.map.getMaxExtent()});if(resolution!==serverResolution){bounds=this.map.calculateBounds(null,serverResolution);if(forceReTile){var scale=serverResolution/resolution;this.transformDiv(scale);}}else{this.div.style.width='100%';this.div.style.height='100%';this.div.style.left='0%';this.div.style.top='0%';}
+if(forceReTile){if(zoomChanged&&this.transitionEffect==='resize'){this.applyBackBuffer(serverResolution);}
+this.initGriddedTiles(bounds);}else{this.moveGriddedTiles();}}}},getTileData:function(loc){var data=null,x=loc.lon,y=loc.lat,numRows=this.grid.length;if(this.map&&numRows){var res=this.map.getResolution(),tileWidth=this.tileSize.w,tileHeight=this.tileSize.h,bounds=this.grid[0][0].bounds,left=bounds.left,top=bounds.top;if(x<left){if(this.map.baseLayer.wrapDateLine){var worldWidth=this.map.getMaxExtent().getWidth();var worldsAway=Math.ceil((left-x)/worldWidth);x+=worldWidth*worldsAway;}}
+var dtx=(x-left)/(res*tileWidth);var dty=(top-y)/(res*tileHeight);var col=Math.floor(dtx);var row=Math.floor(dty);if(row>=0&&row<numRows){var tile=this.grid[row][col];if(tile){data={tile:tile,i:Math.floor((dtx-col)*tileWidth),j:Math.floor((dty-row)*tileHeight)};}}}
+return data;},queueTileDraw:function(evt){var tile=evt.object;if(!~OpenLayers.Util.indexOf(this.tileQueue,tile)){this.tileQueue.push(tile);}
+if(!this.tileQueueId){this.tileQueueId=OpenLayers.Animation.start(OpenLayers.Function.bind(this.drawTileFromQueue,this),null,this.div);}
+return false;},drawTileFromQueue:function(){if(this.tileQueue.length===0){this.clearTileQueue();}else{this.tileQueue.shift().draw(true);}},clearTileQueue:function(){OpenLayers.Animation.stop(this.tileQueueId);this.tileQueueId=null;this.tileQueue=[];},destroyTile:function(tile){this.removeTileMonitoringHooks(tile);tile.destroy();},getServerResolution:function(resolution){resolution=resolution||this.map.getResolution();if(this.serverResolutions&&OpenLayers.Util.indexOf(this.serverResolutio [...]
+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= [...]
+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 backBuf [...]
+tile.style.top=(i*this.tileSize.h)+'%';tile.style.left=(j*this.tileSize.w)+'%';backBuffer.appendChild(tile);}}}
+return backBuffer;},removeBackBuffer:function(){if(this.backBuffer){this.div.removeChild(this.backBuffer);this.backBuffer=null;this.backBufferResolution=null;if(this.backBufferTimerId!==null){window.clearTimeout(this.backBufferTimerId);this.backBufferTimerId=null;}}},moveByPx:function(dx,dy){if(!this.singleTile){this.moveGriddedTiles();}},setTileSize:function(size){if(this.singleTile){size=this.map.getSize();size.h=parseInt(size.h*this.ratio);size.w=parseInt(size.w*this.ratio);}
+OpenLayers.Layer.HTTPRequest.prototype.setTileSize.apply(this,[size]);},getTilesBounds:function(){var bounds=null;var length=this.grid.length;if(length){var bottomLeftTileBounds=this.grid[length-1][0].bounds,width=this.grid[0].length*bottomLeftTileBounds.getWidth(),height=this.grid.length*bottomLeftTileBounds.getHeight();bounds=new OpenLayers.Bounds(bottomLeftTileBounds.left,bottomLeftTileBounds.bottom,bottomLeftTileBounds.left+width,bottomLeftTileBounds.bottom+height);}
+return bounds;},initSingleTile:function(bounds){this.clearTileQueue();var center=bounds.getCenterLonLat();var tileWidth=bounds.getWidth()*this.ratio;var tileHeight=bounds.getHeight()*this.ratio;var tileBounds=new OpenLayers.Bounds(center.lon-(tileWidth/2),center.lat-(tileHeight/2),center.lon+(tileWidth/2),center.lat+(tileHeight/2));var px=this.map.getLayerPxFromLonLat({lon:tileBounds.left,lat:tileBounds.top});if(!this.grid.length){this.grid[0]=[];}
+var tile=this.grid[0][0];if(!tile){tile=this.addTile(tileBounds,px);this.addTileMonitoringHooks(tile);tile.draw();this.grid[0][0]=tile;}else{tile.moveTo(tileBounds,px);}
+this.removeExcessTiles(1,1);this.gridResolution=this.getServerResolution();},calculateGridLayout:function(bounds,origin,resolution){var tilelon=resolution*this.tileSize.w;var tilelat=resolution*this.tileSize.h;var offsetlon=bounds.left-origin.lon;var tilecol=Math.floor(offsetlon/tilelon)-this.buffer;var tilecolremain=offsetlon/tilelon-tilecol;var tileoffsetx=-tilecolremain*this.tileSize.w;var tileoffsetlon=origin.lon+tilecol*tilelon;var offsetlat=bounds.top-(origin.lat+tilelat);var tiler [...]
+return origin;},initGriddedTiles:function(bounds){this.clearTileQueue();var viewSize=this.map.getSize();var minRows=Math.ceil(viewSize.h/this.tileSize.h)+
+Math.max(1,2*this.buffer);var minCols=Math.ceil(viewSize.w/this.tileSize.w)+
+Math.max(1,2*this.buffer);var origin=this.getTileOrigin();var resolution=this.getServerResolution();var tileLayout=this.calculateGridLayout(bounds,origin,resolution);var tileoffsetx=Math.round(tileLayout.tileoffsetx);var tileoffsety=Math.round(tileLayout.tileoffsety);var tileoffsetlon=tileLayout.tileoffsetlon;var tileoffsetlat=tileLayout.tileoffsetlat;var tilelon=tileLayout.tilelon;var tilelat=tileLayout.tilelat;var startX=tileoffsetx;var startLon=tileoffsetlon;var rowidx=0;var layerCont [...]
+tileoffsetlon=startLon;tileoffsetx=startX;var colidx=0;do{var tileBounds=new OpenLayers.Bounds(tileoffsetlon,tileoffsetlat,tileoffsetlon+tilelon,tileoffsetlat+tilelat);var x=tileoffsetx;x-=layerContainerDivLeft;var y=tileoffsety;y-=layerContainerDivTop;var px=new OpenLayers.Pixel(x,y);var tile=row[colidx++];if(!tile){tile=this.addTile(tileBounds,px);this.addTileMonitoringHooks(tile);row.push(tile);}else{tile.moveTo(tileBounds,px,false);}
+var tileCenter=tileBounds.getCenterLonLat();tileData.push({tile:tile,distance:Math.pow(tileCenter.lon-center.lon,2)+
+Math.pow(tileCenter.lat-center.lat,2)});tileoffsetlon+=tilelon;tileoffsetx+=this.tileSize.w;}while((tileoffsetlon<=bounds.right+tilelon*this.buffer)||colidx<minCols);tileoffsetlat-=tilelat;tileoffsety+=this.tileSize.h;}while((tileoffsetlat>=bounds.bottom-tilelat*this.buffer)||rowidx<minRows);this.removeExcessTiles(rowidx,colidx);this.gridResolution=this.getServerResolution();tileData.sort(function(a,b){return a.distance-b.distance;});for(var i=0,ii=tileData.length;i<ii;++i){tileData[i].t [...]
+this.events.triggerEvent("tileloadstart",{tile:tile});this.numLoadingTiles++;};tile.onLoadEnd=function(){this.numLoadingTiles--;this.events.triggerEvent("tileloaded",{tile:tile});if(this.tileQueue.length===0&&this.numLoadingTiles===0){this.loading=false;this.events.triggerEvent("loadend");if(this.backBuffer){this.backBufferTimerId=window.setTimeout(OpenLayers.Function.bind(this.removeBackBuffer,this),this.removeBackBufferDelay);}}};tile.onLoadError=function(){this.events.triggerEvent("ti [...]
+this.moveTimerId=window.setTimeout(this.deferMoveGriddedTiles,this.tileLoadingDelay);return;}
+var buffer=this.buffer||1;var scale=this.getResolutionScale();while(true){var tlViewPort={x:(this.grid[0][0].position.x*scale)+
+parseInt(this.div.style.left,10)+
+parseInt(this.map.layerContainerDiv.style.left),y:(this.grid[0][0].position.y*scale)+
+parseInt(this.div.style.top,10)+
+parseInt(this.map.layerContainerDiv.style.top)};var tileSize={w:this.tileSize.w*scale,h:this.tileSize.h*scale};if(tlViewPort.x>-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 modelR [...]
+if(prepend){grid.unshift(row);}else{grid.push(row);}},shiftColumn:function(prepend){var deltaX=(prepend)?-this.tileSize.w:this.tileSize.w;var resolution=this.getServerResolution();var deltaLon=resolution*deltaX;for(var i=0,len=this.grid.length;i<len;i++){var row=this.grid[i];var modelTileIndex=(prepend)?0:(row.length-1);var modelTile=row[modelTileIndex];var bounds=modelTile.bounds.clone();var position=modelTile.position.clone();bounds.left=bounds.left+deltaLon;bounds.right=bounds.right+d [...]
+for(i=0,l=this.grid.length;i<l;i++){while(this.grid[i].length>columns){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+( [...]
+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.ex [...]
+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.appl [...]
+this.div.innerHTML=OpenLayers.String.format(this.template,{layers:attributions.join(this.separator)});}},CLASS_NAME:"OpenLayers.Control.Attribution"});OpenLayers.Handler=OpenLayers.Class({id:null,control:null,map:null,keyMask:null,active:false,evt:null,initialize:function(control,callbacks,options){OpenLayers.Util.extend(this,options);this.control=control;this.callbacks=callbacks;var map=this.map||control.map;if(map){this.setMap(map);}
+this.id=OpenLayers.Util.createUniqueID(this.CLASS_NAME+"_");},setMap:function(map){this.map=map;},checkModifiers:function(evt){if(this.keyMask==null){return true;}
+var keyModifiers=(evt.shiftKey?OpenLayers.Handler.MOD_SHIFT:0)|(evt.ctrlKey?OpenLayers.Handler.MOD_CTRL:0)|(evt.altKey?OpenLayers.Handler.MOD_ALT:0);return(keyModifiers==this.keyMask);},activate:function(){if(this.active){return false;}
+var events=OpenLayers.Events.prototype.BROWSER_EVENTS;for(var i=0,len=events.length;i<len;i++){if(this[events[i]]){this.register(events[i],this[events[i]]);}}
+this.active=true;return true;},deactivate:function(){if(!this.active){return false;}
+var events=OpenLayers.Events.prototype.BROWSER_EVENTS;for(var i=0,len=events.length;i<len;i++){if(this[events[i]]){this.unregister(events[i],this[events[i]]);}}
+this.active=false;return true;},callback:function(name,args){if(name&&this.callbacks[name]){this.callbacks[name].apply(this.control,args);}},register:function(name,method){this.map.events.registerPriority(name,this,method);this.map.events.registerPriority(name,this,this.setEvent);},unregister:function(name,method){this.map.events.unregister(name,this,method);this.map.events.unregister(name,this,this.setEvent);},setEvent:function(evt){this.evt=evt;return true;},destroy:function(){this.dea [...]
+this.down=this.getEventInfo(evt);this.last=this.getEventInfo(evt);return true;},touchmove:function(evt){this.last=this.getEventInfo(evt);return true;},touchend:function(evt){if(this.down){evt.xy=this.last.xy;evt.lastTouches=this.last.touches;this.handleSingle(evt);this.down=null;}
+return true;},unregisterMouseListeners:function(){this.map.events.un({mousedown:this.mousedown,mouseup:this.mouseup,click:this.click,dblclick:this.dblclick,scope:this});},mousedown:function(evt){this.down=this.getEventInfo(evt);this.last=this.getEventInfo(evt);return true;},mouseup:function(evt){var propagate=true;if(this.checkModifiers(evt)&&this.control.handleRightClicks&&OpenLayers.Event.isRightClick(evt)){propagate=this.rightclick(evt);}
+return propagate;},rightclick:function(evt){if(this.passesTolerance(evt)){if(this.rightclickTimerId!=null){this.clearTimer();this.callback('dblrightclick',[evt]);return!this.stopDouble;}else{var clickEvent=this['double']?OpenLayers.Util.extend({},evt):this.callback('rightclick',[evt]);var delayedRightCall=OpenLayers.Function.bind(this.delayedRightCall,this,clickEvent);this.rightclickTimerId=window.setTimeout(delayedRightCall,this.delay);}}
+return!this.stopSingle;},delayedRightCall:function(evt){this.rightclickTimerId=null;if(evt){this.callback('rightclick',[evt]);}},click:function(evt){if(!this.last){this.last=this.getEventInfo(evt);}
+this.handleSingle(evt);return!this.stopSingle;},dblclick:function(evt){this.handleDouble(evt);return!this.stopDouble;},handleDouble:function(evt){if(this.passesDblclickTolerance(evt)){if(this["double"]){this.callback("dblclick",[evt]);}
+this.clearTimer();}},handleSingle:function(evt){if(this.passesTolerance(evt)){if(this.timerId!=null){if(this.last.touches&&this.last.touches.length===1){if(this["double"]){OpenLayers.Event.stop(evt);}
+this.handleDouble(evt);}
+if(!this.last.touches||this.last.touches.length!==2){this.clearTimer();}}else{this.first=this.getEventInfo(evt);var clickEvent=this.single?OpenLayers.Util.extend({},evt):null;this.queuePotentialClick(clickEvent);}}},queuePotentialClick:function(evt){this.timerId=window.setTimeout(OpenLayers.Function.bind(this.delayedCall,this,evt),this.delay);},passesTolerance:function(evt){var passes=true;if(this.pixelTolerance!=null&&this.down&&this.down.xy){passes=this.pixelTolerance>=this.down.xy.dis [...]
+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<len;i++){touch=evt.touches[i];touches[i]={clientX:touch.clientX,clientY:touch.clientY};}}
+return{xy:evt.xy,touches:touches};},deactivate:function(){var deactivated=false;if(OpenLayers.Handler.prototype.deactivate.apply(this,arguments)){this.clearTimer();this.down=null;this.first=null;this.last=null;this.touch=false;deactivated=true;}
+return deactivated;},CLASS_NAME:"OpenLayers.Handler.Click"});OpenLayers.Events.buttonclick=OpenLayers.Class({target:null,events:['mousedown','mouseup','click','dblclick','touchstart','touchmove','touchend','keydown'],startRegEx:/^mousedown|touchstart$/,cancelRegEx:/^touchmove$/,completeRegEx:/^mouseup|touchend$/,initialize:function(target){this.target=target;for(var i=this.events.length-1;i>=0;--i){this.target.register(this.events[i],this,this.buttonClick,{extension:true});}},destroy:fun [...]
+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);propagat [...]
+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},eleme [...]
+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 [...]
+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;OpenLa [...]
+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()){ [...]
+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") [...]
+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. [...]
+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,pan [...]
+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.wheelListene [...]
+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<len;i++){if(elem==this.map.layers[i].div||elem==this.map.layers[i].pane){overLayerDiv=true;break;}}}
+overMapDiv=(elem==this.map.div);elem=elem.parentNode;}
+if(!overScrollableDiv&&overMapDiv){if(overLayerDiv){var delta=0;if(!e){e=window.event;}
+if(e.wheelDelta){delta=e.wheelDelta/120;if(window.opera&&window.opera.version()<9.2){delta=-delta;}}else if(e.detail){delta=-e.detail/3;}
+this.delta=this.delta+delta;if(this.interval){window.clearTimeout(this._timeoutId);this._timeoutId=window.setTimeout(OpenLayers.Function.bind(function(){this.wheelZoom(e);},this),this.interval);}else{this.wheelZoom(e);}}
+OpenLayers.Event.stop(e);}},wheelZoom:function(e){var delta=this.delta;this.delta=0;if(delta){if(this.mousePosition){e.xy=this.mousePosition;}
+if(!e.xy){e.xy=this.map.getPixelFromLonLat(this.map.getCenter());}
+if(delta<0){this.callback("down",[e,this.cumulative?delta:-1]);}else{this.callback("up",[e,this.cumulative?delta:1]);}}},mousemove:function(evt){this.mousePosition=evt.xy;},activate:function(evt){if(OpenLayers.Handler.prototype.activate.apply(this,arguments)){var wheelListener=this.wheelListener;OpenLayers.Event.observe(window,"DOMMouseScroll",wheelListener);OpenLayers.Event.observe(window,"mousewheel",wheelListener);OpenLayers.Event.observe(document,"mousewheel",wheelListener);return tr [...]
+this.dragPan=null;if(this.zoomBox){this.zoomBox.destroy();}
+this.zoomBox=null;if(this.pinchZoom){this.pinchZoom.destroy();}
+this.pinchZoom=null;OpenLayers.Control.prototype.destroy.apply(this,arguments);},activate:function(){this.dragPan.activate();if(this.zoomWheelEnabled){this.handlers.wheel.activate();}
+this.handlers.click.activate();if(this.zoomBoxEnabled){this.zoomBox.activate();}
+if(this.pinchZoom){this.pinchZoom.activate();}
+return OpenLayers.Control.prototype.activate.apply(this,arguments);},deactivate:function(){if(this.pinchZoom){this.pinchZoom.deactivate();}
+this.zoomBox.deactivate();this.dragPan.deactivate();this.handlers.click.deactivate();this.handlers.wheel.deactivate();return OpenLayers.Control.prototype.deactivate.apply(this,arguments);},draw:function(){if(this.handleRightClicks){this.map.viewPortDiv.oncontextmenu=OpenLayers.Function.False;}
+var clickCallbacks={'click':this.defaultClick,'dblclick':this.defaultDblClick,'dblrightclick':this.defaultDblRightClick};var clickOptions={'double':true,'stopDouble':true};this.handlers.click=new OpenLayers.Handler.Click(this,clickCallbacks,clickOptions);this.dragPan=new OpenLayers.Control.DragPan(OpenLayers.Util.extend({map:this.map,documentDrag:this.documentDrag},this.dragPanOptions));this.zoomBox=new OpenLayers.Control.ZoomBox({map:this.map,keyMask:this.zoomBoxKeyMask});this.dragPan.d [...]
+var size=this.map.getSize();var deltaX=size.w/2-evt.xy.x;var deltaY=evt.xy.y-size.h/2;var newRes=this.map.baseLayer.getResolutionForZoom(newZoom);var zoomPoint=this.map.getLonLatFromPixel(evt.xy);var newCenter=new OpenLayers.LonLat(zoomPoint.lon+deltaX*newRes,zoomPoint.lat+deltaY*newRes);this.map.setCenter(newCenter,newZoom);},wheelUp:function(evt,delta){this.wheelChange(evt,delta||1);},wheelDown:function(evt,delta){this.wheelChange(evt,delta||-1);},disableZoomBox:function(){this.zoomBox [...]
+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(re [...]
+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("but [...]
+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, [...]
+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(p [...]
+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,matrix [...]
+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<len;++i){this.matrixIds[i]={identifier:ids[i]};}}}},setMap:function(){OpenLayers.Layer.Grid.prototype.setMap.apply(this,arguments);this.updateMatrixProperties();},updateMatrixProperties:function(){this.matrix=this.getMatrix();if(this.matrix){if(this.matrix.topLeftCorner){this.tileOrigin=this.matrix.topLeftCorner;}
+if(this.matrix.tileWidth&&this.matrix.tileHeight){this.tileSize=new OpenLayers.Size(this.matrix.tileWidth,this.matrix.tileHeight);}
+if(!this.tileOrigin){this.tileOrigin=new OpenLayers.LonLat(this.maxExtent.left,this.maxExtent.top);}
+if(!this.tileFullExtent){this.tileFullExtent=this.maxExtent;}}},moveTo:function(bounds,zoomChanged,dragging){if(zoomChanged||!this.matrix){this.updateMatrixProperties();}
+return OpenLayers.Layer.Grid.prototype.moveTo.apply(this,arguments);},clone:function(obj){if(obj==null){obj=new OpenLayers.Layer.WMTS(this.options);}
+obj=OpenLayers.Layer.Grid.prototype.clone.apply(this,[obj]);return obj;},getIdentifier:function(){return this.getServerZoom();},getMatrix:function(){var matrix;if(!this.matrixIds||this.matrixIds.length===0){matrix={identifier:this.getIdentifier()};}else{if("scaleDenominator"in this.matrixIds[0]){var denom=OpenLayers.METERS_PER_INCH*OpenLayers.INCHES_PER_UNIT[this.units]*this.getServerResolution()/0.28E-3;var diff=Number.POSITIVE_INFINITY;var delta;for(var i=0,ii=this.matrixIds.length;i<i [...]
+return matrix;},getTileInfo:function(loc){var res=this.getServerResolution();var fx=(loc.lon-this.tileOrigin.lon)/(res*this.tileSize.w);var fy=(this.tileOrigin.lat-loc.lat)/(res*this.tileSize.h);var col=Math.floor(fx);var row=Math.floor(fy);return{col:col,row:row,i:Math.floor((fx-col)*this.tileSize.w),j:Math.floor((fy-row)*this.tileSize.h)};},getURL:function(bounds){bounds=this.adjustBounds(bounds);var url="";if(!this.tileFullExtent||this.tileFullExtent.intersectsBounds(bounds)){var cent [...]
+url=OpenLayers.String.format(template,context);}else{var path=this.version+"/"+this.layer+"/"+this.style+"/";if(dimensions){for(var i=0;i<dimensions.length;i++){if(params[dimensions[i]]){path=path+params[dimensions[i]]+"/";}}}
+path=path+this.matrixSet+"/"+this.matrix.identifier+"/"+info.row+"/"+info.col+"."+this.formatSuffix;if(OpenLayers.Util.isArray(this.url)){url=this.selectUrl(path,this.url);}else{url=this.url;}
+if(!url.match(/\/$/)){url=url+"/";}
+url=url+path;}}else if(this.requestEncoding.toUpperCase()==="KVP"){params={SERVICE:"WMTS",REQUEST:"GetTile",VERSION:this.version,LAYER:this.layer,STYLE:this.style,TILEMATRIXSET:this.matrixSet,TILEMATRIX:this.matrix.identifier,TILEROW:info.row,TILECOL:info.col,FORMAT:this.format};url=OpenLayers.Layer.Grid.prototype.getFullRequestString.apply(this,[params]);}}
+return url;},mergeNewParams:function(newParams){if(this.requestEncoding.toUpperCase()==="KVP"){return OpenLayers.Layer.Grid.prototype.mergeNewParams.apply(this,[OpenLayers.Util.upperCaseObject(newParams)]);}},CLASS_NAME:"OpenLayers.Layer.WMTS"});
\ No newline at end of file
diff --git a/mapproxy/service/templates/demo/static/img/blank.gif b/mapproxy/service/templates/demo/static/img/blank.gif
new file mode 100644
index 0000000..4bcc753
Binary files /dev/null and b/mapproxy/service/templates/demo/static/img/blank.gif differ
diff --git a/mapproxy/service/templates/demo/static/img/east-mini.png b/mapproxy/service/templates/demo/static/img/east-mini.png
new file mode 100644
index 0000000..0707567
Binary files /dev/null and b/mapproxy/service/templates/demo/static/img/east-mini.png differ
diff --git a/mapproxy/service/templates/demo/static/img/north-mini.png b/mapproxy/service/templates/demo/static/img/north-mini.png
new file mode 100644
index 0000000..a8a0b40
Binary files /dev/null and b/mapproxy/service/templates/demo/static/img/north-mini.png differ
diff --git a/mapproxy/service/templates/demo/static/img/south-mini.png b/mapproxy/service/templates/demo/static/img/south-mini.png
new file mode 100644
index 0000000..6c4ac8a
Binary files /dev/null and b/mapproxy/service/templates/demo/static/img/south-mini.png differ
diff --git a/mapproxy/service/templates/demo/static/img/west-mini.png b/mapproxy/service/templates/demo/static/img/west-mini.png
new file mode 100644
index 0000000..db5f420
Binary files /dev/null and b/mapproxy/service/templates/demo/static/img/west-mini.png differ
diff --git a/mapproxy/service/templates/demo/static/img/zoom-minus-mini.png b/mapproxy/service/templates/demo/static/img/zoom-minus-mini.png
new file mode 100644
index 0000000..f9b63ab
Binary files /dev/null and b/mapproxy/service/templates/demo/static/img/zoom-minus-mini.png differ
diff --git a/mapproxy/service/templates/demo/static/img/zoom-plus-mini.png b/mapproxy/service/templates/demo/static/img/zoom-plus-mini.png
new file mode 100644
index 0000000..eecf2eb
Binary files /dev/null and b/mapproxy/service/templates/demo/static/img/zoom-plus-mini.png differ
diff --git a/mapproxy/service/templates/demo/static/img/zoom-world-mini.png b/mapproxy/service/templates/demo/static/img/zoom-world-mini.png
new file mode 100644
index 0000000..2159dde
Binary files /dev/null and b/mapproxy/service/templates/demo/static/img/zoom-world-mini.png differ
diff --git a/mapproxy/service/templates/demo/static/logo.png b/mapproxy/service/templates/demo/static/logo.png
new file mode 100644
index 0000000..0883226
Binary files /dev/null and b/mapproxy/service/templates/demo/static/logo.png differ
diff --git a/mapproxy/service/templates/demo/static/site.css b/mapproxy/service/templates/demo/static/site.css
new file mode 100644
index 0000000..6b4be76
--- /dev/null
+++ b/mapproxy/service/templates/demo/static/site.css
@@ -0,0 +1,132 @@
+
+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;
+}
diff --git a/mapproxy/service/templates/demo/static/theme/default/framedCloud.css b/mapproxy/service/templates/demo/static/theme/default/framedCloud.css
new file mode 100644
index 0000000..e69de29
diff --git a/mapproxy/service/templates/demo/static/theme/default/google.css b/mapproxy/service/templates/demo/static/theme/default/google.css
new file mode 100644
index 0000000..3ee757c
--- /dev/null
+++ b/mapproxy/service/templates/demo/static/theme/default/google.css
@@ -0,0 +1,17 @@
+.olLayerGoogleCopyright {
+    right: 3px;
+    bottom: 2px;
+    left: auto;  
+}
+.olLayerGoogleV3.olLayerGoogleCopyright {
+    bottom: 0px;
+    right: 0px !important;
+}
+.olLayerGooglePoweredBy {
+    left: 2px;
+    bottom: 2px;   
+}
+.olLayerGoogleV3.olLayerGooglePoweredBy {
+    bottom: 0px !important;
+}
+
diff --git a/mapproxy/service/templates/demo/static/theme/default/ie6-style.css b/mapproxy/service/templates/demo/static/theme/default/ie6-style.css
new file mode 100644
index 0000000..a0fd7c6
--- /dev/null
+++ b/mapproxy/service/templates/demo/static/theme/default/ie6-style.css
@@ -0,0 +1,10 @@
+.olControlZoomPanel div {
+    background-image: url(img/zoom-panel-NOALPHA.png);
+}
+.olControlPanPanel div {
+    background-image: url(img/pan-panel-NOALPHA.png);
+}
+.olControlEditingToolbar {
+    width: 200px;
+}
+
diff --git a/mapproxy/service/templates/demo/static/theme/default/style.css b/mapproxy/service/templates/demo/static/theme/default/style.css
new file mode 100644
index 0000000..f8a6bfb
--- /dev/null
+++ b/mapproxy/service/templates/demo/static/theme/default/style.css
@@ -0,0 +1,482 @@
+div.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);
+}
+ at 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;
+}
diff --git a/mapproxy/service/templates/demo/tms_demo.html b/mapproxy/service/templates/demo/tms_demo.html
new file mode 100644
index 0000000..66e4edf
--- /dev/null
+++ b/mapproxy/service/templates/demo/tms_demo.html
@@ -0,0 +1,81 @@
+{{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}}
+<script src="static/OpenLayers.js"></script>
+<script type="text/javascript">
+var map;
+function init(){
+    var mapOptions = {
+    projection: new OpenLayers.Projection('{{srs}}'),
+    maxResolution: {{resolutions[0]}},
+    {{if add_res_to_options}}
+    resolutions: [{{', '.join(str(r) for r in resolutions)}}],
+    {{endif}}
+    units: '{{units}}',
+    numZoomLevels: {{len(resolutions)}},
+    maxExtent: new OpenLayers.Bounds({{', '.join(str(s) for s in layer.grid.bbox)}})
+    };
+
+    map = new OpenLayers.Map('map', mapOptions);
+
+    var layer = new OpenLayers.Layer.TMS('TMS {{layer.name}}', '../tms/',
+        {layername: '{{"/".join(layer.md["name_path"])}}', type: '{{format}}',
+         tileSize: new OpenLayers.Size{{layer.grid.tile_size}}
+    });
+
+    map.addLayer(layer)
+    map.zoomToExtent(new OpenLayers.Bounds({{approx_bbox(layer, srs)}}));
+}
+</script>
+{{enddef}}
+            <h2>Openlayers Client - Layer {{layer.name}}</h2>
+            <form action="" method="GET">
+                <table>
+                    <tr><th>Coordinate System</th><th>Image format</th></tr>
+                    <tr><td>
+                            <select name="srs" size="1" onchange="this.form.submit()">
+                                {{for tms_layer in all_tile_layers.values()}}
+                                    {{if tms_layer.name == layer.name and tms_layer.grid.supports_access_with_origin('sw')}}
+                                        {{if tms_layer.md['name_internal'] == layer.md['name_internal']}}
+                                            <option selected value="{{srs}}">{{srs}}</option>
+                                        {{else}}
+                                            <option value="{{tms_layer.grid.srs.srs_code}}">{{tms_layer.grid.srs.srs_code}}</option>
+                                        {{endif}}
+                                    {{endif}}
+                                {{endfor}}
+                            </select>
+                            <input type="hidden" name="format" value="{{format}}">
+                            <input type="hidden" name="tms_layer" value="{{layer.name}}">
+                        </td>
+                        <td>{{format}}</td></tr>
+                </table>
+              </form>
+            <div id='map'></div>
+            <h3>Bounding Box</h3>
+            <p class="code">{{', '.join(str(s) for s in layer.grid.bbox)}}</p>
+            <h3>Level and Resolutions</h3>
+            <table class="code">
+                <tr><th>Level</th><th>Resolution</th></tr>
+                {{for level, res in layer.grid.tile_sets}}
+                <tr><td>{{level}}</td><td>{{res}}</td></tr>
+                {{endfor}}
+            </table>
+            <h3>JavaScript code</h3>
+            <pre>
+{{for line in jscript_openlayers().split('\n')}}
+{{cgi.escape(wrapper.fill(line))}}
+{{endfor}}
+            </pre>
\ No newline at end of file
diff --git a/mapproxy/service/templates/demo/wms_demo.html b/mapproxy/service/templates/demo/wms_demo.html
new file mode 100644
index 0000000..de5037e
--- /dev/null
+++ b/mapproxy/service/templates/demo/wms_demo.html
@@ -0,0 +1,77 @@
+{{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}}
+<script src="static/OpenLayers.js"></script>
+<script type="text/javascript">
+    var map;
+    function init(){
+        OpenLayers.Util.onImageLoadErrorColor = "transparent";
+
+        map = new OpenLayers.Map('map', {
+          maxResolution: {{res}},
+          maxExtent: new OpenLayers.Bounds({{', '.join(str(s) for s in bbox)}}),
+          projection: new OpenLayers.Projection("{{srs}}"),
+          numZoomLevels: 22
+        });
+        var layer = new OpenLayers.Layer.WMS( "WMS {{layer.name}}",
+            "../service?",
+            {layers: "{{layer.name}}", format: "{{format}}", srs:"{{srs}}",
+             exceptions: "application/vnd.ogc.se_inimage"{{if format == 'image/png'}}, transparent: true{{endif}}},
+            {singleTile: true, ratio: 1, isBaseLayer: true} );
+
+        map.addLayer(layer);
+        map.zoomToMaxExtent();
+    }
+</script>
+{{enddef}}
+            <h2>Openlayers Client - Layer {{layer.name}}</h2>
+            <form action="" method="GET">
+                <table>
+                    <tr>
+                        <th>Coordinate System</th>
+                        <th>Image format</th>
+                    </tr>
+                    <tr>
+                        <td>
+                            <select name="srs" onchange="this.form.submit()">
+                                {{for srs_name, srs_code in layer_srs(layer)}}
+                                    {{if srs_code == srs}}
+                                    <option selected value="{{srs_code}}">{{srs_name}}</option>
+                                    {{else}}
+                                    <option value="{{srs_code}}">{{srs_name}}</option>
+                                    {{endif}}
+                                {{endfor}}
+                            </select>
+                        </td>
+                        <td>
+                            <select name="format" onchange="this.form.submit()">
+                            {{for image_format in image_formats}}
+                                {{if image_format == format}}
+                                <option selected value="{{image_format}}">{{image_format | strip}}</option>
+                                {{else}}
+                                <option value="{{image_format}}">{{image_format | strip}}</option>
+                                {{endif}}
+                            {{endfor}}
+                            </select>
+                        </td>
+                    </tr>
+                </table>
+                <input type="hidden" name="wms_layer" value="{{layer.name}}">
+              </form>
+            <div id='map'></div>
+            <h3>JavaScript code</h3>
+            <pre>
+{{for line in jscript_openlayers().split('\n')}}
+{{cgi.escape(wrapper.fill(line))}}
+{{endfor}}
+            </pre>
\ No newline at end of file
diff --git a/mapproxy/service/templates/demo/wmts_demo.html b/mapproxy/service/templates/demo/wmts_demo.html
new file mode 100644
index 0000000..4d302f0
--- /dev/null
+++ b/mapproxy/service/templates/demo/wmts_demo.html
@@ -0,0 +1,74 @@
+{{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}}
+<script src="static/OpenLayers.js"></script>
+<script type="text/javascript">
+var map;
+function init(){
+    var mapOptions = {
+    projection: new OpenLayers.Projection('{{srs}}'),
+    resolutions: [{{', '.join(str(r) for r in resolutions)}}],
+    units: '{{units}}',
+    maxExtent: new OpenLayers.Bounds({{', '.join(str(s) for s in layer.grid.bbox)}})
+    };
+
+    map = new OpenLayers.Map('map', mapOptions);
+
+    var layer = new OpenLayers.Layer.WMTS({
+     name: "WMTS {{layer.name}}",
+     url: '../wmts{{restful_url}}',
+     layer: '{{layer.name}}',
+     matrixSet: '{{matrix_set}}',
+     format: '{{format}}',
+     isBaseLayer: true,
+     style: 'default',
+     requestEncoding: 'REST'
+    });
+
+    map.addLayer(layer)
+    map.zoomToExtent(new OpenLayers.Bounds({{approx_bbox(layer, srs)}}));
+}
+</script>
+{{enddef}}
+            <h2>Openlayers Client - Layer {{layer.name}}</h2>
+            <table>
+                <tr><th>Coordinate System</th><th>Image format</th></tr>
+                <tr><td>
+                        <select name="srs" size="1" onchange="this.form.submit()">
+                                {{for wmts_layer in all_tile_layers.values()}}
+                                    {{if wmts_layer.name == layer.name and wmts_layer.grid.supports_access_with_origin('nw')}}
+                                        {{if wmts_layer.md['name_internal'] == layer.md['name_internal']}}
+                                            <option selected value="{{srs}}">{{srs}}</option>
+                                        {{else}}
+                                            <option value="{{wmts_layer.grid.srs.srs_code}}">{{wmts_layer.grid.srs.srs_code}}</option>
+                                        {{endif}}
+                                    {{endif}}
+                                {{endfor}}
+                            </select>
+                            <input type="hidden" name="format" value="{{format}}">
+                            <input type="hidden" name="wmts_layer" value="{{layer.name}}">
+                    </td>
+                    <td>{{layer.format}}</td></tr>
+            </table>
+            <div id='map'></div>
+            <h3>Bounding Box</h3>
+            <p class="code">{{', '.join(str(s) for s in layer.grid.bbox)}}</p>
+            <h3>JavaScript code</h3>
+            <pre>
+{{for line in jscript_openlayers().split('\n')}}
+{{cgi.escape(wrapper.fill(line))}}
+{{endfor}}
+            </pre>
\ No newline at end of file
diff --git a/mapproxy/service/templates/tms_capabilities.xml b/mapproxy/service/templates/tms_capabilities.xml
new file mode 100644
index 0000000..0a1b754
--- /dev/null
+++ b/mapproxy/service/templates/tms_capabilities.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+    <TileMapService version="1.0.0">
+    <Title>{{service.title}}</Title>
+    <Abstract>{{service.abstract}}</Abstract>
+    <TileMaps>
+{{for layer in layers.values()}}
+        <TileMap title="{{layer.title}}"
+                 srs="{{layer.grid.srs_name}}"
+                 profile="{{layer.grid.profile}}"
+                 href="{{service.url.rstrip('/')}}/{{'/'.join(layer.md['name_path'])}}" />
+{{endfor}}
+    </TileMaps>
+</TileMapService>
\ No newline at end of file
diff --git a/mapproxy/service/templates/tms_exception.xml b/mapproxy/service/templates/tms_exception.xml
new file mode 100644
index 0000000..95b803f
--- /dev/null
+++ b/mapproxy/service/templates/tms_exception.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0"?>
+<TileMapServerError>
+    <Message>{{exception}}</Message>
+</TileMapServerError>
\ No newline at end of file
diff --git a/mapproxy/service/templates/tms_root_resource.xml b/mapproxy/service/templates/tms_root_resource.xml
new file mode 100644
index 0000000..d2f0016
--- /dev/null
+++ b/mapproxy/service/templates/tms_root_resource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0"?>
+    <Services>
+{{if service}}
+    <TileMapService title="{{service.title}}" version="1.0.0"
+        href="{{service.url if service.url.endswith('/') else service.url + '/'}}1.0.0/" />
+{{endif}}
+</Services>
\ No newline at end of file
diff --git a/mapproxy/service/templates/tms_tilemap_capabilities.xml b/mapproxy/service/templates/tms_tilemap_capabilities.xml
new file mode 100644
index 0000000..e91d60c
--- /dev/null
+++ b/mapproxy/service/templates/tms_tilemap_capabilities.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+ <TileMap version="1.0.0">
+  <Title>{{layer.title}}</Title>
+  <Abstract></Abstract>
+  <SRS>{{layer.grid.srs_name}}</SRS>
+  <BoundingBox minx="{{layer.bbox[0]}}" miny="{{layer.bbox[1]}}" maxx="{{layer.bbox[2]}}" maxy="{{layer.bbox[3]}}" />
+  <Origin x="{{layer.bbox[0]}}" y="{{layer.bbox[1]}}" />
+  <TileFormat width="{{layer.grid.tile_size[0]}}" height="{{layer.grid.tile_size[1]}}" mime-type="{{layer.format_mime_type}}" extension="{{layer.format}}" />
+  <TileSets profile="{{layer.grid.profile}}">
+{{for level, res in layer.grid.tile_sets }}
+    <TileSet href="{{service.url}}/{{level}}" units-per-pixel="{{res}}" order="{{level}}" />
+{{endfor}}
+  </TileSets>
+</TileMap>
\ No newline at end of file
diff --git a/mapproxy/service/templates/wms100capabilities.xml b/mapproxy/service/templates/wms100capabilities.xml
new file mode 100644
index 0000000..a0ec8f8
--- /dev/null
+++ b/mapproxy/service/templates/wms100capabilities.xml
@@ -0,0 +1,101 @@
+<?xml version="1.0"?>
+<!DOCTYPE WMT_MS_Capabilities SYSTEM "http://schemas.opengis.net/wms/1.0.0/WMS_MS_Capabilities.dtd"
+ [
+ <!ELEMENT VendorSpecificCapabilities EMPTY>
+ ]>  <!-- end of DOCTYPE declaration -->
+<WMT_MS_Capabilities version="1.0.0">
+<Service>
+  <Name>OGC:WMS</Name>
+  <Title>{{service.title}}</Title>
+  <Abstract>{{service.abstract}}</Abstract>
+{{if service.online_resource}}
+  <OnlineResource>{{service.online_resource}}</OnlineResource>
+{{else}}
+  <OnlineResource>{{service.url}}</OnlineResource>
+{{endif}}
+  <Fees>{{service.get('fees', 'none')}}</Fees>
+  <AccessConstraints>{{service.get('access_constraints', 'none')}}</AccessConstraints>
+</Service>
+
+<Capability>
+  <Request>
+    <Capabilities>
+      <Format>
+        <WMS_XML/>
+      </Format>
+      <DCPType>
+        <HTTP>
+          <Get onlineResource="{{service.url}}?"/>
+        </HTTP>
+      </DCPType>
+    </Capabilities>
+    <Map>
+      <Format>
+{{for format in formats}}
+{{if wms100format(format)}}
+        <{{wms100format(format)}}/>
+{{endif}}
+{{endfor}}
+      </Format>
+      <DCPType>
+        <HTTP>
+          <Get onlineResource="{{service.url}}?"/>
+        </HTTP>
+      </DCPType>
+    </Map>
+    <FeatureInfo>
+      <Format>
+{{for format in info_formats}}
+{{if wms100info_format(format)}}
+        <{{wms100info_format(format)}}/>
+{{endif}}
+{{endfor}}
+      </Format>
+      <DCPType>
+        <HTTP>
+          <Get onlineResource="{{service.url}}?"/>
+        </HTTP>
+      </DCPType>
+    </FeatureInfo>
+  </Request>
+  <Exception>
+    <Format>
+      <INIMAGE/>
+      <BLANK/>
+      <WMS_XML/>
+    </Format>
+  </Exception>
+
+{{def layer_capabilities(layer, with_srs)}}
+  <Layer{{if layer.queryable}} queryable="1"{{endif}}>
+    {{if layer.name}}
+    <Name>{{ layer.name }}</Name>
+    {{endif}}
+    <Title>{{ layer.title }}</Title>
+    <SRS>{{for s in srs}}{{s}} {{endfor}}</SRS>
+    {{py: extent = limit_llbbox(layer.extent.llbbox)}}
+    <LatLonBoundingBox minx="{{ extent[0] }}" miny="{{ extent[1] }}" maxx="{{ extent[2] }}" maxy="{{ extent[3] }}" />
+    {{for srs_code, bbox in layer_srs_bbox(layer)}}
+    <BoundingBox SRS="{{srs_code}}" minx="{{ bbox[0] }}" miny="{{ bbox[1] }}" maxx="{{ bbox[2] }}" maxy="{{ bbox[3] }}" />
+    {{endfor}}
+{{if layer.is_active and layer.has_legend and layer.legend_url}}
+    <Style>
+      <Name>default</Name>
+      <Title>default</Title>
+      <StyleURL>{{service.url}}{{layer.legend_url|escape}}</StyleURL>
+    </Style>
+{{endif}}
+    {{if layer.res_range}}
+    {{py: max_scale, min_scale = layer.res_range.scale_hint()}}
+    <ScaleHint {{if min_scale}}min="{{min_scale}}"{{endif}} {{if max_scale}}max="{{max_scale}}"{{endif}} />
+    {{endif}}
+    {{for layer in layer.layers}}
+{{layer_capabilities(layer, False)|indent}}
+    {{endfor}}
+  </Layer>
+{{enddef}}
+
+{{layer_capabilities(layers, True)}}
+
+</Capability>
+</WMT_MS_Capabilities>
diff --git a/mapproxy/service/templates/wms100exception.xml b/mapproxy/service/templates/wms100exception.xml
new file mode 100644
index 0000000..a382f22
--- /dev/null
+++ b/mapproxy/service/templates/wms100exception.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0"?>
+<WMTException version="1.0.0">
+{{exception}}
+</WMTException>
\ No newline at end of file
diff --git a/mapproxy/service/templates/wms110capabilities.xml b/mapproxy/service/templates/wms110capabilities.xml
new file mode 100644
index 0000000..53dce73
--- /dev/null
+++ b/mapproxy/service/templates/wms110capabilities.xml
@@ -0,0 +1,141 @@
+<?xml version="1.0"?>
+<!DOCTYPE WMT_MS_Capabilities SYSTEM "http://schemas.opengis.net/wms/1.1.0/capabilities_1_1_0.dtd"
+ [
+ {{if tile_layers}}
+ <!ELEMENT VendorSpecificCapabilities (TileSet*) >
+ <!ELEMENT TileSet (SRS, BoundingBox?, Resolutions, Width, Height, Format, Layers*, Styles*) >
+ <!ELEMENT Resolutions (#PCDATA) >
+ <!ELEMENT Width (#PCDATA) >
+ <!ELEMENT Height (#PCDATA) >
+ <!ELEMENT Layers (#PCDATA) >
+ <!ELEMENT Styles (#PCDATA) >
+ {{else}}
+ <!ELEMENT VendorSpecificCapabilities EMPTY>
+ {{endif}}
+ ]>  <!-- end of DOCTYPE declaration -->
+<WMT_MS_Capabilities version="1.1.0">
+<Service>
+  <Name>OGC:WMS</Name>
+  <Title>{{service.title}}</Title>
+  <Abstract>{{service.abstract}}</Abstract>
+{{if service.online_resource}}
+  <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{{service.online_resource}}"/>
+{{else}}
+  <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{{service.url}}"/>
+{{endif}}
+{{if service.contact}}
+{{py:service.contact = bunch(default='', **service.contact)}}
+  <ContactInformation>
+      <ContactPersonPrimary>
+        <ContactPerson>{{service.contact.person}}</ContactPerson>
+        <ContactOrganization>{{service.contact.organization}}</ContactOrganization>
+      </ContactPersonPrimary>
+      <ContactPosition>{{service.contact.position}}</ContactPosition>
+      <ContactAddress>
+        <AddressType>{{service.contact.get('address_type', 'postal')}}</AddressType>
+        <Address>{{service.contact.address}}</Address>
+        <City>{{service.contact.city}}</City>
+        <StateOrProvince>{{service.contact.state}}</StateOrProvince>
+        <PostCode>{{service.contact.postcode}}</PostCode>
+        <Country>{{service.contact.country}}</Country>
+      </ContactAddress>
+      <ContactVoiceTelephone>{{service.contact.phone}}</ContactVoiceTelephone>
+      <ContactFacsimileTelephone>{{service.contact.fax}}</ContactFacsimileTelephone>
+      <ContactElectronicMailAddress>{{service.contact.email}}</ContactElectronicMailAddress>
+  </ContactInformation>
+{{endif}}
+  <Fees>{{service.get('fees', 'none')}}</Fees>
+  <AccessConstraints>{{service.get('access_constraints', 'none')}}</AccessConstraints>
+</Service>
+
+<Capability>
+  <Request>
+    <GetCapabilities>
+      <Format>application/vnd.ogc.wms_xml</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{{service.url}}?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetCapabilities>
+    <GetMap>
+{{for format in formats}}
+        <Format>{{format}}</Format>
+{{endfor}}
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{{service.url}}?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetMap>
+    <GetFeatureInfo>
+{{for format in info_formats}}
+      <Format>{{format}}</Format>
+{{endfor}}
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{{service.url}}?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetFeatureInfo>
+  </Request>
+  <Exception>
+    <Format>application/vnd.ogc.se_xml</Format>
+    <Format>application/vnd.ogc.se_inimage</Format>
+    <Format>application/vnd.ogc.se_blank</Format>
+  </Exception>
+{{if tile_layers}}
+  <VendorSpecificCapabilities>
+{{for layer in tile_layers}}
+    <TileSet>
+      <SRS>{{layer.grid.srs_name}}</SRS>
+      <BoundingBox SRS="{{layer.grid.srs_name}}" minx="{{ layer.extent.bbox[0] }}" miny="{{ layer.extent.bbox[1] }}" maxx="{{ layer.extent.bbox[2] }}" maxy="{{ layer.extent.bbox[3] }}" />
+      <Resolutions>{{for level, res in layer.grid.tile_sets}}{{res}} {{endfor}}</Resolutions>
+      <Width>{{layer.grid.tile_size[0]}}</Width>
+      <Height>{{layer.grid.tile_size[1]}}</Height>
+      <Format>{{layer.md['format']}}</Format>
+      <Layers>{{layer.name}}</Layers>
+      <Styles></Styles>
+    </TileSet>
+{{endfor}}
+  </VendorSpecificCapabilities>
+{{endif}}
+
+{{def layer_capabilities(layer, with_srs)}}
+  <Layer{{if layer.queryable}} queryable="1"{{endif}}>
+    {{if layer.name}}
+    <Name>{{ layer.name }}</Name>
+    {{endif}}
+    <Title>{{ layer.title }}</Title>
+    {{if with_srs}}
+    <SRS>{{for s in srs}}{{s}} {{endfor}}</SRS>
+    {{endif}}
+    {{py: extent = limit_llbbox(layer.extent.llbbox)}}
+    <LatLonBoundingBox minx="{{ extent[0] }}" miny="{{ extent[1] }}" maxx="{{ extent[2] }}" maxy="{{ extent[3] }}" />
+    {{for srs_code, bbox in layer_srs_bbox(layer)}}
+    <BoundingBox SRS="{{srs_code}}" minx="{{ bbox[0] }}" miny="{{ bbox[1] }}" maxx="{{ bbox[2] }}" maxy="{{ bbox[3] }}" />
+    {{endfor}}
+    {{if layer.is_active and layer.has_legend and layer.legend_url}}
+    <Style>
+        <Name>default</Name>
+        <Title>default</Title>
+        <LegendURL width="{{layer.legend_size[0]}}" height="{{layer.legend_size[1]}}">
+            <Format>image/png</Format>
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="{{service.url}}{{layer.legend_url|escape}}"/>
+        </LegendURL>
+    </Style>
+    {{endif}}
+    {{if layer.res_range}}
+    {{py: max_scale, min_scale = layer.res_range.scale_hint()}}
+    <ScaleHint {{if min_scale}}min="{{min_scale}}"{{endif}} {{if max_scale}}max="{{max_scale}}"{{endif}} />
+    {{endif}}
+    {{for layer in layer.layers}}
+{{layer_capabilities(layer, False)|indent}}
+    {{endfor}}
+  </Layer>
+{{enddef}}
+
+{{layer_capabilities(layers, True)}}
+
+</Capability>
+</WMT_MS_Capabilities>
diff --git a/mapproxy/service/templates/wms110exception.xml b/mapproxy/service/templates/wms110exception.xml
new file mode 100644
index 0000000..bccc4a0
--- /dev/null
+++ b/mapproxy/service/templates/wms110exception.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0"?>
+<!DOCTYPE ServiceExceptionReport SYSTEM "http://schemas.opengis.net/wms/1.1.0/exception_1_1_0.dtd">
+<ServiceExceptionReport version="1.1.0">
+    <ServiceException{{if code is not None}} code="{{code}}"{{endif}}>{{exception}}</ServiceException>
+</ServiceExceptionReport>
\ No newline at end of file
diff --git a/mapproxy/service/templates/wms111capabilities.xml b/mapproxy/service/templates/wms111capabilities.xml
new file mode 100644
index 0000000..4baaa73
--- /dev/null
+++ b/mapproxy/service/templates/wms111capabilities.xml
@@ -0,0 +1,172 @@
+<?xml version="1.0"?>
+<!DOCTYPE WMT_MS_Capabilities SYSTEM "http://schemas.opengis.net/wms/1.1.1/WMS_MS_Capabilities.dtd"
+ [
+ {{if tile_layers}}
+ <!ELEMENT VendorSpecificCapabilities (TileSet*) >
+ <!ELEMENT TileSet (SRS, BoundingBox?, Resolutions, Width, Height, Format, Layers*, Styles*) >
+ <!ELEMENT Resolutions (#PCDATA) >
+ <!ELEMENT Width (#PCDATA) >
+ <!ELEMENT Height (#PCDATA) >
+ <!ELEMENT Layers (#PCDATA) >
+ <!ELEMENT Styles (#PCDATA) >
+ {{else}}
+ <!ELEMENT VendorSpecificCapabilities EMPTY>
+ {{endif}}
+ ]>  <!-- end of DOCTYPE declaration -->
+<WMT_MS_Capabilities version="1.1.1">
+<Service>
+  <Name>OGC:WMS</Name>
+  <Title>{{service.title}}</Title>
+  <Abstract>{{service.abstract}}</Abstract>
+{{if service.online_resource}}
+  <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{{service.online_resource}}"/>
+{{else}}
+  <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{{service.url}}"/>
+{{endif}}
+{{if service.contact}}
+{{py:service.contact = bunch(default='', **service.contact)}}
+  <ContactInformation>
+      <ContactPersonPrimary>
+        <ContactPerson>{{service.contact.person}}</ContactPerson>
+        <ContactOrganization>{{service.contact.organization}}</ContactOrganization>
+      </ContactPersonPrimary>
+      <ContactPosition>{{service.contact.position}}</ContactPosition>
+      <ContactAddress>
+        <AddressType>{{service.contact.get('address_type', 'postal')}}</AddressType>
+        <Address>{{service.contact.address}}</Address>
+        <City>{{service.contact.city}}</City>
+        <StateOrProvince>{{service.contact.state}}</StateOrProvince>
+        <PostCode>{{service.contact.postcode}}</PostCode>
+        <Country>{{service.contact.country}}</Country>
+      </ContactAddress>
+      <ContactVoiceTelephone>{{service.contact.phone}}</ContactVoiceTelephone>
+      <ContactFacsimileTelephone>{{service.contact.fax}}</ContactFacsimileTelephone>
+      <ContactElectronicMailAddress>{{service.contact.email}}</ContactElectronicMailAddress>
+  </ContactInformation>
+{{endif}}
+  <Fees>{{service.get('fees', 'none')}}</Fees>
+  <AccessConstraints>{{service.get('access_constraints', 'none')}}</AccessConstraints>
+</Service>
+
+<Capability>
+  <Request>
+    <GetCapabilities>
+      <Format>application/vnd.ogc.wms_xml</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{{service.url}}?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetCapabilities>
+    <GetMap>
+{{for format in formats}}
+        <Format>{{format}}</Format>
+{{endfor}}
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{{service.url}}?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetMap>
+    <GetFeatureInfo>
+{{for format in info_formats}}
+      <Format>{{format}}</Format>
+{{endfor}}
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{{service.url}}?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetFeatureInfo>
+{{if service.has_legend}}
+    <GetLegendGraphic>
+{{for format in formats}}
+        <Format>{{format}}</Format>
+{{endfor}}
+        <DCPType>
+            <HTTP>
+                <Get><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{{service.url}}?"/></Get>
+            </HTTP>
+        </DCPType>
+    </GetLegendGraphic>
+{{endif}}
+  </Request>
+  <Exception>
+    <Format>application/vnd.ogc.se_xml</Format>
+    <Format>application/vnd.ogc.se_inimage</Format>
+    <Format>application/vnd.ogc.se_blank</Format>
+  </Exception>
+{{if tile_layers}}
+  <VendorSpecificCapabilities>
+{{for layer in tile_layers}}
+    <TileSet>
+      <SRS>{{layer.grid.srs.srs_code}}</SRS>
+      <BoundingBox SRS="{{layer.grid.srs.srs_code}}" minx="{{ layer.extent.bbox[0] }}" miny="{{ layer.extent.bbox[1] }}" maxx="{{ layer.extent.bbox[2] }}" maxy="{{ layer.extent.bbox[3] }}" />
+      <Resolutions>{{for level, res in layer.grid.tile_sets}}{{res}} {{endfor}}</Resolutions>
+      <Width>{{layer.grid.tile_size[0]}}</Width>
+      <Height>{{layer.grid.tile_size[1]}}</Height>
+      <Format>{{layer.md['format']}}</Format>
+      <Layers>{{layer.name}}</Layers>
+      <Styles></Styles>
+    </TileSet>
+{{endfor}}
+  </VendorSpecificCapabilities>
+{{endif}}
+
+{{def layer_capabilities(layer, with_srs)}}
+  <Layer{{if layer.queryable}} queryable="1"{{endif}}>
+    {{if layer.name}}
+    <Name>{{ layer.name }}</Name>
+    {{endif}}
+    <Title>{{ layer.title }}</Title>
+    {{if layer.md and 'abstract' in layer.md}}
+    <Abstract>{{ layer.md['abstract'] }}</Abstract>
+    {{endif}}
+    {{if with_srs}}
+    {{for s in srs}}
+    <SRS>{{s}}</SRS>
+    {{endfor}}
+    {{endif}}
+    {{py: extent = limit_llbbox(layer.extent.llbbox)}}
+    <LatLonBoundingBox minx="{{ extent[0] }}" miny="{{ extent[1] }}" maxx="{{ extent[2] }}" maxy="{{ extent[3] }}" />
+    {{for srs_code, bbox in layer_srs_bbox(layer)}}
+    <BoundingBox SRS="{{srs_code}}" minx="{{ bbox[0] }}" miny="{{ bbox[1] }}" maxx="{{ bbox[2] }}" maxy="{{ bbox[3] }}" />
+    {{endfor}}
+    {{py: md = bunch(default='', **layer.md)}}
+    {{if md.metadata}}
+      {{for data in md.metadata}}
+          {{py: data = bunch(default='', **data)}}
+          {{if wms111metadatatype(data.type)}}
+    <MetadataURL type="{{wms111metadatatype(data.type)|escape}}">
+      {{if data.format}}
+      <Format>{{data.format}}</Format>
+      {{endif}}
+      <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{{data.url|escape}}"/>
+    </MetadataURL>
+          {{endif}}
+      {{endfor}}
+    {{endif}}
+    {{if layer.is_active and layer.has_legend and layer.legend_url}}
+    <Style>
+        <Name>default</Name>
+        <Title>default</Title>
+        <LegendURL width="{{layer.legend_size[0]}}" height="{{layer.legend_size[1]}}">
+            <Format>image/png</Format>
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="{{service.url}}{{layer.legend_url|escape}}"/>
+        </LegendURL>
+    </Style>
+    {{endif}}
+    {{if layer.res_range}}
+    {{py: max_scale, min_scale = layer.res_range.scale_hint()}}
+    <ScaleHint {{if min_scale}}min="{{min_scale}}"{{endif}} {{if max_scale}}max="{{max_scale}}"{{endif}} />
+    {{endif}}
+    {{for layer in layer.layers}}
+{{layer_capabilities(layer, False)|indent}}
+    {{endfor}}
+  </Layer>
+{{enddef}}
+
+{{layer_capabilities(layers, True)}}
+
+</Capability>
+</WMT_MS_Capabilities>
diff --git a/mapproxy/service/templates/wms111exception.xml b/mapproxy/service/templates/wms111exception.xml
new file mode 100644
index 0000000..e604104
--- /dev/null
+++ b/mapproxy/service/templates/wms111exception.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0"?>
+<!DOCTYPE ServiceExceptionReport SYSTEM "http://schemas.opengis.net/wms/1.1.1/exception_1_1_1.dtd">
+<ServiceExceptionReport version="1.1.1">
+    <ServiceException{{if code is not None}} code="{{code}}"{{endif}}>{{exception}}</ServiceException>
+</ServiceExceptionReport>
\ No newline at end of file
diff --git a/mapproxy/service/templates/wms130capabilities.xml b/mapproxy/service/templates/wms130capabilities.xml
new file mode 100644
index 0000000..b73bb19
--- /dev/null
+++ b/mapproxy/service/templates/wms130capabilities.xml
@@ -0,0 +1,311 @@
+<?xml version="1.0" ?>
+<WMS_Capabilities xmlns="http://www.opengis.net/wms" xmlns:sld="http://www.opengis.net/sld" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" {{if inspire_md}}xmlns:inspire_common="http://inspire.ec.europa.eu/schemas/common/1.0" xmlns:inspire_vs="http://inspire.ec.europa.eu/schemas/inspire_vs/1.0" {{endif}}version="1.3.0" xsi:schemaLocation="http://www.opengis.net/wms http://schemas.opengis.net/wms/1.3.0/capabilities_1_3_0.xsd http://www.ope [...]
+<Service>
+  <Name>WMS</Name>
+  <Title>{{service.title}}</Title>
+  <Abstract>{{service.abstract}}</Abstract>
+  {{if service.keyword_list and len(service.keyword_list) > 0}}
+  <KeywordList>
+  {{for list in service.keyword_list}}
+    {{py: kw=bunch(default='', **list)}}
+    {{for keyword in kw.keywords}}
+    <Keyword{{if kw.vocabulary}} vocabulary="{{kw.vocabulary}}"{{endif}}>{{keyword}}</Keyword>
+    {{endfor}}
+  {{endfor}}
+  </KeywordList>
+  {{endif}}{{if service.online_resource}}
+  <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{{service.online_resource}}"/>
+{{else}}
+  <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{{service.url}}"/>
+{{endif}}
+{{if service.contact}}
+{{py:service.contact = bunch(default='', **service.contact)}}
+  <ContactInformation>
+      <ContactPersonPrimary>
+        <ContactPerson>{{service.contact.person}}</ContactPerson>
+        <ContactOrganization>{{service.contact.organization}}</ContactOrganization>
+      </ContactPersonPrimary>
+      <ContactPosition>{{service.contact.position}}</ContactPosition>
+      <ContactAddress>
+        <AddressType>{{service.contact.get('address_type', 'postal')}}</AddressType>
+        <Address>{{service.contact.address}}</Address>
+        <City>{{service.contact.city}}</City>
+        <StateOrProvince>{{service.contact.state}}</StateOrProvince>
+        <PostCode>{{service.contact.postcode}}</PostCode>
+        <Country>{{service.contact.country}}</Country>
+      </ContactAddress>
+      <ContactVoiceTelephone>{{service.contact.phone}}</ContactVoiceTelephone>
+      <ContactFacsimileTelephone>{{service.contact.fax}}</ContactFacsimileTelephone>
+      <ContactElectronicMailAddress>{{service.contact.email}}</ContactElectronicMailAddress>
+  </ContactInformation>
+{{endif}}
+    <Fees>{{service.get('fees', 'none')}}</Fees>
+    <AccessConstraints>{{service.get('access_constraints', 'none')}}</AccessConstraints>
+</Service>
+
+<Capability>
+  <Request>
+    <GetCapabilities>
+      <Format>text/xml</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xlink:href="{{service.url}}?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetCapabilities>
+    <GetMap>
+{{for format in formats}}
+      <Format>{{format}}</Format>
+{{endfor}}
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xlink:href="{{service.url}}?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetMap>
+    <GetFeatureInfo>
+{{for format in info_formats}}
+      <Format>{{format}}</Format>
+{{endfor}}
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xlink:href="{{service.url}}?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetFeatureInfo>
+{{if service.has_legend}}
+    <sld:GetLegendGraphic>
+{{for format in formats}}
+      <Format>{{format}}</Format>
+{{endfor}}
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xlink:href="{{service.url}}?"/></Get>
+        </HTTP>
+      </DCPType>
+    </sld:GetLegendGraphic>
+{{endif}}
+  </Request>
+  <Exception>
+    <Format>XML</Format>
+    <Format>INIMAGE</Format>
+    <Format>BLANK</Format>
+  </Exception>
+{{if inspire_md}}
+
+{{def inspire_dates(config)}}
+  {{if 'date_of_publication' in config}}
+<inspire_common:DateOfPublication>{{config['date_of_publication']}}</inspire_common:DateOfPublication>
+  {{endif}}
+  {{if 'date_of_creation' in config}}
+<inspire_common:DateOfCreation>{{config['date_of_creation']}}</inspire_common:DateOfCreation>
+  {{endif}}
+  {{if 'date_of_last_revision' in config}}
+<inspire_common:DateOfLastRevision>{{config['date_of_last_revision']}}</inspire_common:DateOfLastRevision>
+  {{endif}}
+{{enddef}}
+
+  <inspire_vs:ExtendedCapabilities>
+  {{if inspire_md.type == 'linked'}}
+    <inspire_common:MetadataUrl>
+      <inspire_common:URL>{{ inspire_md.metadata_url.url | escape }}</inspire_common:URL>
+      <inspire_common:MediaType>{{ inspire_md.metadata_url.media_type }}</inspire_common:MediaType>
+    </inspire_common:MetadataUrl>
+    <inspire_common:SupportedLanguages>
+      <inspire_common:DefaultLanguage>
+        <inspire_common:Language>{{ inspire_md.languages.default }}</inspire_common:Language>
+      </inspire_common:DefaultLanguage>
+    </inspire_common:SupportedLanguages>
+    <inspire_common:ResponseLanguage>
+      <inspire_common:Language>{{ inspire_md.languages.default }}</inspire_common:Language>
+    </inspire_common:ResponseLanguage>
+  {{else}}
+    {{for rl in inspire_md.resource_locators}}
+    <inspire_common:ResourceLocator>
+      <inspire_common:URL>{{ rl['url'] | escape }}</inspire_common:URL>
+      <inspire_common:MediaType>{{ rl['media_type'] }}</inspire_common:MediaType>
+    </inspire_common:ResourceLocator>
+    {{endfor}}
+    <inspire_common:ResourceType>service</inspire_common:ResourceType>
+    <inspire_common:TemporalReference>
+      {{ inspire_dates(inspire_md.temporal_reference) }}
+    </inspire_common:TemporalReference>
+    {{for c in inspire_md.conformities}}
+    <inspire_common:Conformity>
+      <inspire_common:Specification>
+        <inspire_common:Title>{{c['title']}}</inspire_common:Title>
+        {{ inspire_dates(c) }}
+        {{for uri in c.get('uris', [])}}
+        <inspire_common:URI>{{uri | escape}}</inspire_common:URI>
+        {{endfor}}
+        {{for rl in c.get('resource_locators')}}
+        <inspire_common:ResourceLocator>
+          <inspire_common:URL>{{ rl['url'] | escape }}</inspire_common:URL>
+          <inspire_common:MediaType>{{ rl['media_type'] }}</inspire_common:MediaType>
+        </inspire_common:ResourceLocator>
+        {{endfor}}
+      </inspire_common:Specification>
+      <inspire_common:Degree>{{c['degree']}}</inspire_common:Degree>
+    </inspire_common:Conformity>
+    {{endfor}}
+    {{for c in inspire_md.metadata_points_of_contact}}
+    <inspire_common:MetadataPointOfContact>
+      <inspire_common:OrganisationName>{{c['organisation_name']}}</inspire_common:OrganisationName>
+      <inspire_common:EmailAddress>{{c['email']}}</inspire_common:EmailAddress>
+    </inspire_common:MetadataPointOfContact>
+    {{endfor}}
+    <inspire_common:MetadataDate>{{inspire_md.metadata_date}}</inspire_common:MetadataDate>
+    <inspire_common:SpatialDataServiceType>view</inspire_common:SpatialDataServiceType>
+    {{for k in inspire_md.mandatory_keywords}}
+    <inspire_common:MandatoryKeyword>
+      <inspire_common:KeywordValue>{{ k }}</inspire_common:KeywordValue>
+    </inspire_common:MandatoryKeyword>
+    {{endfor}}
+    {{for k in inspire_md.get('keywords', [])}}
+    <inspire_common:Keyword>
+      <inspire_common:OriginatingControlledVocabulary>
+        <inspire_common:Title>{{ k['title']}}</inspire_common:Title>
+        {{ inspire_dates(k) }}
+        {{for uri in k.get('uris', [])}}
+        <inspire_common:URI>{{uri | escape}}</inspire_common:URI>
+        {{endfor}}
+        {{for rl in k.get('resource_locators', [])}}
+        <inspire_common:ResourceLocator>
+          <inspire_common:URL>{{ rl['url'] | escape }}</inspire_common:URL>
+          <inspire_common:MediaType>{{ rl['media_type'] }}</inspire_common:MediaType>
+        </inspire_common:ResourceLocator>
+        {{endfor}}
+      </inspire_common:OriginatingControlledVocabulary>
+      <inspire_common:KeywordValue>{{ k['keyword_value']}}</inspire_common:KeywordValue>
+    </inspire_common:Keyword>
+    {{endfor}}
+    <inspire_common:SupportedLanguages>
+      <inspire_common:DefaultLanguage>
+        <inspire_common:Language>{{ inspire_md.languages.default }}</inspire_common:Language>
+      </inspire_common:DefaultLanguage>
+    </inspire_common:SupportedLanguages>
+    <inspire_common:ResponseLanguage>
+      <inspire_common:Language>{{ inspire_md.languages.default }}</inspire_common:Language>
+    </inspire_common:ResponseLanguage>
+    {{if inspire_md.metadata_url}}
+    <inspire_common:MetadataUrl>
+      <inspire_common:URL>{{ inspire_md.metadata_url.url | escape }}</inspire_common:URL>
+      <inspire_common:MediaType>{{ inspire_md.metadata_url.media_type }}</inspire_common:MediaType>
+    </inspire_common:MetadataUrl>
+    {{endif}}
+  {{endif}}
+  </inspire_vs:ExtendedCapabilities>
+{{endif}}
+{{def layer_capabilities(layer, with_srs)}}
+  {{py: md = bunch(default='', **layer.md)}}
+
+  <Layer{{if layer.queryable}} queryable="1"{{endif}}>
+    {{if layer.name}}
+    <Name>{{ layer.name }}</Name>
+    {{endif}}
+    <Title>{{ layer.title }}</Title>
+    {{if md.abstract}}
+    <Abstract>{{md.abstract}}</Abstract>
+    {{endif}}
+    {{if md.keyword_list and len(md.keyword_list) > 0}}
+    <KeywordList>
+    {{for list in md.keyword_list}}
+      {{py: kw=bunch(default='', **list)}}
+      {{for keyword in kw.keywords}}
+      <Keyword{{if kw.vocabulary}} vocabulary="{{kw.vocabulary}}"{{endif}}>{{keyword}}</Keyword>
+      {{endfor}}
+    {{endfor}}
+    </KeywordList>
+    {{endif}}
+    {{if with_srs}}
+    {{for s in srs}}
+    <CRS>{{s}}</CRS>
+    {{endfor}}
+    {{endif}}
+    {{py: extent = limit_llbbox(layer.extent.llbbox)}}
+    <EX_GeographicBoundingBox>
+      <westBoundLongitude>{{ extent[0] }}</westBoundLongitude>
+      <eastBoundLongitude>{{ extent[2] }}</eastBoundLongitude>
+      <southBoundLatitude>{{ extent[1] }}</southBoundLatitude>
+      <northBoundLatitude>{{ extent[3] }}</northBoundLatitude>
+    </EX_GeographicBoundingBox>
+    <BoundingBox CRS="CRS:84" minx="{{ extent[0] }}" miny="{{ extent[1] }}" maxx="{{ extent[2] }}" maxy="{{ extent[3] }}" />
+    {{for srs_code, bbox in layer_srs_bbox(layer, epsg_axis_order=True)}}
+    <BoundingBox CRS="{{srs_code}}" minx="{{ bbox[0] }}" miny="{{ bbox[1] }}" maxx="{{ bbox[2] }}" maxy="{{ bbox[3] }}" />
+    {{endfor}}
+    {{if md.attribution}}
+    {{py: attribution = bunch(default='', **md.attribution)}}
+    <Attribution>
+      {{if attribution.title}}
+      <Title>{{attribution.title}}</Title>
+      {{endif}}
+      {{if attribution.url}}
+      <OnlineResource xlink:href="{{attribution.url|escape}}"/>
+      {{endif}}
+      {{if attribution.logo}}
+      {{py: logo = bunch(default='', **attribution.logo)}}
+      <LogoURL{{if logo.width}} width="{{logo.width}}"{{endif}}{{if logo.height}} height="{{logo.height}}"{{endif}}>
+        {{if logo.format}}
+        <Format>{{logo.format}}</Format>
+        {{endif}}
+        {{if logo.url}}
+        <OnlineResource xlink:href="{{logo.url|escape}}"/>
+        {{endif}}
+      </LogoURL>
+      {{endif}}
+    </Attribution>
+    {{endif}}
+    {{if md.identifier}}
+    {{for idf in md.identifier}}
+    {{py: idf=bunch(default='', **idf)}}
+    <AuthorityURL name="{{idf.name}}">
+    {{if idf.url}}
+      <OnlineResource xlink:href="{{idf.url|escape}}" />
+    {{endif}}
+    </AuthorityURL>
+    {{endfor}}
+    {{for idf in md.identifier}}
+    {{py:idf=bunch(default='', **idf)}}
+    <Identifier authority="{{idf.name}}">{{idf.value}}</Identifier>
+    {{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}}
+    <Style>
+      <Name>{{if inspire_md}}inspire_common:DEFAULT{{else}}default{{endif}}</Name>
+      <Title>default</Title>
+      <LegendURL width="{{layer.legend_size[0]}}" height="{{layer.legend_size[1]}}">
+        <Format>image/png</Format>
+        <OnlineResource xlink:type="simple" xlink:href="{{service.url}}{{layer.legend_url|escape}}" />
+      </LegendURL>
+    </Style>
+{{endif}}
+    {{if layer.res_range}}
+    {{py: min_scale, max_scale = layer.res_range.scale_denominator()}}
+    {{if min_scale}}<MinScaleDenominator>{{min_scale}}</MinScaleDenominator>{{endif}}
+    {{if max_scale}}<MaxScaleDenominator>{{max_scale}}</MaxScaleDenominator>{{endif}}
+    {{endif}}
+    {{for layer in layer.layers}}
+{{layer_capabilities(layer, False)|indent}}
+    {{endfor}}
+  </Layer>
+{{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}}
+      <Format>{{data.format}}</Format>
+      {{endif}}
+      <OnlineResource xlink:href="{{data.url|escape}}"/>
+    </{{tag}}>
+  {{endfor}}
+{{enddef}}
+
+{{layer_capabilities(layers, True)}}
+
+</Capability>
+</WMS_Capabilities>
diff --git a/mapproxy/service/templates/wms130exception.xml b/mapproxy/service/templates/wms130exception.xml
new file mode 100644
index 0000000..8fff4b1
--- /dev/null
+++ b/mapproxy/service/templates/wms130exception.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<ServiceExceptionReport version="1.3.0"
+  xmlns="http://www.opengis.net/ogc"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://www.opengis.net/ogc
+http://schemas.opengis.net/wms/1.3.0/exceptions_1_3_0.xsd">
+    <ServiceException{{if code is not None}} code="{{code}}"{{endif}}>{{exception}}</ServiceException>
+</ServiceExceptionReport>
\ No newline at end of file
diff --git a/mapproxy/service/templates/wmts100capabilities.xml b/mapproxy/service/templates/wmts100capabilities.xml
new file mode 100644
index 0000000..d0e0d0c
--- /dev/null
+++ b/mapproxy/service/templates/wmts100capabilities.xml
@@ -0,0 +1,120 @@
+<?xml version="1.0"?>
+<Capabilities xmlns="http://www.opengis.net/wmts/1.0" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gml="http://www.opengis.net/gml" xsi:schemaLocation="http://www.opengis.net/wmts/1.0   ../wmtsGetCapabilities_response.xsd" version="1.0.0">
+  <ows:ServiceIdentification>
+    <ows:Title>{{service.title}}</ows:Title>
+    <ows:Abstract>{{service.abstract}}</ows:Abstract>
+    <ows:ServiceType>OGC WMTS</ows:ServiceType>
+    <ows:ServiceTypeVersion>1.0.0</ows:ServiceTypeVersion>
+    <ows:Fees>{{service.get('fees', 'none')}}</ows:Fees>
+    <ows:AccessConstraints>{{service.get('access_constraints', 'none')}}</ows:AccessConstraints>
+  </ows:ServiceIdentification>
+{{if service.contact}}
+{{py:service.contact = bunch(default='', **service.contact)}}
+  <ows:ServiceProvider>
+    <ows:ProviderName>{{service.contact.organization}}</ows:ProviderName>
+    <ows:ProviderSite xlink:href="{{service.online_resource}}"/>
+    <ows:ServiceContact>
+      <ows:IndividualName>{{service.contact.person}}</ows:IndividualName>
+      <ows:PositionName>{{service.contact.position}}</ows:PositionName>
+      <ows:ContactInfo>
+        <ows:Phone>
+          <ows:Voice>{{service.contact.phone}}</ows:Voice>
+          <ows:Facsimile>{{service.contact.fax}}</ows:Facsimile>
+        </ows:Phone>
+        <ows:Address>
+          <ows:DeliveryPoint>{{service.contact.organization}}</ows:DeliveryPoint>
+          <ows:City>{{service.contact.city}}</ows:City>
+          <ows:PostalCode>{{service.contact.postcode}}</ows:PostalCode>
+          <ows:Country>{{service.contact.country}}</ows:Country>
+          <ows:ElectronicMailAddress>{{service.contact.email}}</ows:ElectronicMailAddress>
+        </ows:Address>
+      </ows:ContactInfo>
+    </ows:ServiceContact>
+  </ows:ServiceProvider>
+{{endif}}
+{{if not restful}}
+  <ows:OperationsMetadata>
+    <ows:Operation name="GetCapabilities">
+      <ows:DCP>
+        <ows:HTTP>
+          <ows:Get xlink:href="{{service.url}}?">
+            <ows:Constraint name="GetEncoding">
+              <ows:AllowedValues>
+                <ows:Value>KVP</ows:Value>
+              </ows:AllowedValues>
+            </ows:Constraint>
+          </ows:Get>
+        </ows:HTTP>
+      </ows:DCP>
+    </ows:Operation>
+    <ows:Operation name="GetTile">
+      <ows:DCP>
+        <ows:HTTP>
+          <ows:Get xlink:href="{{service.url}}?">
+            <ows:Constraint name="GetEncoding">
+              <ows:AllowedValues>
+                <ows:Value>KVP</ows:Value>
+              </ows:AllowedValues>
+            </ows:Constraint>
+          </ows:Get>
+        </ows:HTTP>
+      </ows:DCP>
+    </ows:Operation>
+  </ows:OperationsMetadata>
+{{endif}}
+  <Contents>
+{{for layer in layers}}
+    <Layer>
+      <ows:Title>{{layer.title}}</ows:Title>
+      <ows:Abstract></ows:Abstract>
+      <ows:WGS84BoundingBox>
+        <ows:LowerCorner>{{layer.md['extent'].llbbox[0]}} {{layer.md['extent'].llbbox[1]}}</ows:LowerCorner>
+        <ows:UpperCorner>{{layer.md['extent'].llbbox[2]}} {{layer.md['extent'].llbbox[3]}}</ows:UpperCorner>
+      </ows:WGS84BoundingBox>
+      <ows:Identifier>{{layer.name}}</ows:Identifier>
+      <Style>
+        <ows:Identifier>default</ows:Identifier>
+      </Style>
+      <Format>image/{{layer.format}}</Format>
+      {{for dimension in layer.dimensions}}
+      <Dimension>
+        <ows:Identifier>{{ dimension_keys[dimension] }}</ows:Identifier>
+        <Default>{{ layer.dimensions[dimension].default }}</Default>
+        {{for value in layer.dimensions[dimension]}}
+        <Value>{{ value }}</Value>
+        {{endfor}}
+      </Dimension>
+      {{endfor}}
+      {{for grid in layer.grids}}
+      <TileMatrixSetLink>
+        <TileMatrixSet>{{grid.name}}</TileMatrixSet>
+      </TileMatrixSetLink>
+      {{endfor}}
+      {{if restful}}
+      <ResourceURL format="image/{{layer.format}}" resourceType="tile"
+          template="{{format_resource_template(layer, resource_template, service)}}"/>
+      {{endif}}
+    </Layer>
+{{endfor}}
+{{for tile_matrix_set in tile_matrix_sets}}
+    <TileMatrixSet>
+      <ows:Identifier>{{tile_matrix_set.name}}</ows:Identifier>
+      <ows:SupportedCRS>{{tile_matrix_set.srs_name}}</ows:SupportedCRS>
+  {{for matrix in tile_matrix_set}}
+      <TileMatrix>
+        <ows:Identifier>{{matrix.identifier}}</ows:Identifier>
+        <ScaleDenominator>{{matrix.scale_denom}}</ScaleDenominator>
+        <TopLeftCorner>{{matrix.topleft[0]}} {{matrix.topleft[1]}}</TopLeftCorner>
+        <TileWidth>{{matrix.tile_size[0]}}</TileWidth>
+        <TileHeight>{{matrix.tile_size[1]}}</TileHeight>
+        <MatrixWidth>{{matrix.grid_size[0]}}</MatrixWidth>
+        <MatrixHeight>{{matrix.grid_size[1]}}</MatrixHeight>
+      </TileMatrix>
+  {{endfor}}
+    </TileMatrixSet>
+{{endfor}}
+  </Contents>
+{{if restful}}
+  <ServiceMetadataURL xlink:href="{{service.url}}/1.0.0/WMTSCapabilities.xml"/>
+{{endif}}
+</Capabilities>
diff --git a/mapproxy/service/templates/wmts100exception.xml b/mapproxy/service/templates/wmts100exception.xml
new file mode 100644
index 0000000..e20a36a
--- /dev/null
+++ b/mapproxy/service/templates/wmts100exception.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<ows:ExceptionReport xmlns:ows="http://www.opengis.net/ows/1.1"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://www.opengis.net/ows/1.1 http://schemas.opengis.net/ows/1.1.0/owsExceptionReport.xsd"
+  version="1.0.0" xml:lang="en">
+  <ows:Exception exceptionCode="{{if code is None}}NoApplicableCode{{else}}{{code}}{{endif}}">
+    <ows:ExceptionText>{{exception}}</ows:ExceptionText>
+  </ows:Exception>
+</ows:ExceptionReport>
diff --git a/mapproxy/service/tile.py b/mapproxy/service/tile.py
new file mode 100644
index 0000000..8885d96
--- /dev/null
+++ b/mapproxy/service/tile.py
@@ -0,0 +1,479 @@
+# -:- 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.
+
+from __future__ import division, with_statement
+
+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)
diff --git a/mapproxy/service/wms.py b/mapproxy/service/wms.py
new file mode 100644
index 0000000..177d9c8
--- /dev/null
+++ b/mapproxy/service/wms.py
@@ -0,0 +1,797 @@
+# This file is part of the MapProxy project.
+# Copyright (C) 2010-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.
+
+"""
+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
+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 not layer.transparent:
+                    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):
+        layer_srs_code = layer.extent.srs.srs_code
+        for srs, extent in iteritems(self.srs_extents):
+            if extent.is_default:
+                bbox = layer.extent.bbox_for(SRS(srs))
+            else:
+                bbox = extent.bbox_for(SRS(srs))
+
+            if epsg_axis_order:
+                bbox = switch_bbox_epsg_axis_order(bbox, srs)
+            yield srs, bbox
+
+        # add native srs
+        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)
+            yield layer_srs_code, bbox
+
+    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,
+                                   inspire_md=inspire_md,
+        )
+        # strip blank lines
+        doc = '\n'.join(l for l in doc.split('\n') if l.rstrip())
+        return doc
+
+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=layer)
+                else:
+                    ex = layer_task.exception
+                    async_pool.shutdown(True)
+                    raise ex[1]
+        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=layer)
+                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)
+                    raise ex[1]
+
+        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
+
+    transparent = 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 is_opaque(self):
+        return not self.transparent
+
+    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.transparent = all(not map_lyr.is_opaque() for map_lyr in self.map_layers)
+        self.has_legend = True if legend_layers else False
+
+    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.transparent = True if this and not this.is_opaque() or all(not l.is_opaque() for l in layers) else False
+        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)
+
+    @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
diff --git a/mapproxy/service/wmts.py b/mapproxy/service/wmts.py
new file mode 100644
index 0000000..8f7a15b
--- /dev/null
+++ b/mapproxy/service/wmts.py
@@ -0,0 +1,298 @@
+# 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.
+
+"""
+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.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 = {}
+        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, {})[grid.name] = layer
+        wmts_layers = {}
+        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())
diff --git a/mapproxy/source/__init__.py b/mapproxy/source/__init__.py
new file mode 100644
index 0000000..1934384
--- /dev/null
+++ b/mapproxy/source/__init__.py
@@ -0,0 +1,74 @@
+# 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.
+
+"""
+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.transparent = True
+        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.extent = MapExtent((-180, -90, 180, 90), SRS(4326))
+        self.transparent = True
+        self.extent = MapExtent(coverage.bbox, coverage.srs) if coverage else DefaultMapExtent()
+    def get_map(self, query):
+        raise BlankImage()
diff --git a/mapproxy/source/error.py b/mapproxy/source/error.py
new file mode 100644
index 0000000..e107931
--- /dev/null
+++ b/mapproxy/source/error.py
@@ -0,0 +1,38 @@
+# 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.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_source
\ No newline at end of file
diff --git a/mapproxy/source/mapnik.py b/mapproxy/source/mapnik.py
new file mode 100644
index 0000000..de04565
--- /dev/null
+++ b/mapproxy/source/mapnik.py
@@ -0,0 +1,151 @@
+# 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 __future__ import with_statement, 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(transparent=self.transparent, format=query.format))
diff --git a/mapproxy/source/tile.py b/mapproxy/source/tile.py
new file mode 100644
index 0000000..2f08316
--- /dev/null
+++ b/mapproxy/source/tile.py
@@ -0,0 +1,96 @@
+# 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.
+
+"""
+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)
+
diff --git a/mapproxy/source/wms.py b/mapproxy/source/wms.py
new file mode 100644
index 0000000..00f3445
--- /dev/null
+++ b/mapproxy/source/wms.py
@@ -0,0 +1,239 @@
+# 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.
+
+"""
+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.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 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
+
diff --git a/mapproxy/srs.py b/mapproxy/srs.py
new file mode 100644
index 0000000..996335f
--- /dev/null
+++ b/mapproxy/srs.py
@@ -0,0 +1,422 @@
+# -*- coding: 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.
+
+"""
+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
+    # INF is not portable for <2.6 so we check against a large value
+    MAX = 1e300
+    try:
+        minx = min(p[0] for p in points if p[0] <= MAX)
+        miny = min(p[1] for p in points if p[1] <= MAX)
+        maxx = max(p[0] for p in points if p[0] <= MAX)
+        maxy = max(p[1] for p in points if p[1] <= MAX)
+        return (minx, miny, maxx, maxy)
+    except ValueError: # 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
diff --git a/mapproxy/template.py b/mapproxy/template.py
new file mode 100644
index 0000000..8b7c351
--- /dev/null
+++ b/mapproxy/template.py
@@ -0,0 +1,52 @@
+# 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.
+
+"""
+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
diff --git a/mapproxy/test/__init__.py b/mapproxy/test/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/mapproxy/test/helper.py b/mapproxy/test/helper.py
new file mode 100644
index 0000000..52ddfd9
--- /dev/null
+++ b/mapproxy/test/helper.py
@@ -0,0 +1,230 @@
+# 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
+
+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(' <foo> bar\\n zing\\t1')
+    '<foo>barzing1'
+    """
+    if isinstance(data, bytes):
+        return re.sub(b'\s+', b'', data)
+    else:
+        return re.sub('\s+', '', data)
+
+
+ at 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_stderr
\ No newline at end of file
diff --git a/mapproxy/test/http.py b/mapproxy/test/http.py
new file mode 100644
index 0000000..00f12bb
--- /dev/null
+++ b/mapproxy/test/http.py
@@ -0,0 +1,428 @@
+# 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
+
+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 RequestsMissmatchError(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 missmatch:\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 RequestMissmatch(object):
+    def __init__(self, msg, expected, actual):
+        self.msg = msg
+        self.expected = expected
+        self.actual = actual
+
+    def __str__(self):
+        return ('requests missmatch, 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(
+                        RequestMissmatch('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(
+                            RequestMissmatch('missing header', k, self.headers)
+                        )
+                    elif self.headers[k] != v:
+                        self.server.assertions.append(
+                            RequestMissmatch('header missmatch', '%s: %s' % (k, v), self.headers)
+                        )
+            if not query_comparator(req['path'], self.query_data):
+                self.server.assertions.append(
+                    RequestMissmatch('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(
+                    RequestMissmatch('requests params differ', query_actual - query_expected, query_expected - query_actual)
+                )
+                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' % RequestsMissmatchError(self._thread.assertions))
+        if value:
+            raise reraise((type, value, traceback))
+        if not self._thread.sucess:
+            raise RequestsMissmatchError(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
+
+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
+    """
+    return (query_to_dict(expected) == query_to_dict(actual) and
+            path_from_query(expected) == path_from_query(actual))
+
+def assert_query_eq(expected, actual):
+    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())
+
+    assert query_expected == query_actual, '%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')
+
+ at 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(RequestsMissmatchError(t.assertions)))
+        raise
+    finally:
+        t.shutdown = True
+        t.join(1)
+    if not t.sucess:
+        raise RequestsMissmatchError(t.assertions)
+
+ at 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(RequestsMissmatchError(t.assertions)))
+        raise
+    finally:
+        t.shutdown = True
+        t.join(1)
+    if not t.sucess:
+        raise RequestsMissmatchError(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'))
diff --git a/mapproxy/test/image.py b/mapproxy/test/image.py
new file mode 100644
index 0000000..1fbffb3
--- /dev/null
+++ b/mapproxy/test/image.py
@@ -0,0 +1,209 @@
+# 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
+
+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)
+
+
+ at 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)
+    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)
diff --git a/mapproxy/test/mocker.py b/mapproxy/test/mocker.py
new file mode 100644
index 0000000..902cbaf
--- /dev/null
+++ b/mapproxy/test/mocker.py
@@ -0,0 +1,2268 @@
+"""
+Mocker
+
+Graceful platform for test doubles in Python: mocks, stubs, fakes, and dummies.
+
+Copyright (c) 2007-2010, Gustavo Niemeyer <gustavo at niemeyer.net>
+
+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 <gustavo at niemeyer.net>"
+__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()
+            <record events>
+            with mocker:
+                <reproduce events>
+
+        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():
+                <record events>
+
+        In this case, only expressions in <record events> 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 "<mock>"
+        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)
diff --git a/mapproxy/test/schemas/inspire/common/1.0/common.xsd b/mapproxy/test/schemas/inspire/common/1.0/common.xsd
new file mode 100644
index 0000000..fe6e672
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/common.xsd
@@ -0,0 +1,1461 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" xmlns:jaxb="http://java.sun.com/xml/ns/jaxb" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1" jaxb:version="2.0">
+<!--
+26-APR-2011 1.0.1 Conformity element:
+                    Restricted allowed citations to "INSPIRE interoperability of spatial data sets and services", according to the MD regulation,
+                    requirements for the conformity element (page 17 of the MD regulation).
+                  Inspire Themes:
+                    Removed leading blank for German, Bulgarian, Czech and Danish translations
+18-FEB-2011 Added types for encoding for Language Elements in Network Services
+05-FEB-2011 Added comment to classificationOfSpatialDataService
+04-FEB-2011 Renamed codelists as enumerations. No change required in exsiting xml documents.
+02-FEB-2011 Refactored originating controlled vocabulary to derive from citation. No change required in exsiting xml documents.
+31-JAN-2011 Removed language dependent schema implementations. This is now the top schema file.
+-->
+	<xs:annotation>
+		<xs:appinfo>
+			<jaxb:globalBindings typesafeEnumMaxMembers="1000"/>
+		</xs:appinfo>
+	</xs:annotation>
+	<xs:import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="../../../xml.xsd"/>
+	<xs:include schemaLocation="network.xsd"/>
+	<!--Load language dependent types-->
+	<xs:include schemaLocation="enums/enum_bul.xsd"/>
+	<xs:include schemaLocation="enums/enum_cze.xsd"/>
+	<xs:include schemaLocation="enums/enum_dan.xsd"/>
+	<xs:include schemaLocation="enums/enum_dut.xsd"/>
+	<xs:include schemaLocation="enums/enum_eng.xsd"/>
+	<xs:include schemaLocation="enums/enum_est.xsd"/>
+	<xs:include schemaLocation="enums/enum_fin.xsd"/>
+	<xs:include schemaLocation="enums/enum_fre.xsd"/>
+	<xs:include schemaLocation="enums/enum_ger.xsd"/>
+	<xs:include schemaLocation="enums/enum_gle.xsd"/>
+	<xs:include schemaLocation="enums/enum_gre.xsd"/>
+	<xs:include schemaLocation="enums/enum_hun.xsd"/>
+	<xs:include schemaLocation="enums/enum_ita.xsd"/>
+	<xs:include schemaLocation="enums/enum_lav.xsd"/>
+	<xs:include schemaLocation="enums/enum_lit.xsd"/>
+	<xs:include schemaLocation="enums/enum_mlt.xsd"/>
+	<xs:include schemaLocation="enums/enum_pol.xsd"/>
+	<xs:include schemaLocation="enums/enum_por.xsd"/>
+	<xs:include schemaLocation="enums/enum_rum.xsd"/>
+	<xs:include schemaLocation="enums/enum_slo.xsd"/>
+	<xs:include schemaLocation="enums/enum_slv.xsd"/>
+	<xs:include schemaLocation="enums/enum_spa.xsd"/>
+	<xs:include schemaLocation="enums/enum_swe.xsd"/>
+	<xs:element name="SpatialDataService" type="service"/>
+	<xs:element name="SpatialDataSet" type="dataset"/>
+	<xs:element name="SpatialDataSetSeries" type="series"/>
+	<!--TBD
+	<xs:element name="INSPIREResource" type="resource">
+	</xs:element>
+-->
+	<!--Types-->
+	<xs:complexType name="resource" abstract="true">
+		<xs:sequence>
+			<xs:element name="ResourceTitle" type="notEmptyString"/>
+			<xs:element name="ResourceAbstract" type="notEmptyString"/>
+			<xs:element name="ResourceType" type="resourceType"/>
+			<xs:element name="ResourceLocator" type="resourceLocatorType" minOccurs="0" maxOccurs="unbounded">
+				<xs:annotation>
+					<xs:documentation xml:lang="en">Mandatory if a URL is available to obtain more information on
+the resource, and/or access related services.</xs:documentation>
+				</xs:annotation>
+			</xs:element>
+			<xs:element name="MandatoryKeyword" type="keyword" minOccurs="1" maxOccurs="unbounded"/>
+			<xs:element name="Keyword" type="keyword" minOccurs="0" maxOccurs="unbounded">
+				<xs:annotation>
+					<xs:documentation xml:lang="en">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.</xs:documentation>
+				</xs:annotation>
+			</xs:element>
+			<xs:element name="GeographicBoundingBox" type="geographicBoundingBox" minOccurs="0" maxOccurs="unbounded"/>
+			<xs:element name="TemporalReference" type="temporalReference" maxOccurs="unbounded"/>
+			<xs:element name="SpatialResolution" type="spatialResolution" minOccurs="0" maxOccurs="unbounded">
+				<xs:annotation>
+					<xs:documentation xml:lang="en">Mandatory when there is a restriction on the spatial resolution for this service.</xs:documentation>
+				</xs:annotation>
+			</xs:element>
+			<xs:element name="Conformity" type="conformity" maxOccurs="unbounded"/>
+			<xs:element name="ConditionsForAccessAndUse" type="notEmptyString" maxOccurs="unbounded"/>
+			<xs:element name="LimitationsOnPublicAccess" type="notEmptyString" maxOccurs="unbounded"/>
+			<xs:element name="ResponsibleOrganisation" type="responsibleOrganisation" maxOccurs="unbounded"/>
+			<xs:element name="MetadataPointOfContact" type="metadataPointOfContact" maxOccurs="unbounded"/>
+			<xs:element name="MetadataDate" type="iso8601Date"/>
+			<xs:element name="MetadataLanguage" type="euLanguageISO6392B"/>
+		</xs:sequence>
+		<xs:attribute ref="xml:lang"/>
+	</xs:complexType>
+	<xs:complexType name="data" abstract="true">
+		<xs:complexContent>
+			<xs:extension base="resource">
+				<xs:sequence>
+					<xs:element name="UniqueResourceIdentifier" type="uniqueResourceIdentifier" minOccurs="1" maxOccurs="unbounded"/>
+					<xs:element name="ResourceLanguage" type="languageISO6392B" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory if the resource includes textual information.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="TopicCategory" type="topicCategory" maxOccurs="unbounded"/>
+					<xs:element name="Lineage" type="notEmptyString"/>
+				</xs:sequence>
+			</xs:extension>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="dataset">
+		<xs:complexContent>
+			<xs:restriction base="data">
+				<xs:sequence>
+					<xs:element name="ResourceTitle" type="notEmptyString"/>
+					<xs:element name="ResourceAbstract" type="notEmptyString"/>
+					<xs:element name="ResourceType">
+						<xs:simpleType>
+							<xs:restriction base="resourceType">
+								<xs:enumeration value="dataset"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resourceLocatorType" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory if a URL is available to obtain more information on
+the resource, and/or access related services.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="MandatoryKeyword" type="inspireTheme" minOccurs="1" maxOccurs="unbounded"/>
+					<xs:element name="Keyword" type="keyword" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">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.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="GeographicBoundingBox" type="geographicBoundingBox" maxOccurs="unbounded"/>
+					<xs:element name="TemporalReference" type="temporalReference" maxOccurs="unbounded"/>
+					<xs:element name="SpatialResolution" type="spatialResolution" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory when there is a restriction on the spatial resolution for this service.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="Conformity" type="conformity" maxOccurs="unbounded"/>
+					<xs:element name="ConditionsForAccessAndUse" type="notEmptyString" maxOccurs="unbounded"/>
+					<xs:element name="LimitationsOnPublicAccess" type="notEmptyString" maxOccurs="unbounded"/>
+					<xs:element name="ResponsibleOrganisation" type="responsibleOrganisation" maxOccurs="unbounded"/>
+					<xs:element name="MetadataPointOfContact" type="metadataPointOfContact" maxOccurs="unbounded"/>
+					<xs:element name="MetadataDate" type="iso8601Date"/>
+					<xs:element name="MetadataLanguage" type="euLanguageISO6392B"/>
+					<xs:element name="UniqueResourceIdentifier" type="uniqueResourceIdentifier" minOccurs="1" maxOccurs="unbounded"/>
+					<xs:element name="ResourceLanguage" type="languageISO6392B" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory if the resource includes textual information.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="TopicCategory" type="topicCategory" maxOccurs="unbounded"/>
+					<xs:element name="Lineage" type="notEmptyString"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="series">
+		<xs:complexContent>
+			<xs:restriction base="data">
+				<xs:sequence>
+					<xs:element name="ResourceTitle" type="notEmptyString"/>
+					<xs:element name="ResourceAbstract" type="notEmptyString"/>
+					<xs:element name="ResourceType">
+						<xs:simpleType>
+							<xs:restriction base="resourceType">
+								<xs:enumeration value="series"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resourceLocatorType" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory if a URL is available to obtain more information on
+the resource, and/or access related services.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="MandatoryKeyword" type="inspireTheme" minOccurs="1" maxOccurs="unbounded"/>
+					<xs:element name="Keyword" type="keyword" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">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.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="GeographicBoundingBox" type="geographicBoundingBox" maxOccurs="unbounded"/>
+					<xs:element name="TemporalReference" type="temporalReference" maxOccurs="unbounded"/>
+					<xs:element name="SpatialResolution" type="spatialResolution" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory when there is a restriction on the spatial resolution for this service.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="Conformity" type="conformity" maxOccurs="unbounded"/>
+					<xs:element name="ConditionsForAccessAndUse" type="notEmptyString" maxOccurs="unbounded"/>
+					<xs:element name="LimitationsOnPublicAccess" type="notEmptyString" maxOccurs="unbounded"/>
+					<xs:element name="ResponsibleOrganisation" type="responsibleOrganisation" maxOccurs="unbounded"/>
+					<xs:element name="MetadataPointOfContact" type="metadataPointOfContact" maxOccurs="unbounded"/>
+					<xs:element name="MetadataDate" type="iso8601Date"/>
+					<xs:element name="MetadataLanguage" type="euLanguageISO6392B"/>
+					<xs:element name="UniqueResourceIdentifier" type="uniqueResourceIdentifier" minOccurs="1" maxOccurs="unbounded"/>
+					<xs:element name="ResourceLanguage" type="languageISO6392B" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory if the resource includes textual information.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="TopicCategory" type="topicCategory" maxOccurs="unbounded"/>
+					<xs:element name="Lineage" type="notEmptyString"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Service-->
+	<xs:complexType name="service_ext" abstract="true">
+		<xs:complexContent>
+			<xs:extension base="resource">
+				<xs:sequence>
+					<xs:element name="CoupledResource" type="uniqueResourceIdentifier" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory if linkage to data sets on which the service operates are available.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="SpatialDataServiceType" type="spatialDataServiceType"/>
+				</xs:sequence>
+			</xs:extension>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="service">
+		<xs:complexContent>
+			<xs:restriction base="service_ext">
+				<xs:sequence>
+					<xs:element name="ResourceTitle" type="notEmptyString"/>
+					<xs:element name="ResourceAbstract" type="notEmptyString"/>
+					<xs:element name="ResourceType" type="serviceSpatialDataResourceType"/>
+					<xs:element name="ResourceLocator" type="resourceLocatorType" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory if linkage to the service is available</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="MandatoryKeyword" type="classificationOfSpatialDataService" minOccurs="1" maxOccurs="unbounded"/>
+					<xs:element name="Keyword" type="keyword" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">If the resource is a spatial data service, at least one keyword from Part D.4 shall be provided.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="GeographicBoundingBox" type="geographicBoundingBox" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory for services with an explicit geographic extent.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="TemporalReference" type="temporalReference" maxOccurs="unbounded"/>
+					<xs:element name="SpatialResolution" type="spatialResolution" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory when there is a restriction on the spatial resolution for this service.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="Conformity" type="conformity" maxOccurs="unbounded"/>
+					<xs:element name="ConditionsForAccessAndUse" type="notEmptyString" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation>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.
+			</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="LimitationsOnPublicAccess" type="notEmptyString" maxOccurs="unbounded"/>
+					<xs:element name="ResponsibleOrganisation" type="responsibleOrganisation" maxOccurs="unbounded"/>
+					<xs:element name="MetadataPointOfContact" type="metadataPointOfContact" maxOccurs="unbounded"/>
+					<xs:element name="MetadataDate" type="iso8601Date"/>
+					<xs:element name="MetadataLanguage" type="euLanguageISO6392B"/>
+					<xs:element name="CoupledResource" type="uniqueResourceIdentifier" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory if linkage to data sets on which the service operates are available.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="SpatialDataServiceType" type="spatialDataServiceType"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Shared types-->
+	<xs:element name="SpatialDataServiceType" type="spatialDataServiceType"/>
+	<xs:simpleType name="serviceSpatialDataResourceType">
+		<xs:restriction base="resourceType">
+			<xs:enumeration value="service"/>
+		</xs:restriction>
+	</xs:simpleType>
+	<xs:simpleType name="viewSpatialDataServiceType">
+		<xs:restriction base="spatialDataServiceType">
+			<xs:enumeration value="view"/>
+		</xs:restriction>
+	</xs:simpleType>
+	<xs:simpleType name="discoverySpatialDataServiceType">
+		<xs:restriction base="spatialDataServiceType">
+			<xs:enumeration value="discovery"/>
+		</xs:restriction>
+	</xs:simpleType>
+	<xs:simpleType name="transformationSpatialDataServiceType">
+		<xs:restriction base="spatialDataServiceType">
+			<xs:enumeration value="transformation"/>
+		</xs:restriction>
+	</xs:simpleType>
+	<xs:simpleType name="invokeSpatialDataServiceType">
+		<xs:restriction base="spatialDataServiceType">
+			<xs:enumeration value="invoke"/>
+		</xs:restriction>
+	</xs:simpleType>
+	<xs:simpleType name="otherSpatialDataServiceType">
+		<xs:restriction base="spatialDataServiceType">
+			<xs:enumeration value="other"/>
+		</xs:restriction>
+	</xs:simpleType>
+	<xs:complexType name="metadataPointOfContact">
+		<xs:sequence>
+			<xs:element name="OrganisationName" type="xs:string"/>
+			<xs:element name="EmailAddress" type="emailType"/>
+		</xs:sequence>
+	</xs:complexType>
+	<xs:complexType name="responsibleOrganisation">
+		<xs:sequence>
+			<xs:element name="ResponsibleParty">
+				<xs:complexType>
+					<xs:sequence>
+						<xs:element name="OrganisationName" type="notEmptyString"/>
+						<xs:element name="EmailAddress" type="emailType"/>
+					</xs:sequence>
+				</xs:complexType>
+			</xs:element>
+			<xs:element name="ResponsiblePartyRole" type="responsiblePartyRole"/>
+		</xs:sequence>
+	</xs:complexType>
+	<xs:complexType name="citation">
+		<xs:sequence>
+			<xs:element name="Title" type="notEmptyString"/>
+			<xs:choice>
+				<xs:element name="DateOfPublication" type="iso8601Date"/>
+				<xs:element name="DateOfCreation" type="iso8601Date"/>
+				<xs:element name="DateOfLastRevision" type="iso8601Date"/>
+			</xs:choice>
+			<xs:element name="URI" type="xs:anyURI" minOccurs="0" maxOccurs="unbounded">
+				<xs:annotation>
+					<xs:documentation>One or more Documentation URIs if available</xs:documentation>
+				</xs:annotation>
+			</xs:element>
+			<xs:element name="ResourceLocator" type="resourceLocatorType" minOccurs="0" maxOccurs="unbounded">
+				<xs:annotation>
+					<xs:documentation>One or more URLs if available</xs:documentation>
+				</xs:annotation>
+			</xs:element>
+		</xs:sequence>
+	</xs:complexType>
+	<xs:complexType name="citationConformity">
+		<xs:complexContent>
+			<xs:restriction base="citation">
+				<xs:sequence>
+					<xs:element name="Title" type="notEmptyString"/>
+					<xs:choice>
+						<xs:element name="DateOfPublication" type="iso8601Date"/>
+						<xs:element name="DateOfCreation" type="iso8601Date"/>
+						<xs:element name="DateOfLastRevision" type="iso8601Date"/>
+					</xs:choice>
+					<xs:element name="URI" type="xs:anyURI" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation>One or more Documentation URIs if available</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resourceLocatorType" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation>One or more URLs if available</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="conformity">
+		<xs:sequence>
+			<xs:element name="Specification" type="citationConformity"/>
+			<xs:element name="Degree" type="degreeOfConformity"/>
+		</xs:sequence>
+	</xs:complexType>
+	<xs:complexType name="originatingControlledVocabulary">
+		<xs:complexContent>
+			<xs:restriction base="citation">
+				<xs:sequence>
+					<xs:element name="Title" type="notEmptyString"/>
+					<xs:choice>
+						<xs:element name="DateOfPublication" type="iso8601Date"/>
+						<xs:element name="DateOfCreation" type="iso8601Date"/>
+						<xs:element name="DateOfLastRevision" type="iso8601Date"/>
+					</xs:choice>
+					<xs:element name="URI" type="xs:anyURI" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation>One or more Documentation URIs if available</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resourceLocatorType" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation>One or more URLs if available</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="originatingControlledVocabularyGemetInspireThemes">
+		<xs:complexContent>
+			<xs:restriction base="originatingControlledVocabulary">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="GEMET - INSPIRE themes"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2008-06-01"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="originatingControlledVocabularyMD">
+		<xs:complexContent>
+			<xs:restriction base="originatingControlledVocabulary">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="GEMET - INSPIRE themes"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2008-06-01"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="temporalReference">
+		<xs:sequence>
+			<xs:element name="DateOfCreation" type="iso8601Date" minOccurs="0"/>
+			<xs:element name="DateOfLastRevision" type="iso8601Date" minOccurs="0"/>
+			<xs:element name="DateOfPublication" type="iso8601Date" minOccurs="0" maxOccurs="unbounded"/>
+			<xs:element name="TemporalExtent" type="temporalExtent" minOccurs="0" maxOccurs="unbounded"/>
+		</xs:sequence>
+	</xs:complexType>
+	<xs:element name="TemporalExtentElement" abstract="true"/>
+	<xs:element name="IndividualDate" type="iso8601Date" substitutionGroup="TemporalExtentElement"/>
+	<xs:element name="IntervalOfDates" substitutionGroup="TemporalExtentElement">
+		<xs:complexType>
+			<xs:sequence>
+				<xs:element name="StartingDate" type="iso8601Date"/>
+				<xs:element name="EndDate" type="iso8601Date"/>
+			</xs:sequence>
+		</xs:complexType>
+	</xs:element>
+	<xs:complexType name="temporalExtent">
+		<xs:sequence>
+			<xs:element ref="TemporalExtentElement" maxOccurs="unbounded"/>
+		</xs:sequence>
+	</xs:complexType>
+	<xs:simpleType name="keywordValue">
+		<xs:restriction base="notEmptyString"/>
+	</xs:simpleType>
+	<xs:complexType name="keyword">
+		<xs:sequence>
+			<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabulary" minOccurs="0"/>
+			<xs:element name="KeywordValue" type="xs:string"/>
+		</xs:sequence>
+	</xs:complexType>
+	<xs:simpleType name="topicCategory">
+		<xs:restriction base="xs:string">
+			<xs:enumeration value="farming"/>
+			<xs:enumeration value="biota"/>
+			<xs:enumeration value="boundaries"/>
+			<xs:enumeration value="climatologyMeteorologyAtmosphere"/>
+			<xs:enumeration value="economy"/>
+			<xs:enumeration value="elevation"/>
+			<xs:enumeration value="environment"/>
+			<xs:enumeration value="geoscientificInformation"/>
+			<xs:enumeration value="health"/>
+			<xs:enumeration value="imageryBaseMapsEarthCover"/>
+			<xs:enumeration value="intelligenceMilitary"/>
+			<xs:enumeration value="inlandWaters"/>
+			<xs:enumeration value="location"/>
+			<xs:enumeration value="oceans"/>
+			<xs:enumeration value="planningCadastre"/>
+			<xs:enumeration value="society"/>
+			<xs:enumeration value="structure"/>
+			<xs:enumeration value="transportation"/>
+			<xs:enumeration value="utilitiesCommunication"/>
+		</xs:restriction>
+	</xs:simpleType>
+	<xs:complexType name="classificationOfSpatialDataService">
+		<xs:complexContent>
+			<xs:restriction base="keyword">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabulary" minOccurs="0" maxOccurs="0">
+						<xs:annotation>
+							<xs:documentation>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</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="chainDefinitionService"/>
+								<xs:enumeration value="comEncodingService"/>
+								<xs:enumeration value="comGeographicCompressionService"/>
+								<xs:enumeration value="comGeographicFormatConversionService"/>
+								<xs:enumeration value="comMessagingService"/>
+								<xs:enumeration value="comRemoteFileAndExecutableManagement"/>
+								<xs:enumeration value="comService"/>
+								<xs:enumeration value="comTransferService"/>
+								<xs:enumeration value="humanCatalogueViewer"/>
+								<xs:enumeration value="humanChainDefinitionEditor"/>
+								<xs:enumeration value="humanFeatureGeneralizationEditor"/>
+								<xs:enumeration value="humanGeographicDataStructureViewer"/>
+								<xs:enumeration value="humanGeographicFeatureEditor"/>
+								<xs:enumeration value="humanGeographicSpreadsheetViewer"/>
+								<xs:enumeration value="humanGeographicSymbolEditor"/>
+								<xs:enumeration value="humanGeographicViewer"/>
+								<xs:enumeration value="humanInteractionService"/>
+								<xs:enumeration value="humanServiceEditor"/>
+								<xs:enumeration value="humanWorkflowEnactmentManager"/>
+								<xs:enumeration value="infoCatalogueService"/>
+								<xs:enumeration value="infoCoverageAccessService"/>
+								<xs:enumeration value="infoFeatureAccessService"/>
+								<xs:enumeration value="infoFeatureTypeService"/>
+								<xs:enumeration value="infoGazetteerService"/>
+								<xs:enumeration value="infoManagementService"/>
+								<xs:enumeration value="infoMapAccessService"/>
+								<xs:enumeration value="infoOrderHandlingService"/>
+								<xs:enumeration value="infoProductAccessService"/>
+								<xs:enumeration value="infoRegistryService"/>
+								<xs:enumeration value="infoSensorDescriptionService"/>
+								<xs:enumeration value="infoStandingOrderService"/>
+								<xs:enumeration value="metadataGeographicAnnotationService"/>
+								<xs:enumeration value="metadataProcessingService"/>
+								<xs:enumeration value="metadataStatisticalCalculationService"/>
+								<xs:enumeration value="spatialCoordinateConversionService"/>
+								<xs:enumeration value="spatialCoordinateTransformationService"/>
+								<xs:enumeration value="spatialCoverageVectorConversionService"/>
+								<xs:enumeration value="spatialDimensionMeasurementService"/>
+								<xs:enumeration value="spatialFeatureGeneralizationService"/>
+								<xs:enumeration value="spatialFeatureManipulationService"/>
+								<xs:enumeration value="spatialFeatureMatchingService"/>
+								<xs:enumeration value="spatialImageCoordinateConversionService"/>
+								<xs:enumeration value="spatialImageGeometryModelConversionService"/>
+								<xs:enumeration value="spatialOrthorectificationService"/>
+								<xs:enumeration value="spatialPositioningService"/>
+								<xs:enumeration value="spatialProcessingService"/>
+								<xs:enumeration value="spatialProximityAnalysisService"/>
+								<xs:enumeration value="spatialRectificationService"/>
+								<xs:enumeration value="spatialRouteDeterminationService"/>
+								<xs:enumeration value="spatialSamplingService"/>
+								<xs:enumeration value="spatialSensorGeometryModelAdjustmentService"/>
+								<xs:enumeration value="spatialSubsettingService"/>
+								<xs:enumeration value="spatialTilingChangeService"/>
+								<xs:enumeration value="subscriptionService"/>
+								<xs:enumeration value="taskManagementService"/>
+								<xs:enumeration value="temporalProcessingService"/>
+								<xs:enumeration value="temporalProximityAnalysisService"/>
+								<xs:enumeration value="temporalReferenceSystemTransformationService"/>
+								<xs:enumeration value="temporalSamplingService"/>
+								<xs:enumeration value="temporalSubsettingService"/>
+								<xs:enumeration value="thematicChangeDetectionService"/>
+								<xs:enumeration value="thematicClassificationService"/>
+								<xs:enumeration value="thematicFeatureGeneralizationService"/>
+								<xs:enumeration value="thematicGeocodingService"/>
+								<xs:enumeration value="thematicGeographicInformationExtractionService"/>
+								<xs:enumeration value="thematicGeoparsingService"/>
+								<xs:enumeration value="thematicGoparameterCalculationService"/>
+								<xs:enumeration value="thematicImageManipulationService"/>
+								<xs:enumeration value="thematicImageProcessingService"/>
+								<xs:enumeration value="thematicImageSynthesisService"/>
+								<xs:enumeration value="thematicImageUnderstandingService"/>
+								<xs:enumeration value="thematicMultibandImageManipulationService"/>
+								<xs:enumeration value="thematicObjectDetectionService"/>
+								<xs:enumeration value="thematicProcessingService"/>
+								<xs:enumeration value="thematicReducedResolutionGenerationService"/>
+								<xs:enumeration value="thematicSpatialCountingService"/>
+								<xs:enumeration value="thematicSubsettingService"/>
+								<xs:enumeration value="workflowEnactmentService"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:simpleType name="notEmptyString">
+		<xs:restriction base="xs:string">
+			<xs:minLength value="1"/>
+		</xs:restriction>
+	</xs:simpleType>
+	<xs:simpleType name="mediaType">
+		<xs:annotation>
+			<xs:documentation>
+	Draft implementation to be refined
+	</xs:documentation>
+		</xs:annotation>
+		<xs:restriction base="xs:string">
+			<xs:enumeration value="text/plain"/>
+			<xs:enumeration value="text/html"/>
+			<xs:enumeration value="text/xml"/>
+			<xs:enumeration value="application/xml"/>
+			<xs:enumeration value="application/json"/>
+			<xs:enumeration value="application/pdf"/>
+			<xs:enumeration value="application/rdf+xml"/>
+			<xs:enumeration value="application/json"/>
+			<xs:enumeration value="application/soap+xml"/>
+			<xs:enumeration value="application/vnd.eu.europa.ec.inspire.resource+xml">
+				<xs:annotation>
+					<xs:documentation>Official mime type for INSPIRE Resources</xs:documentation>
+				</xs:annotation>
+			</xs:enumeration>
+			<xs:enumeration value="application/vnd.google-earth.kml+xml"/>
+			<xs:enumeration value="application/vnd.google-earth.kml"/>
+			<xs:enumeration value="application/vnd.google-earth.kmx"/>
+			<xs:enumeration value="application/vnd.msword"/>
+			<xs:enumeration value="application/vnd.ms-excel"/>
+			<xs:enumeration value="application/vnd.ms-powerpoint"/>
+			<xs:enumeration value="application/vnd.oasis.opendocument.text"/>
+			<xs:enumeration value="application/vnd.oasis.opendocument.spreadsheet"/>
+			<xs:enumeration value="application/vnd.oasis.opendocument.presentation"/>
+			<xs:enumeration value="application/vnd.oasis.opendocument.graphics"/>
+			<xs:enumeration value="application/gml+xml">
+				<xs:annotation>
+					<xs:documentation>Official mime type for OGC GML currently in the registration process of IANA</xs:documentation>
+				</xs:annotation>
+			</xs:enumeration>
+			<xs:enumeration value="application/vnd.ogc.wms_xml">
+				<xs:annotation>
+					<xs:documentation>Unofficial mime type for OGC WMS</xs:documentation>
+				</xs:annotation>
+			</xs:enumeration>
+			<xs:enumeration value="application/vnd.ogc.csw_xml">
+				<xs:annotation>
+					<xs:documentation>Unofficial mime type for OGC CSW</xs:documentation>
+				</xs:annotation>
+			</xs:enumeration>
+			<xs:enumeration value="application/vnd.ogc.csw.capabilities.response_xml">
+				<xs:annotation>
+					<xs:documentation>Unofficial mime type for OGC CSW Capabilities response document</xs:documentation>
+				</xs:annotation>
+			</xs:enumeration>
+			<xs:enumeration value="application/vnd.ogc.csw.GetRecordByIdResponse_xml">
+				<xs:annotation>
+					<xs:documentation>Unofficial mime type for OGC CSW Capabilities response document</xs:documentation>
+				</xs:annotation>
+			</xs:enumeration>
+			<xs:enumeration value="application/vnd.ogc.csw.GetRecordsResponse_xml">
+				<xs:annotation>
+					<xs:documentation>Unofficial mime type for OGC CSW Capabilities response document</xs:documentation>
+				</xs:annotation>
+			</xs:enumeration>
+			<xs:enumeration value="application/vnd.ogc.wfs_xml">
+				<xs:annotation>
+					<xs:documentation>Unofficial mime type for OGC WFS</xs:documentation>
+				</xs:annotation>
+			</xs:enumeration>
+			<xs:enumeration value="application/vnd.ogc.se_xml">
+				<xs:annotation>
+					<xs:documentation>Unofficial mime type for OGC Service Exception</xs:documentation>
+				</xs:annotation>
+			</xs:enumeration>
+			<xs:enumeration value="application/vnd.iso.19139+xml">
+				<xs:annotation>
+					<xs:documentation>Unofficial mime type for ISO 19139 metadata</xs:documentation>
+				</xs:annotation>
+			</xs:enumeration>
+		</xs:restriction>
+	</xs:simpleType>
+	<xs:complexType name="resourceLocatorType">
+		<xs:annotation>
+			<xs:documentation xml:lang="en">Mandatory if a URL is available to obtain more information on
+the resource, and/or access related services.</xs:documentation>
+		</xs:annotation>
+		<xs:sequence>
+			<xs:element name="URL" type="xs:anyURI"/>
+			<xs:element name="MediaType" type="mediaType" minOccurs="0" maxOccurs="unbounded"/>
+		</xs:sequence>
+	</xs:complexType>
+	<xs:simpleType name="emailType">
+		<xs:restriction base="xs:string">
+			<xs:pattern value="[^@]+@[^\.]+\..+"/>
+		</xs:restriction>
+	</xs:simpleType>
+	<xs:simpleType name="iso8601Date">
+		<xs:restriction base="xs:string">
+			<xs:annotation>
+				<xs:documentation>Simplified ISO 8601 implementation</xs:documentation>
+			</xs:annotation>
+			<xs:pattern value="\d{4}-(1[0-2]|0[1-9])-(3[0-1]|0[1-9]|[1-2][0-9])(T(2[0-3]|[0-1][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?)?(Z|([+|-](2[0-3]|[0-1][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9])?)?)?">
+				<xs:annotation>
+					<xs:documentation>YEAR MONTH DAY-----------------------TIME--------------------FRACTIONAL SECONDS---------TIME ZONE</xs:documentation>
+				</xs:annotation>
+			</xs:pattern>
+		</xs:restriction>
+	</xs:simpleType>
+	<xs:simpleType name="resourceType">
+		<xs:restriction base="xs:string">
+			<xs:enumeration value="dataset"/>
+			<xs:enumeration value="series"/>
+			<xs:enumeration value="service"/>
+		</xs:restriction>
+	</xs:simpleType>
+	<xs:simpleType name="spatialDataServiceType">
+		<xs:restriction base="xs:string">
+			<xs:enumeration value="discovery"/>
+			<xs:enumeration value="view"/>
+			<xs:enumeration value="download"/>
+			<xs:enumeration value="transformation"/>
+			<xs:enumeration value="invoke"/>
+			<xs:enumeration value="other"/>
+		</xs:restriction>
+	</xs:simpleType>
+	<xs:simpleType name="degreeOfConformity">
+		<xs:restriction base="xs:string">
+			<xs:enumeration value="conformant"/>
+			<xs:enumeration value="notConformant"/>
+			<xs:enumeration value="notEvaluated"/>
+		</xs:restriction>
+	</xs:simpleType>
+	<xs:simpleType name="euLanguageISO6392B">
+		<xs:restriction base="languageISO6392B">
+			<xs:enumeration value="bul"/>
+			<xs:enumeration value="cze"/>
+			<xs:enumeration value="dan"/>
+			<xs:enumeration value="dut"/>
+			<xs:enumeration value="eng"/>
+			<xs:enumeration value="est"/>
+			<xs:enumeration value="fin"/>
+			<xs:enumeration value="fre"/>
+			<xs:enumeration value="ger"/>
+			<xs:enumeration value="gre"/>
+			<xs:enumeration value="hun"/>
+			<xs:enumeration value="gle"/>
+			<xs:enumeration value="ita"/>
+			<xs:enumeration value="lav"/>
+			<xs:enumeration value="lit"/>
+			<xs:enumeration value="mlt"/>
+			<xs:enumeration value="pol"/>
+			<xs:enumeration value="por"/>
+			<xs:enumeration value="rum"/>
+			<xs:enumeration value="slo"/>
+			<xs:enumeration value="slv"/>
+			<xs:enumeration value="spa"/>
+			<xs:enumeration value="swe"/>
+		</xs:restriction>
+	</xs:simpleType>
+	<xs:simpleType name="languageISO6392B">
+		<xs:restriction base="xs:string">
+			<xs:enumeration value="aar"/>
+			<xs:enumeration value="abk"/>
+			<xs:enumeration value="ace"/>
+			<xs:enumeration value="ach"/>
+			<xs:enumeration value="ada"/>
+			<xs:enumeration value="ady"/>
+			<xs:enumeration value="afa"/>
+			<xs:enumeration value="afh"/>
+			<xs:enumeration value="afr"/>
+			<xs:enumeration value="ain"/>
+			<xs:enumeration value="aka"/>
+			<xs:enumeration value="akk"/>
+			<xs:enumeration value="alb"/>
+			<xs:enumeration value="ale"/>
+			<xs:enumeration value="alg"/>
+			<xs:enumeration value="alt"/>
+			<xs:enumeration value="amh"/>
+			<xs:enumeration value="ang"/>
+			<xs:enumeration value="anp"/>
+			<xs:enumeration value="apa"/>
+			<xs:enumeration value="ara"/>
+			<xs:enumeration value="arc"/>
+			<xs:enumeration value="arg"/>
+			<xs:enumeration value="arm"/>
+			<xs:enumeration value="arn"/>
+			<xs:enumeration value="arp"/>
+			<xs:enumeration value="art"/>
+			<xs:enumeration value="arw"/>
+			<xs:enumeration value="asm"/>
+			<xs:enumeration value="ast"/>
+			<xs:enumeration value="ath"/>
+			<xs:enumeration value="aus"/>
+			<xs:enumeration value="ava"/>
+			<xs:enumeration value="ave"/>
+			<xs:enumeration value="awa"/>
+			<xs:enumeration value="aym"/>
+			<xs:enumeration value="aze"/>
+			<xs:enumeration value="bad"/>
+			<xs:enumeration value="bai"/>
+			<xs:enumeration value="bak"/>
+			<xs:enumeration value="bal"/>
+			<xs:enumeration value="bam"/>
+			<xs:enumeration value="ban"/>
+			<xs:enumeration value="baq"/>
+			<xs:enumeration value="bas"/>
+			<xs:enumeration value="bat"/>
+			<xs:enumeration value="bej"/>
+			<xs:enumeration value="bel"/>
+			<xs:enumeration value="bem"/>
+			<xs:enumeration value="ben"/>
+			<xs:enumeration value="ber"/>
+			<xs:enumeration value="bho"/>
+			<xs:enumeration value="bih"/>
+			<xs:enumeration value="bik"/>
+			<xs:enumeration value="bin"/>
+			<xs:enumeration value="bis"/>
+			<xs:enumeration value="bla"/>
+			<xs:enumeration value="bnt"/>
+			<xs:enumeration value="bos"/>
+			<xs:enumeration value="bra"/>
+			<xs:enumeration value="bre"/>
+			<xs:enumeration value="btk"/>
+			<xs:enumeration value="bua"/>
+			<xs:enumeration value="bug"/>
+			<xs:enumeration value="bul"/>
+			<xs:enumeration value="bur"/>
+			<xs:enumeration value="byn"/>
+			<xs:enumeration value="cad"/>
+			<xs:enumeration value="cai"/>
+			<xs:enumeration value="car"/>
+			<xs:enumeration value="cat"/>
+			<xs:enumeration value="cau"/>
+			<xs:enumeration value="ceb"/>
+			<xs:enumeration value="cel"/>
+			<xs:enumeration value="cha"/>
+			<xs:enumeration value="chb"/>
+			<xs:enumeration value="che"/>
+			<xs:enumeration value="chg"/>
+			<xs:enumeration value="chi"/>
+			<xs:enumeration value="chk"/>
+			<xs:enumeration value="chm"/>
+			<xs:enumeration value="chn"/>
+			<xs:enumeration value="cho"/>
+			<xs:enumeration value="chp"/>
+			<xs:enumeration value="chr"/>
+			<xs:enumeration value="chu"/>
+			<xs:enumeration value="chv"/>
+			<xs:enumeration value="chy"/>
+			<xs:enumeration value="cmc"/>
+			<xs:enumeration value="cop"/>
+			<xs:enumeration value="cor"/>
+			<xs:enumeration value="cos"/>
+			<xs:enumeration value="cpe"/>
+			<xs:enumeration value="cpf"/>
+			<xs:enumeration value="cpp"/>
+			<xs:enumeration value="cre"/>
+			<xs:enumeration value="crh"/>
+			<xs:enumeration value="crp"/>
+			<xs:enumeration value="csb"/>
+			<xs:enumeration value="cus"/>
+			<xs:enumeration value="cze"/>
+			<xs:enumeration value="dak"/>
+			<xs:enumeration value="dan"/>
+			<xs:enumeration value="dar"/>
+			<xs:enumeration value="day"/>
+			<xs:enumeration value="del"/>
+			<xs:enumeration value="den"/>
+			<xs:enumeration value="dgr"/>
+			<xs:enumeration value="din"/>
+			<xs:enumeration value="div"/>
+			<xs:enumeration value="doi"/>
+			<xs:enumeration value="dra"/>
+			<xs:enumeration value="dsb"/>
+			<xs:enumeration value="dua"/>
+			<xs:enumeration value="dum"/>
+			<xs:enumeration value="dut"/>
+			<xs:enumeration value="dyu"/>
+			<xs:enumeration value="dzo"/>
+			<xs:enumeration value="efi"/>
+			<xs:enumeration value="egy"/>
+			<xs:enumeration value="eka"/>
+			<xs:enumeration value="elx"/>
+			<xs:enumeration value="eng"/>
+			<xs:enumeration value="enm"/>
+			<xs:enumeration value="epo"/>
+			<xs:enumeration value="est"/>
+			<xs:enumeration value="ewe"/>
+			<xs:enumeration value="ewo"/>
+			<xs:enumeration value="fan"/>
+			<xs:enumeration value="fao"/>
+			<xs:enumeration value="fat"/>
+			<xs:enumeration value="fij"/>
+			<xs:enumeration value="fil"/>
+			<xs:enumeration value="fin"/>
+			<xs:enumeration value="fiu"/>
+			<xs:enumeration value="fon"/>
+			<xs:enumeration value="fre"/>
+			<xs:enumeration value="frm"/>
+			<xs:enumeration value="fro"/>
+			<xs:enumeration value="frr"/>
+			<xs:enumeration value="frs"/>
+			<xs:enumeration value="fry"/>
+			<xs:enumeration value="ful"/>
+			<xs:enumeration value="fur"/>
+			<xs:enumeration value="gaa"/>
+			<xs:enumeration value="gay"/>
+			<xs:enumeration value="gba"/>
+			<xs:enumeration value="gem"/>
+			<xs:enumeration value="geo"/>
+			<xs:enumeration value="ger"/>
+			<xs:enumeration value="gez"/>
+			<xs:enumeration value="gil"/>
+			<xs:enumeration value="gla"/>
+			<xs:enumeration value="gle"/>
+			<xs:enumeration value="glg"/>
+			<xs:enumeration value="glv"/>
+			<xs:enumeration value="gmh"/>
+			<xs:enumeration value="goh"/>
+			<xs:enumeration value="gon"/>
+			<xs:enumeration value="gor"/>
+			<xs:enumeration value="got"/>
+			<xs:enumeration value="grb"/>
+			<xs:enumeration value="grc"/>
+			<xs:enumeration value="gre"/>
+			<xs:enumeration value="grn"/>
+			<xs:enumeration value="gsw"/>
+			<xs:enumeration value="guj"/>
+			<xs:enumeration value="gwi"/>
+			<xs:enumeration value="hai"/>
+			<xs:enumeration value="hat"/>
+			<xs:enumeration value="hau"/>
+			<xs:enumeration value="haw"/>
+			<xs:enumeration value="heb"/>
+			<xs:enumeration value="her"/>
+			<xs:enumeration value="hil"/>
+			<xs:enumeration value="him"/>
+			<xs:enumeration value="hin"/>
+			<xs:enumeration value="hit"/>
+			<xs:enumeration value="hmn"/>
+			<xs:enumeration value="hmo"/>
+			<xs:enumeration value="hrv"/>
+			<xs:enumeration value="hsb"/>
+			<xs:enumeration value="hun"/>
+			<xs:enumeration value="hup"/>
+			<xs:enumeration value="iba"/>
+			<xs:enumeration value="ibo"/>
+			<xs:enumeration value="ice"/>
+			<xs:enumeration value="ido"/>
+			<xs:enumeration value="iii"/>
+			<xs:enumeration value="ijo"/>
+			<xs:enumeration value="iku"/>
+			<xs:enumeration value="ile"/>
+			<xs:enumeration value="ilo"/>
+			<xs:enumeration value="ina"/>
+			<xs:enumeration value="inc"/>
+			<xs:enumeration value="ind"/>
+			<xs:enumeration value="ine"/>
+			<xs:enumeration value="inh"/>
+			<xs:enumeration value="ipk"/>
+			<xs:enumeration value="ira"/>
+			<xs:enumeration value="iro"/>
+			<xs:enumeration value="ita"/>
+			<xs:enumeration value="jav"/>
+			<xs:enumeration value="jbo"/>
+			<xs:enumeration value="jpn"/>
+			<xs:enumeration value="jpr"/>
+			<xs:enumeration value="jrb"/>
+			<xs:enumeration value="kaa"/>
+			<xs:enumeration value="kab"/>
+			<xs:enumeration value="kac"/>
+			<xs:enumeration value="kal"/>
+			<xs:enumeration value="kam"/>
+			<xs:enumeration value="kan"/>
+			<xs:enumeration value="kar"/>
+			<xs:enumeration value="kas"/>
+			<xs:enumeration value="kau"/>
+			<xs:enumeration value="kaw"/>
+			<xs:enumeration value="kaz"/>
+			<xs:enumeration value="kbd"/>
+			<xs:enumeration value="kha"/>
+			<xs:enumeration value="khi"/>
+			<xs:enumeration value="khm"/>
+			<xs:enumeration value="kho"/>
+			<xs:enumeration value="kik"/>
+			<xs:enumeration value="kin"/>
+			<xs:enumeration value="kir"/>
+			<xs:enumeration value="kmb"/>
+			<xs:enumeration value="kok"/>
+			<xs:enumeration value="kom"/>
+			<xs:enumeration value="kon"/>
+			<xs:enumeration value="kor"/>
+			<xs:enumeration value="kos"/>
+			<xs:enumeration value="kpe"/>
+			<xs:enumeration value="krc"/>
+			<xs:enumeration value="krl"/>
+			<xs:enumeration value="kro"/>
+			<xs:enumeration value="kru"/>
+			<xs:enumeration value="kua"/>
+			<xs:enumeration value="kum"/>
+			<xs:enumeration value="kur"/>
+			<xs:enumeration value="kut"/>
+			<xs:enumeration value="lad"/>
+			<xs:enumeration value="lah"/>
+			<xs:enumeration value="lam"/>
+			<xs:enumeration value="lao"/>
+			<xs:enumeration value="lat"/>
+			<xs:enumeration value="lav"/>
+			<xs:enumeration value="lez"/>
+			<xs:enumeration value="lim"/>
+			<xs:enumeration value="lin"/>
+			<xs:enumeration value="lit"/>
+			<xs:enumeration value="lol"/>
+			<xs:enumeration value="loz"/>
+			<xs:enumeration value="ltz"/>
+			<xs:enumeration value="lua"/>
+			<xs:enumeration value="lub"/>
+			<xs:enumeration value="lug"/>
+			<xs:enumeration value="lui"/>
+			<xs:enumeration value="lun"/>
+			<xs:enumeration value="luo"/>
+			<xs:enumeration value="lus"/>
+			<xs:enumeration value="mac"/>
+			<xs:enumeration value="mad"/>
+			<xs:enumeration value="mag"/>
+			<xs:enumeration value="mah"/>
+			<xs:enumeration value="mai"/>
+			<xs:enumeration value="mak"/>
+			<xs:enumeration value="mal"/>
+			<xs:enumeration value="man"/>
+			<xs:enumeration value="mao"/>
+			<xs:enumeration value="map"/>
+			<xs:enumeration value="mar"/>
+			<xs:enumeration value="mas"/>
+			<xs:enumeration value="may"/>
+			<xs:enumeration value="mdf"/>
+			<xs:enumeration value="mdr"/>
+			<xs:enumeration value="men"/>
+			<xs:enumeration value="mga"/>
+			<xs:enumeration value="mic"/>
+			<xs:enumeration value="min"/>
+			<xs:enumeration value="mis"/>
+			<xs:enumeration value="mkh"/>
+			<xs:enumeration value="mlg"/>
+			<xs:enumeration value="mlt"/>
+			<xs:enumeration value="mnc"/>
+			<xs:enumeration value="mni"/>
+			<xs:enumeration value="mno"/>
+			<xs:enumeration value="moh"/>
+			<xs:enumeration value="mon"/>
+			<xs:enumeration value="mos"/>
+			<xs:enumeration value="mul"/>
+			<xs:enumeration value="mun"/>
+			<xs:enumeration value="mus"/>
+			<xs:enumeration value="mwl"/>
+			<xs:enumeration value="mwr"/>
+			<xs:enumeration value="myn"/>
+			<xs:enumeration value="myv"/>
+			<xs:enumeration value="nah"/>
+			<xs:enumeration value="nai"/>
+			<xs:enumeration value="nap"/>
+			<xs:enumeration value="nau"/>
+			<xs:enumeration value="nav"/>
+			<xs:enumeration value="nbl"/>
+			<xs:enumeration value="nde"/>
+			<xs:enumeration value="ndo"/>
+			<xs:enumeration value="nds"/>
+			<xs:enumeration value="nep"/>
+			<xs:enumeration value="new"/>
+			<xs:enumeration value="nia"/>
+			<xs:enumeration value="nia"/>
+			<xs:enumeration value="nic"/>
+			<xs:enumeration value="niu"/>
+			<xs:enumeration value="nno"/>
+			<xs:enumeration value="nob"/>
+			<xs:enumeration value="nog"/>
+			<xs:enumeration value="non"/>
+			<xs:enumeration value="nor"/>
+			<xs:enumeration value="nqo"/>
+			<xs:enumeration value="nso"/>
+			<xs:enumeration value="nub"/>
+			<xs:enumeration value="nwc"/>
+			<xs:enumeration value="nya"/>
+			<xs:enumeration value="nym"/>
+			<xs:enumeration value="nyn"/>
+			<xs:enumeration value="nyo"/>
+			<xs:enumeration value="nzi"/>
+			<xs:enumeration value="oci"/>
+			<xs:enumeration value="oji"/>
+			<xs:enumeration value="ori"/>
+			<xs:enumeration value="orm"/>
+			<xs:enumeration value="osa"/>
+			<xs:enumeration value="oss"/>
+			<xs:enumeration value="ota"/>
+			<xs:enumeration value="oto"/>
+			<xs:enumeration value="paa"/>
+			<xs:enumeration value="pag"/>
+			<xs:enumeration value="pal"/>
+			<xs:enumeration value="pam"/>
+			<xs:enumeration value="pan"/>
+			<xs:enumeration value="pap"/>
+			<xs:enumeration value="pau"/>
+			<xs:enumeration value="peo"/>
+			<xs:enumeration value="per"/>
+			<xs:enumeration value="phi"/>
+			<xs:enumeration value="phn"/>
+			<xs:enumeration value="pli"/>
+			<xs:enumeration value="pol"/>
+			<xs:enumeration value="pon"/>
+			<xs:enumeration value="por"/>
+			<xs:enumeration value="pra"/>
+			<xs:enumeration value="pro"/>
+			<xs:enumeration value="pus"/>
+			<xs:enumeration value="qaa-qtz"/>
+			<xs:enumeration value="que"/>
+			<xs:enumeration value="raj"/>
+			<xs:enumeration value="rap"/>
+			<xs:enumeration value="rar"/>
+			<xs:enumeration value="roa"/>
+			<xs:enumeration value="roh"/>
+			<xs:enumeration value="rom"/>
+			<xs:enumeration value="rum"/>
+			<xs:enumeration value="run"/>
+			<xs:enumeration value="rup"/>
+			<xs:enumeration value="rus"/>
+			<xs:enumeration value="sad"/>
+			<xs:enumeration value="sag"/>
+			<xs:enumeration value="sah"/>
+			<xs:enumeration value="sai"/>
+			<xs:enumeration value="sal"/>
+			<xs:enumeration value="sam"/>
+			<xs:enumeration value="san"/>
+			<xs:enumeration value="sas"/>
+			<xs:enumeration value="sat"/>
+			<xs:enumeration value="scn"/>
+			<xs:enumeration value="sco"/>
+			<xs:enumeration value="sel"/>
+			<xs:enumeration value="sem"/>
+			<xs:enumeration value="sga"/>
+			<xs:enumeration value="sgn"/>
+			<xs:enumeration value="shn"/>
+			<xs:enumeration value="sid"/>
+			<xs:enumeration value="sin"/>
+			<xs:enumeration value="sio"/>
+			<xs:enumeration value="sit"/>
+			<xs:enumeration value="sla"/>
+			<xs:enumeration value="slo"/>
+			<xs:enumeration value="slv"/>
+			<xs:enumeration value="sma"/>
+			<xs:enumeration value="sme"/>
+			<xs:enumeration value="smi"/>
+			<xs:enumeration value="smj"/>
+			<xs:enumeration value="smn"/>
+			<xs:enumeration value="smo"/>
+			<xs:enumeration value="sms"/>
+			<xs:enumeration value="sna"/>
+			<xs:enumeration value="snd"/>
+			<xs:enumeration value="snk"/>
+			<xs:enumeration value="sog"/>
+			<xs:enumeration value="som"/>
+			<xs:enumeration value="son"/>
+			<xs:enumeration value="sot"/>
+			<xs:enumeration value="spa"/>
+			<xs:enumeration value="srd"/>
+			<xs:enumeration value="srn"/>
+			<xs:enumeration value="srp"/>
+			<xs:enumeration value="srr"/>
+			<xs:enumeration value="ssa"/>
+			<xs:enumeration value="ssw"/>
+			<xs:enumeration value="suk"/>
+			<xs:enumeration value="sun"/>
+			<xs:enumeration value="sus"/>
+			<xs:enumeration value="sux"/>
+			<xs:enumeration value="swa"/>
+			<xs:enumeration value="swe"/>
+			<xs:enumeration value="syc"/>
+			<xs:enumeration value="syr"/>
+			<xs:enumeration value="tah"/>
+			<xs:enumeration value="tai"/>
+			<xs:enumeration value="tam"/>
+			<xs:enumeration value="tat"/>
+			<xs:enumeration value="tel"/>
+			<xs:enumeration value="tem"/>
+			<xs:enumeration value="ter"/>
+			<xs:enumeration value="tet"/>
+			<xs:enumeration value="tgk"/>
+			<xs:enumeration value="tgl"/>
+			<xs:enumeration value="tha"/>
+			<xs:enumeration value="tib"/>
+			<xs:enumeration value="tig"/>
+			<xs:enumeration value="tir"/>
+			<xs:enumeration value="tiv"/>
+			<xs:enumeration value="tkl"/>
+			<xs:enumeration value="tlh"/>
+			<xs:enumeration value="tli"/>
+			<xs:enumeration value="tmh"/>
+			<xs:enumeration value="tog"/>
+			<xs:enumeration value="ton"/>
+			<xs:enumeration value="tpi"/>
+			<xs:enumeration value="tsi"/>
+			<xs:enumeration value="tsn"/>
+			<xs:enumeration value="tso"/>
+			<xs:enumeration value="tuk"/>
+			<xs:enumeration value="tum"/>
+			<xs:enumeration value="tup"/>
+			<xs:enumeration value="tur"/>
+			<xs:enumeration value="tut"/>
+			<xs:enumeration value="tvl"/>
+			<xs:enumeration value="twi"/>
+			<xs:enumeration value="tyv"/>
+			<xs:enumeration value="udm"/>
+			<xs:enumeration value="uga"/>
+			<xs:enumeration value="uig"/>
+			<xs:enumeration value="ukr"/>
+			<xs:enumeration value="umb"/>
+			<xs:enumeration value="und"/>
+			<xs:enumeration value="urd"/>
+			<xs:enumeration value="uzb"/>
+			<xs:enumeration value="vai"/>
+			<xs:enumeration value="ven"/>
+			<xs:enumeration value="vie"/>
+			<xs:enumeration value="vol"/>
+			<xs:enumeration value="vot"/>
+			<xs:enumeration value="wak"/>
+			<xs:enumeration value="wal"/>
+			<xs:enumeration value="war"/>
+			<xs:enumeration value="was"/>
+			<xs:enumeration value="wel"/>
+			<xs:enumeration value="wen"/>
+			<xs:enumeration value="wln"/>
+			<xs:enumeration value="wol"/>
+			<xs:enumeration value="xal"/>
+			<xs:enumeration value="xho"/>
+			<xs:enumeration value="yao"/>
+			<xs:enumeration value="yap"/>
+			<xs:enumeration value="yid"/>
+			<xs:enumeration value="yor"/>
+			<xs:enumeration value="ypk"/>
+			<xs:enumeration value="zap"/>
+			<xs:enumeration value="zbl"/>
+			<xs:enumeration value="zen"/>
+			<xs:enumeration value="zha"/>
+			<xs:enumeration value="znd"/>
+			<xs:enumeration value="zul"/>
+			<xs:enumeration value="zun"/>
+			<xs:enumeration value="zxx"/>
+			<xs:enumeration value="zza"/>
+		</xs:restriction>
+	</xs:simpleType>
+	<xs:simpleType name="languageIETF">
+		<xs:restriction base="xs:language"/>
+	</xs:simpleType>
+	<xs:simpleType name="euLanguageIETF">
+		<xs:restriction base="languageIETF">
+			<xs:enumeration value="bg"/>
+			<xs:enumeration value="bg-BG"/>
+			<xs:enumeration value="cs"/>
+			<xs:enumeration value="cs-CZ"/>
+			<xs:enumeration value="da"/>
+			<xs:enumeration value="da-DK"/>
+			<xs:enumeration value="nl"/>
+			<xs:enumeration value="nl-NL"/>
+			<xs:enumeration value="en"/>
+			<xs:enumeration value="en-GB"/>
+			<xs:enumeration value="et"/>
+			<xs:enumeration value="et-EE"/>
+			<xs:enumeration value="fi"/>
+			<xs:enumeration value="fi-FI"/>
+			<xs:enumeration value="fr"/>
+			<xs:enumeration value="fr-FR"/>
+			<xs:enumeration value="de"/>
+			<xs:enumeration value="de-DE"/>
+			<xs:enumeration value="de-AT"/>
+			<xs:enumeration value="el"/>
+			<xs:enumeration value="el-GR"/>
+			<xs:enumeration value="hu"/>
+			<xs:enumeration value="hu-HU"/>
+			<xs:enumeration value="ga"/>
+			<xs:enumeration value="ga-IE"/>
+			<xs:enumeration value="it"/>
+			<xs:enumeration value="it-IT"/>
+			<xs:enumeration value="lv"/>
+			<xs:enumeration value="lv-LV"/>
+			<xs:enumeration value="lt"/>
+			<xs:enumeration value="lt-LT"/>
+			<xs:enumeration value="mt"/>
+			<xs:enumeration value="mt-MT"/>
+			<xs:enumeration value="pl"/>
+			<xs:enumeration value="pl-PL"/>
+			<xs:enumeration value="pt"/>
+			<xs:enumeration value="pt-PT"/>
+			<xs:enumeration value="ro"/>
+			<xs:enumeration value="ro-RO"/>
+			<xs:enumeration value="sk"/>
+			<xs:enumeration value="sk-SK"/>
+			<xs:enumeration value="sl"/>
+			<xs:enumeration value="sl-SI"/>
+			<xs:enumeration value="es"/>
+			<xs:enumeration value="es-ES"/>
+			<xs:enumeration value="sv"/>
+			<xs:enumeration value="sv-SE"/>
+		</xs:restriction>
+	</xs:simpleType>
+	<xs:complexType name="languageElement" abstract="true">
+		<xs:sequence>
+			<xs:element name="Language" type="xs:string"/>
+		</xs:sequence>
+	</xs:complexType>
+	<xs:complexType name="languageElementISO6392B">
+		<xs:complexContent>
+			<xs:restriction base="languageElement">
+				<xs:sequence>
+					<xs:element name="Language" type="euLanguageISO6392B"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="languageElementIETF">
+		<xs:complexContent>
+			<xs:restriction base="languageElement">
+				<xs:sequence>
+					<xs:element name="Language" type="euLanguageIETF"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="geographicBoundingBox">
+		<xs:annotation>
+			<xs:documentation xml:lang="en">Mandatory for services with an explicit geographic extent.</xs:documentation>
+		</xs:annotation>
+		<xs:sequence>
+			<xs:element name="East" type="geoBoxDigits"/>
+			<xs:element name="West" type="geoBoxDigits"/>
+			<xs:element name="North" type="geoBoxDigits"/>
+			<xs:element name="South" type="geoBoxDigits"/>
+		</xs:sequence>
+	</xs:complexType>
+	<xs:complexType name="spatialResolution">
+		<xs:annotation>
+			<xs:documentation xml:lang="en">Mandatory if an equivalent scale or a resolution distance is available</xs:documentation>
+			<xs:documentation xml:lang="en">Mandatory for services when there is a restriction on the spatial resolution for service</xs:documentation>
+		</xs:annotation>
+		<xs:choice>
+			<xs:element name="EquivalentScale" type="xs:integer">
+				<xs:annotation>
+					<xs:documentation>An equivalent scale is generally expressed as an integer value expressing the scale denominator</xs:documentation>
+				</xs:annotation>
+			</xs:element>
+			<xs:sequence>
+				<xs:element name="ResolutionDistance" type="xs:integer">
+					<xs:annotation>
+						<xs:documentation>A resolution distance shall be expressed as a numerical value associated with a unit of length</xs:documentation>
+					</xs:annotation>
+				</xs:element>
+				<xs:element name="UnitOfMeasure" type="xs:string"/>
+			</xs:sequence>
+		</xs:choice>
+		<xs:attribute name="abstract" type="xs:string">
+			<xs:annotation>
+				<xs:documentation>
+		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.
+		</xs:documentation>
+			</xs:annotation>
+		</xs:attribute>
+	</xs:complexType>
+	<xs:complexType name="uniqueResourceIdentifier">
+		<xs:sequence>
+			<xs:element name="Code" type="notEmptyString"/>
+			<xs:element name="Namespace" type="xs:anyURI" minOccurs="0"/>
+		</xs:sequence>
+		<xs:attribute name="metadataURL" type="xs:anyURI"/>
+	</xs:complexType>
+	<!--
+	<xs:complexType name="inspireTheme">
+		<xs:complexContent>
+			<xs:restriction base="keyword">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes" minOccurs="0"/>
+					<xs:element name="KeywordValue" type="keywordValue"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+-->
+	<xs:simpleType name="responsiblePartyRole">
+		<xs:restriction base="xs:string">
+			<xs:enumeration value="resourceProvider">
+				<xs:annotation>
+					<xs:documentation xml:lang="en">Resource Provider (resourceProvider): Party that supplies the resource.</xs:documentation>
+				</xs:annotation>
+			</xs:enumeration>
+			<xs:enumeration value="custodian">
+				<xs:annotation>
+					<xs:documentation>6.2. Custodian (custodian): Party that accepts accountability and responsibility for the data and ensures appropriate care and maintenance of the resource.</xs:documentation>
+				</xs:annotation>
+			</xs:enumeration>
+			<xs:enumeration value="owner">
+				<xs:annotation>
+					<xs:documentation>6.3. Owner (owner): Party that owns the resource.</xs:documentation>
+				</xs:annotation>
+			</xs:enumeration>
+			<xs:enumeration value="user">
+				<xs:annotation>
+					<xs:documentation>6.4. User (user): Party who uses the resource.</xs:documentation>
+				</xs:annotation>
+			</xs:enumeration>
+			<xs:enumeration value="distributor">
+				<xs:annotation>
+					<xs:documentation>6.5. Distributor (distributor): Party who distributes the resource.</xs:documentation>
+				</xs:annotation>
+			</xs:enumeration>
+			<xs:enumeration value="originator">
+				<xs:annotation>
+					<xs:documentation>6.6. Originator (originator): Party who created the resource</xs:documentation>
+				</xs:annotation>
+			</xs:enumeration>
+			<xs:enumeration value="pointOfContact">
+				<xs:annotation>
+					<xs:documentation>6.7. Point of Contact (pointOfContact): Party who can be contacted for acquiring knowledge about or acquisition of the resource.</xs:documentation>
+				</xs:annotation>
+			</xs:enumeration>
+			<xs:enumeration value="principalInvestigator">
+				<xs:annotation>
+					<xs:documentation>6.8. Principal Investigator (principalInvestigator): Key party responsible for gathering information and conducting research.</xs:documentation>
+				</xs:annotation>
+			</xs:enumeration>
+			<xs:enumeration value="processor">
+				<xs:annotation>
+					<xs:documentation>6.9. Processor (processor): Party who has processed the data in a manner such that the resource has been modified.</xs:documentation>
+				</xs:annotation>
+			</xs:enumeration>
+			<xs:enumeration value="publisher">
+				<xs:annotation>
+					<xs:documentation>6.10. Publisher (publisher): Party who published the resource.</xs:documentation>
+				</xs:annotation>
+			</xs:enumeration>
+			<xs:enumeration value="author">
+				<xs:annotation>
+					<xs:documentation>6.11. Author (author): Party who authored the resource.</xs:documentation>
+				</xs:annotation>
+			</xs:enumeration>
+		</xs:restriction>
+	</xs:simpleType>
+	<xs:simpleType name="geoBoxDigits">
+		<xs:restriction base="xs:string">
+			<xs:annotation>
+				<xs:documentation xml:lang="en">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.</xs:documentation>
+			</xs:annotation>
+			<xs:pattern value="-?(0+|(0*[1-9]\d*))(\.\d{2,})"/>
+		</xs:restriction>
+	</xs:simpleType>
+	<xs:complexType name="inspireTheme" abstract="true">
+		<xs:complexContent>
+			<xs:restriction base="keyword">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes" minOccurs="1"/>
+					<xs:element name="KeywordValue" type="keywordValue"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_bul.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_bul.xsd
new file mode 100644
index 0000000..7d14d41
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_bul.xsd
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- edited with XMLSpy v2011 sp1 (http://www.altova.com) by - - (European Commission DG JRC IES) -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<xs:complexType name="inspireTheme_bul">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes" minOccurs="1"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="Административни единици"/>
+								<xs:enumeration value="Адреси"/>
+								<xs:enumeration value="Атмосферни условия"/>
+								<xs:enumeration value="Биогеографски региони"/>
+								<xs:enumeration value="Географски координатни системи"/>
+								<xs:enumeration value="Географски наименования"/>
+								<xs:enumeration value="Геология"/>
+								<xs:enumeration value="Енергийни източници"/>
+								<xs:enumeration value="Защитени обекти"/>
+								<xs:enumeration value="Здраве и безопасност на човека"/>
+								<xs:enumeration value="Земна покривка"/>
+								<xs:enumeration value="Кадастрални парцели"/>
+								<xs:enumeration value="Комунално-битови и обществени услуги"/>
+								<xs:enumeration value="Координатни справочни системи"/>
+								<xs:enumeration value="Местообитания и биотопи"/>
+								<xs:enumeration value="Метеорологични географски характеристики"/>
+								<xs:enumeration value="Минерални ресурси"/>
+								<xs:enumeration value="Морски региони"/>
+								<xs:enumeration value="Океанографски географски характеристики"/>
+								<xs:enumeration value="Ортоизображение"/>
+								<xs:enumeration value="Ползване на земята"/>
+								<xs:enumeration value="Почва"/>
+								<xs:enumeration value="Природни рискови зони"/>
+								<xs:enumeration value="Производствени и промишлени съоръжения"/>
+								<xs:enumeration value="Разпределение на видовете"/>
+								<xs:enumeration value="Разпределение на населението — демография"/>
+								<xs:enumeration value="Релеф"/>
+								<xs:enumeration value="Сгради"/>
+								<xs:enumeration value="Селскостопански и водностопански съоръжения"/>
+								<xs:enumeration value="Статистически единици"/>
+								<xs:enumeration value="Съоръжения за управление на околната среда"/>
+								<xs:enumeration value="Транспортни мрежи"/>
+								<xs:enumeration value="Управление на територията/ограничени/регулирани зони и отчетни единици"/>
+								<xs:enumeration value="Хидрография"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_bul">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:BG:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationInspireInteroperabilityRegulation_bul">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="РЕГЛАМЕНТ (ЕС) № 1089/2010 НА КОМИСИЯТА от 23 ноември 2010 година за прилагане на Директива 2007/2/ЕО на Европейския парламент и на Съвета по отношение на оперативната съвместимост на масиви от пространствени данни и услуги за пространствени данни"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:BG:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_bul" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_cze.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_cze.xsd
new file mode 100644
index 0000000..502075a
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_cze.xsd
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- edited with XMLSpy v2011 sp1 (http://www.altova.com) by - - (European Commission DG JRC IES) -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<xs:complexType name="inspireTheme_cze">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes" minOccurs="1"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="Adresy"/>
+								<xs:enumeration value="Bioregiony"/>
+								<xs:enumeration value="Budovy"/>
+								<xs:enumeration value="Chráněná území"/>
+								<xs:enumeration value="Dopravní sítě"/>
+								<xs:enumeration value="Energetické zdroje"/>
+								<xs:enumeration value="Geologie"/>
+								<xs:enumeration value="Katastrální parcely"/>
+								<xs:enumeration value="Krajinný pokryv"/>
+								<xs:enumeration value="Lidské zdraví a bezpečnost"/>
+								<xs:enumeration value="Mořské oblasti"/>
+								<xs:enumeration value="Nadmořská výška"/>
+								<xs:enumeration value="Nerostné suroviny"/>
+								<xs:enumeration value="Oblasti ohrožené přírodními riziky"/>
+								<xs:enumeration value="Ortofotosnímky"/>
+								<xs:enumeration value="Půda"/>
+								<xs:enumeration value="Rozložení druhů"/>
+								<xs:enumeration value="Rozložení obyvatelstva – demografie"/>
+								<xs:enumeration value="Souřadnicové referenční systémy"/>
+								<xs:enumeration value="Správní jednotky"/>
+								<xs:enumeration value="Správní oblasti/chráněná pásma/regulovaná území a jednotky podávající hlášení"/>
+								<xs:enumeration value="Stanoviště a biotopy"/>
+								<xs:enumeration value="Statistické jednotky"/>
+								<xs:enumeration value="Stav ovzduší"/>
+								<xs:enumeration value="Veřejné služby a služby veřejné správy"/>
+								<xs:enumeration value="Vodopis"/>
+								<xs:enumeration value="Výrobní a průmyslová zařízení"/>
+								<xs:enumeration value="Využití území"/>
+								<xs:enumeration value="Zařízení pro sledování životního prostředí"/>
+								<xs:enumeration value="Zemědělská a akvakulturní zařízení"/>
+								<xs:enumeration value="Zeměpisné meteorologické prvky"/>
+								<xs:enumeration value="Zeměpisné názvy"/>
+								<xs:enumeration value="Zeměpisné oceánografické prvky"/>
+								<xs:enumeration value="Zeměpisné soustavy souřadnicových sítí"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_cze">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:CS:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationInspireInteroperabilityRegulation_cze">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="NAŘÍZENÍ KOMISE (EU) č. 1089/2010 ze dne 23. listopadu 2010, kterým se provádí směrnice Evropského parlamentu a Rady 2007/2/ES, pokud jde o interoperabilitu sad prostorových dat a služeb prostorových dat"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:CS:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_cze" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_dan.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_dan.xsd
new file mode 100644
index 0000000..fdb5ff9
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_dan.xsd
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- edited with XMLSpy v2011 sp1 (http://www.altova.com) by - - (European Commission DG JRC IES) -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<xs:complexType name="inspireTheme_dan">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes" minOccurs="1"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="Administrative enheder"/>
+								<xs:enumeration value="Adresser"/>
+								<xs:enumeration value="Arealanvendelse"/>
+								<xs:enumeration value="Arealdække"/>
+								<xs:enumeration value="Artsfordeling"/>
+								<xs:enumeration value="Atmosfæriske forhold"/>
+								<xs:enumeration value="Befolkningsfordeling — demografi"/>
+								<xs:enumeration value="Beskyttede lokaliteter"/>
+								<xs:enumeration value="Biogeografiske regioner"/>
+								<xs:enumeration value="Bygninger"/>
+								<xs:enumeration value="Energiressourcer"/>
+								<xs:enumeration value="Forvaltede og regulerede områder samt områder med brugsbegrænsning og indberetningsenheder"/>
+								<xs:enumeration value="Geografiske kvadratnetsystemer"/>
+								<xs:enumeration value="Geologi"/>
+								<xs:enumeration value="Havområder"/>
+								<xs:enumeration value="Højde"/>
+								<xs:enumeration value="Hydrograf"/>
+								<xs:enumeration value="Jord"/>
+								<xs:enumeration value="Koordinatsystemer"/>
+								<xs:enumeration value="Landbrugs- og akvakulturanlæg"/>
+								<xs:enumeration value="Levesteder og biotoper"/>
+								<xs:enumeration value="Matrikulære parceller"/>
+								<xs:enumeration value="Meteorologisk-geografiske forhold"/>
+								<xs:enumeration value="Miljøovervågningsfaciliteter"/>
+								<xs:enumeration value="Mineralressourcer"/>
+								<xs:enumeration value="Oceanografiske/geografiske forhold"/>
+								<xs:enumeration value="Offentlig forsyningsvirksomhed og offentlige tjenesteydelser"/>
+								<xs:enumeration value="Områder med naturlige risici"/>
+								<xs:enumeration value="Ortofoto"/>
+								<xs:enumeration value="Produktions- og industrifaciliteter"/>
+								<xs:enumeration value="Sikkerhed"/>
+								<xs:enumeration value="Statistiske enheder"/>
+								<xs:enumeration value="Stednavne"/>
+								<xs:enumeration value="Transportnet"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_dan">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:DA:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationInspireInteroperabilityRegulation_dan">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="KOMMISSIONENS FORORDNING (EU) Nr. 1089/2010 af 23. november 2010 om gennemførelse af Europa-Parlamentets og Rådets direktiv 2007/2/EF for så vidt angår interoperabilitet for geodatasæt og -tjenester"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:DA:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_dan" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_dut.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_dut.xsd
new file mode 100644
index 0000000..cdc8044
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_dut.xsd
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- edited with XMLSpy v2011 sp1 (http://www.altova.com) by - - (European Commission DG JRC IES) -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<xs:complexType name="inspireTheme_dut">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes" minOccurs="1"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="Administratieve eenheden"/>
+								<xs:enumeration value="Adressen"/>
+								<xs:enumeration value="Atmosferische omstandigheden"/>
+								<xs:enumeration value="Beschermde gebieden"/>
+								<xs:enumeration value="Biogeografische gebieden"/>
+								<xs:enumeration value="Bodem"/>
+								<xs:enumeration value="Bodemgebruik"/>
+								<xs:enumeration value="Energiebronnen"/>
+								<xs:enumeration value="Faciliteiten voor landbouw en aquacultuur"/>
+								<xs:enumeration value="Faciliteiten voor productie en industrie"/>
+								<xs:enumeration value="Gebieden met natuurrisico'es"/>
+								<xs:enumeration value="Gebiedsbeheer, gebieden waar beperkingen gelden, gereguleerde gebieden en rapportage-eenheden"/>
+								<xs:enumeration value="Gebouwen"/>
+								<xs:enumeration value="Geografisch rastersysteem"/>
+								<xs:enumeration value="Geografische namen"/>
+								<xs:enumeration value="Geologie"/>
+								<xs:enumeration value="Habitats en biotopen"/>
+								<xs:enumeration value="Hoogte"/>
+								<xs:enumeration value="Hydrografie"/>
+								<xs:enumeration value="Kadastrale percelen"/>
+								<xs:enumeration value="Landgebruik"/>
+								<xs:enumeration value="Menselijke gezondheid en veiligheid"/>
+								<xs:enumeration value="Meteorologische geografische kenmerken"/>
+								<xs:enumeration value="Milieubewakingsvoorzieningen"/>
+								<xs:enumeration value="Minerale bronnen"/>
+								<xs:enumeration value="Nutsdiensten en overheidsdiensten"/>
+								<xs:enumeration value="Oceanografische geografische kenmerken"/>
+								<xs:enumeration value="Orthobeeldvorming"/>
+								<xs:enumeration value="Spreiding van de bevolking — demografie"/>
+								<xs:enumeration value="Spreiding van soorten"/>
+								<xs:enumeration value="Statistische eenheden"/>
+								<xs:enumeration value="Systemen voor verwijzing door middel van coördinaten"/>
+								<xs:enumeration value="Vervoersnetwerken"/>
+								<xs:enumeration value="Zeegebieden"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_dut">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:NL:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationInspireInteroperabilityRegulation_dut">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="VERORDENING (EU) Nr. 1089/2010 VAN DE COMMISSIE van 23 november 2010 ter uitvoering van Richtlijn 2007/2/EG van het Europees Parlement en de Raad betreffende de interoperabiliteit van verzamelingen ruimtelijke gegevens en van diensten met betrekking tot ruimtelijke gegevens"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:NL:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_dut" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_eng.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_eng.xsd
new file mode 100644
index 0000000..0d4814c
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_eng.xsd
@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<xs:complexType name="inspireTheme_eng">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes" minOccurs="1"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="Addresses"/>
+								<xs:enumeration value="Administrative units"/>
+								<xs:enumeration value="Agricultural and aquaculture facilities"/>
+								<xs:enumeration value="Area management/restriction/regulation zones and reporting units"/>
+								<xs:enumeration value="Atmospheric conditions"/>
+								<xs:enumeration value="Bio-geographical regions"/>
+								<xs:enumeration value="Buildings"/>
+								<xs:enumeration value="Cadastral parcels"/>
+								<xs:enumeration value="Coordinate reference systems"/>
+								<xs:enumeration value="Elevation"/>
+								<xs:enumeration value="Energy resources"/>
+								<xs:enumeration value="Environmental monitoring facilities"/>
+								<xs:enumeration value="Geographical grid systems"/>
+								<xs:enumeration value="Geographical names"/>
+								<xs:enumeration value="Geology"/>
+								<xs:enumeration value="Habitats and biotopes"/>
+								<xs:enumeration value="Human health and safety"/>
+								<xs:enumeration value="Hydrography"/>
+								<xs:enumeration value="Land cover"/>
+								<xs:enumeration value="Land use"/>
+								<xs:enumeration value="Meteorological geographical features"/>
+								<xs:enumeration value="Mineral resources"/>
+								<xs:enumeration value="Natural risk zones"/>
+								<xs:enumeration value="Oceanographic geographical features"/>
+								<xs:enumeration value="Orthoimagery"/>
+								<xs:enumeration value="Population distribution — demography"/>
+								<xs:enumeration value="Production and industrial facilities"/>
+								<xs:enumeration value="Protected sites"/>
+								<xs:enumeration value="Sea regions"/>
+								<xs:enumeration value="Soil"/>
+								<xs:enumeration value="Species distribution"/>
+								<xs:enumeration value="Statistical units"/>
+								<xs:enumeration value="Transport networks"/>
+								<xs:enumeration value="Utility and governmental services"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="resLocGEMETInspireThemes_eng">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://www.eionet.europa.eu/gemet/inspire_themes?langcode=en"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="text/html"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationGEMETInspireThemes_eng">
+		<xs:complexContent>
+			<xs:restriction base="citation">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="GEMET - INSPIRE themes"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2008-06-01"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="0" maxOccurs="0"/>
+					<xs:element name="ResourceLocator" type="resLocGEMETInspireThemes_eng" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_eng">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:EN:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationInspireInteroperabilityRegulation_eng">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="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"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:EN:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_eng" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_est.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_est.xsd
new file mode 100644
index 0000000..7fb22cf
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_est.xsd
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- edited with XMLSpy v2011 sp1 (http://www.altova.com) by - - (European Commission DG JRC IES) -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<xs:complexType name="inspireTheme_est">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="Aadressid"/>
+								<xs:enumeration value="Atmosfääritingimused"/>
+								<xs:enumeration value="Bio-geograafilised piirkonnad"/>
+								<xs:enumeration value="Ehitised"/>
+								<xs:enumeration value="Elanikkonna jaotumine – demograafia"/>
+								<xs:enumeration value="Elupaigad ja biotoobid"/>
+								<xs:enumeration value="Energiaressursid"/>
+								<xs:enumeration value="Geograafilised nimed"/>
+								<xs:enumeration value="Geograafilised ruutvõrgud"/>
+								<xs:enumeration value="Geoloogia"/>
+								<xs:enumeration value="Haldusüksused"/>
+								<xs:enumeration value="Hüdrograafia"/>
+								<xs:enumeration value="Inimeste tervis ja ohutus"/>
+								<xs:enumeration value="Kaitsealused kohad"/>
+								<xs:enumeration value="Katastriüksused"/>
+								<xs:enumeration value="Keskkonnaseirerajatised"/>
+								<xs:enumeration value="Kommunaal- ja riiklikud teenused"/>
+								<xs:enumeration value="Koordinaatsüsteemid"/>
+								<xs:enumeration value="Kõrgused"/>
+								<xs:enumeration value="Liikide jaotumine"/>
+								<xs:enumeration value="Looduslikud ohutsoonid"/>
+								<xs:enumeration value="Maakasutus"/>
+								<xs:enumeration value="Maakate"/>
+								<xs:enumeration value="Maavarad"/>
+								<xs:enumeration value="Merepiirkonnad"/>
+								<xs:enumeration value="Meteoroloogilis-geograafilised tunnusjooned"/>
+								<xs:enumeration value="Okeanograafilis-geograafilised tunnusjooned"/>
+								<xs:enumeration value="Ortokujutised"/>
+								<xs:enumeration value="Pinnas"/>
+								<xs:enumeration value="Põllumajandus- ja vesiviljelusrajatised"/>
+								<xs:enumeration value="Statistilised üksused"/>
+								<xs:enumeration value="Tootmis- ja tööstusrajatised"/>
+								<xs:enumeration value="Transpordivõrgud"/>
+								<xs:enumeration value="Üldplaneering/piirangu-/reguleeritud tsoonid ja aruandlusüksused"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_est">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:ET:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationInspireInteroperabilityRegulation_est">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="KOMISJONI MÄÄRUS (EL) nr 1089/2010, 23. november 2010, millega rakendatakse Euroopa Parlamendi ja nõukogu direktiivi 2007/2/EÜ seoses ruumiandmekogumite ja -teenuste ristkasutatavusega"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:ET:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_est" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_fin.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_fin.xsd
new file mode 100644
index 0000000..4c33584
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_fin.xsd
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- edited with XMLSpy v2011 sp1 (http://www.altova.com) by - - (European Commission DG JRC IES) -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<xs:complexType name="inspireTheme_fin">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="Aluesuunnittelun, rajoitusten ja sääntelyn piiriin kuuluvat alueet ja raportointiyksiköt"/>
+								<xs:enumeration value="Biomaantieteelliset alueet"/>
+								<xs:enumeration value="Elinympäristöt ja biotoopit"/>
+								<xs:enumeration value="Energiavarat"/>
+								<xs:enumeration value="Geologia"/>
+								<xs:enumeration value="Hallinnolliset yksiköt"/>
+								<xs:enumeration value="Hydrografia"/>
+								<xs:enumeration value="Ilmakehän tila"/>
+								<xs:enumeration value="Ilmaston maantieteelliset ominaispiirteet"/>
+								<xs:enumeration value="Kiinteistöt"/>
+								<xs:enumeration value="Koordinaattijärjestelmät"/>
+								<xs:enumeration value="Korkeus"/>
+								<xs:enumeration value="Lajien levinneisyys"/>
+								<xs:enumeration value="Liikenneverkot"/>
+								<xs:enumeration value="Luonnonriskialueet"/>
+								<xs:enumeration value="Maankäyttö"/>
+								<xs:enumeration value="Maanpeite"/>
+								<xs:enumeration value="Maaperä"/>
+								<xs:enumeration value="Maatalous- ja vesiviljelylaitokset"/>
+								<xs:enumeration value="Merentutkimuksen maantieteelliset ominaispiirteet"/>
+								<xs:enumeration value="Merialueet"/>
+								<xs:enumeration value="Mineraalivarat"/>
+								<xs:enumeration value="Ortoilmakuvat"/>
+								<xs:enumeration value="Osoitteet"/>
+								<xs:enumeration value="Paikannimet"/>
+								<xs:enumeration value="Paikannusruudustot"/>
+								<xs:enumeration value="Rakennukset"/>
+								<xs:enumeration value="Suojellut alueet"/>
+								<xs:enumeration value="Tilastoyksiköt"/>
+								<xs:enumeration value="Tuotanto- ja teollisuuslaitokset"/>
+								<xs:enumeration value="Väestöjakauma – demografia"/>
+								<xs:enumeration value="Väestön terveys ja turvallisuus"/>
+								<xs:enumeration value="Yleishyödylliset ja muut julkiset palvelut"/>
+								<xs:enumeration value="Ympäristön tilan seurantalaitteet"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_fin">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:FI:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationInspireInteroperabilityRegulation_fin">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="KOMISSION ASETUS (EU) N:o 1089/2010, annettu 23 päivänä marraskuuta 2010, Euroopan parlamentin ja neuvoston direktiivin 2007/2/EY täytäntöönpanosta paikkatietoaineistojen ja -palvelujen yhteentoimivuuden osalta"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:FI:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_fin" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_fre.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_fre.xsd
new file mode 100644
index 0000000..61090d9
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_fre.xsd
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- edited with XMLSpy v2011 sp1 (http://www.altova.com) by - - (European Commission DG JRC IES) -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<xs:complexType name="inspireTheme_fre">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="Adresses"/>
+								<xs:enumeration value="Altitude"/>
+								<xs:enumeration value="Bâtiments"/>
+								<xs:enumeration value="Caractéristiques géographiques météorologiques"/>
+								<xs:enumeration value="Caractéristiques géographiques océanographiques"/>
+								<xs:enumeration value="Conditions atmosphériques"/>
+								<xs:enumeration value="Dénominations géographiques"/>
+								<xs:enumeration value="Géologie"/>
+								<xs:enumeration value="Habitats et biotopes"/>
+								<xs:enumeration value="Hydrographie"/>
+								<xs:enumeration value="Installations agricoles et aquacoles"/>
+								<xs:enumeration value="Installations de suivi environnemental"/>
+								<xs:enumeration value="Lieux de production et sites industriels"/>
+								<xs:enumeration value="Occupation des terres"/>
+								<xs:enumeration value="Ortho-imagerie"/>
+								<xs:enumeration value="Parcelles cadastrales"/>
+								<xs:enumeration value="Référentiels de coordonnées"/>
+								<xs:enumeration value="Régions biogéographiques"/>
+								<xs:enumeration value="Régions maritimes"/>
+								<xs:enumeration value="Répartition de la population — démographie"/>
+								<xs:enumeration value="Répartition des espèces"/>
+								<xs:enumeration value="Réseaux de transport"/>
+								<xs:enumeration value="Ressources minérales"/>
+								<xs:enumeration value="Santé et sécurité des personnes"/>
+								<xs:enumeration value="Services d'utilité publique et services publics"/>
+								<xs:enumeration value="Sites protégés"/>
+								<xs:enumeration value="Sols"/>
+								<xs:enumeration value="Sources d'énergie"/>
+								<xs:enumeration value="Systèmes de maillage géographique"/>
+								<xs:enumeration value="Unités administratives"/>
+								<xs:enumeration value="Unités statistiques"/>
+								<xs:enumeration value="Usage des sols"/>
+								<xs:enumeration value="Zones à risque naturel"/>
+								<xs:enumeration value="Zones de gestion, de restriction ou de réglementation et unités de déclaration"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_fre">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:FR:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationInspireInteroperabilityRegulation_fre">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="RÈGLEMENT (UE) N o 1089/2010 DE LA COMMISSION du 23 novembre 2010 portant modalités d'application de la directive 2007/2/CE du Parlement européen et du Conseil en ce qui concerne l'interopérabilité des séries et des services de données géographiques"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:FR:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_fre" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_ger.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_ger.xsd
new file mode 100644
index 0000000..d9b6140
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_ger.xsd
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- edited with XMLSpy v2011 sp1 (http://www.altova.com) by - - (European Commission DG JRC IES) -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<xs:complexType name="inspireTheme_ger">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="Adressen"/>
+								<xs:enumeration value="Atmosphärische Bedingungen"/>
+								<xs:enumeration value="Bewirtschaftungsgebiete/Schutzgebiete/geregelte Gebiete und Berichterstattungseinheiten"/>
+								<xs:enumeration value="Biogeografische Regionen"/>
+								<xs:enumeration value="Boden"/>
+								<xs:enumeration value="Bodenbedeckung"/>
+								<xs:enumeration value="Bodennutzung"/>
+								<xs:enumeration value="Energiequellen"/>
+								<xs:enumeration value="Flurstücke/Grundstücke (Katasterparzellen)"/>
+								<xs:enumeration value="Gebäude"/>
+								<xs:enumeration value="Gebiete mit naturbedingten Risiken"/>
+								<xs:enumeration value="Geografische Bezeichnungen"/>
+								<xs:enumeration value="Geografische Gittersysteme"/>
+								<xs:enumeration value="Geologie"/>
+								<xs:enumeration value="Gesundheit und Sicherheit"/>
+								<xs:enumeration value="Gewässernetz"/>
+								<xs:enumeration value="Höhe"/>
+								<xs:enumeration value="Koordinatenreferenzsysteme"/>
+								<xs:enumeration value="Landwirtschaftliche Anlagen und Aquakulturanlagen"/>
+								<xs:enumeration value="Lebensräume und Biotope"/>
+								<xs:enumeration value="Meeresregionen"/>
+								<xs:enumeration value="Meteorologisch-geografische Kennwerte"/>
+								<xs:enumeration value="Mineralische Bodenschätze"/>
+								<xs:enumeration value="Orthofotografie"/>
+								<xs:enumeration value="Ozeanografisch-geografische Kennwerte"/>
+								<xs:enumeration value="Produktions- und Industrieanlagen"/>
+								<xs:enumeration value="Schutzgebiete"/>
+								<xs:enumeration value="Statistische Einheiten"/>
+								<xs:enumeration value="Umweltüberwachung"/>
+								<xs:enumeration value="Verkehrsnetze"/>
+								<xs:enumeration value="Versorgungswirtschaft und staatliche Dienste"/>
+								<xs:enumeration value="Verteilung der Arten"/>
+								<xs:enumeration value="Verteilung der Bevölkerung — Demografie"/>
+								<xs:enumeration value="Verwaltungseinheiten"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_ger">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:DE:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationInspireInteroperabilityRegulation_ger">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="VERORDNUNG (EG) Nr. 1089/2010 DER KOMMISSION vom 23. November 2010 zur Durchführung der Richtlinie 2007/2/EG des Europäischen Parlaments und des Rates hinsichtlich der Interoperabilität von Geodatensätzen und -diensten"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:DE:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_ger" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_gle.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_gle.xsd
new file mode 100644
index 0000000..7357e29
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_gle.xsd
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- edited with XMLSpy v2011 sp1 (http://www.altova.com) by - - (European Commission DG JRC IES) -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<!-- While official translations become available English values are used-->
+	<xs:complexType name="inspireTheme_gle">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="Addresses"/>
+								<xs:enumeration value="Administrative units"/>
+								<xs:enumeration value="Agricultural and aquaculture facilities"/>
+								<xs:enumeration value="Area management/restriction/regulation zones and reporting units"/>
+								<xs:enumeration value="Atmospheric conditions"/>
+								<xs:enumeration value="Bio-geographical regions"/>
+								<xs:enumeration value="Buildings"/>
+								<xs:enumeration value="Cadastral parcels"/>
+								<xs:enumeration value="Coordinate reference systems"/>
+								<xs:enumeration value="Elevation"/>
+								<xs:enumeration value="Energy resources"/>
+								<xs:enumeration value="Environmental monitoring facilities"/>
+								<xs:enumeration value="Geographical grid systems"/>
+								<xs:enumeration value="Geographical names"/>
+								<xs:enumeration value="Geology"/>
+								<xs:enumeration value="Habitats and biotopes"/>
+								<xs:enumeration value="Human health and safety"/>
+								<xs:enumeration value="Hydrography"/>
+								<xs:enumeration value="Land cover"/>
+								<xs:enumeration value="Land use"/>
+								<xs:enumeration value="Meteorological geographical features"/>
+								<xs:enumeration value="Mineral resources"/>
+								<xs:enumeration value="Natural risk zones"/>
+								<xs:enumeration value="Oceanographic geographical features"/>
+								<xs:enumeration value="Orthoimagery"/>
+								<xs:enumeration value="Population distribution — demography"/>
+								<xs:enumeration value="Production and industrial facilities"/>
+								<xs:enumeration value="Protected sites"/>
+								<xs:enumeration value="Sea regions"/>
+								<xs:enumeration value="Soil"/>
+								<xs:enumeration value="Species distribution"/>
+								<xs:enumeration value="Statistical units"/>
+								<xs:enumeration value="Transport networks"/>
+								<xs:enumeration value="Utility and governmental services"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_gle">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:EN:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationInspireInteroperabilityRegulation_gle">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="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"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:EN:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_gle" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_gre.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_gre.xsd
new file mode 100644
index 0000000..15d5c78
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_gre.xsd
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- edited with XMLSpy v2011 sp1 (http://www.altova.com) by - - (European Commission DG JRC IES) -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<xs:complexType name="inspireTheme_gre">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="Ανθρώπινη υγεία και ασφάλεια"/>
+								<xs:enumeration value="Ατμοσφαιρικές συνθήκες"/>
+								<xs:enumeration value="Βιογεωγραφικές περιοχές"/>
+								<xs:enumeration value="Γεωλογία"/>
+								<xs:enumeration value="Γεωργικές εγκαταστάσεις και εγκαταστάσεις υδατοκαλλιέργειας"/>
+								<xs:enumeration value="Γεωτεμάχια κτηματολογίου"/>
+								<xs:enumeration value="Διευθύνσεις"/>
+								<xs:enumeration value="Δίκτυα μεταφορών"/>
+								<xs:enumeration value="Διοικητικές ενότητες"/>
+								<xs:enumeration value="Εγκαταστάσεις παραγωγής και βιομηχανικές εγκαταστάσεις"/>
+								<xs:enumeration value="Εγκαταστάσεις παρακολούθησης του περιβάλλοντος"/>
+								<xs:enumeration value="Έδαφος"/>
+								<xs:enumeration value="Ενδιαιτήματα και βιότοποι"/>
+								<xs:enumeration value="Ενεργειακοί πόροι"/>
+								<xs:enumeration value="Επιχειρήσεις κοινής ωφελείας και κρατικές υπηρεσίες"/>
+								<xs:enumeration value="Ζώνες διαχείρισης/περιορισμού/ρύθμισης εκτάσεων και μονάδες αναφοράς"/>
+								<xs:enumeration value="Ζώνες φυσικών κινδύνων"/>
+								<xs:enumeration value="Θαλάσσιες περιοχές"/>
+								<xs:enumeration value="Κάλυψη γης"/>
+								<xs:enumeration value="Κατανομή ειδών"/>
+								<xs:enumeration value="Κατανομή πληθυσμού — δημογραφία"/>
+								<xs:enumeration value="Κτίρια"/>
+								<xs:enumeration value="Μετεωρολογικά γεωγραφικά χαρακτηριστικά"/>
+								<xs:enumeration value="Ορθοφωτογραφία"/>
+								<xs:enumeration value="Ορυκτοί πόροι"/>
+								<xs:enumeration value="Προστατευόμενες τοποθεσίες"/>
+								<xs:enumeration value="Στατιστικές μονάδες"/>
+								<xs:enumeration value="Συστήματα γεωγραφικού καννάβου"/>
+								<xs:enumeration value="Συστήματα συντεταγμένων"/>
+								<xs:enumeration value="Τοπωνύμια"/>
+								<xs:enumeration value="Υδρογραφία"/>
+								<xs:enumeration value="Υψομετρία"/>
+								<xs:enumeration value="Χρήσεις γης"/>
+								<xs:enumeration value="Ωκεανογραφικά γεωγραφικά χαρακτηριστικά"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_gre">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:EL:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationInspireInteroperabilityRegulation_gre">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="ΚΑΝΟΝΙΣΜΟΣ (ΕΕ) αριθ. 1089/2010 ΤΗΣ ΕΠΙΤΡΟΠΗΣ της 23ης Νοεμβρίου 2010 σχετικά με την εφαρμογή της οδηγίας 2007/2/ΕΚ του Ευρωπαϊκού Κοινοβουλίου και του Συμβουλίου όσον αφορά τη διαλειτουργικότητα των συνόλων και των υπηρεσιών χωρικών δεδομένων"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:EL:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_gre" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_hun.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_hun.xsd
new file mode 100644
index 0000000..43942f3
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_hun.xsd
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- edited with XMLSpy v2011 sp1 (http://www.altova.com) by - - (European Commission DG JRC IES) -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<xs:complexType name="inspireTheme_hun">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="A fajok megoszlása"/>
+								<xs:enumeration value="A felszín borítása"/>
+								<xs:enumeration value="A népesség eloszlása – demográfia"/>
+								<xs:enumeration value="Ásványi nyersanyagok"/>
+								<xs:enumeration value="Biogeográfiai régiók"/>
+								<xs:enumeration value="Címek"/>
+								<xs:enumeration value="Domborzat"/>
+								<xs:enumeration value="Élőhelyek és biotópok"/>
+								<xs:enumeration value="Emberi egészség és biztonság"/>
+								<xs:enumeration value="Energiaforrások"/>
+								<xs:enumeration value="Épületek"/>
+								<xs:enumeration value="Földhasználat"/>
+								<xs:enumeration value="Földrajzi nevek"/>
+								<xs:enumeration value="Földrajzi rácsrendszerek"/>
+								<xs:enumeration value="Földtan"/>
+								<xs:enumeration value="Kataszteri parcellák"/>
+								<xs:enumeration value="Koordinátarendszerek"/>
+								<xs:enumeration value="Környezetvédelmi monitoringlétesítmények"/>
+								<xs:enumeration value="Közigazgatási egységek"/>
+								<xs:enumeration value="Közlekedési hálózatok"/>
+								<xs:enumeration value="Közüzemi és közszolgáltatások"/>
+								<xs:enumeration value="Légköri viszonyok"/>
+								<xs:enumeration value="Meteorológiai földrajzi jellemzők"/>
+								<xs:enumeration value="Mezőgazdasági és akvakultúra-ágazati létesítmények"/>
+								<xs:enumeration value="Oceanográfiai földrajzi jellemzők"/>
+								<xs:enumeration value="Ortofotók"/>
+								<xs:enumeration value="Statisztikai egységek"/>
+								<xs:enumeration value="Talaj"/>
+								<xs:enumeration value="Tengeri régiók"/>
+								<xs:enumeration value="Termelő és ipari létesítmények"/>
+								<xs:enumeration value="Természeti kockázati zónák"/>
+								<xs:enumeration value="Területgazdálkodási/-korlátozási/-szabályozási övezetek és adatszolgáltató egységek"/>
+								<xs:enumeration value="Védett helyek"/>
+								<xs:enumeration value="Vízrajz"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_hun">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:HU:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationInspireInteroperabilityRegulation_hun">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="A BIZOTTSÁG 1089/2010/EU RENDELETE (2010. november 23.) a 2007/2/EK európai parlamenti és tanácsi irányelv téradatkészletek és -szolgáltatások interoperabilitására vonatkozó rendelkezéseinek végrehajtásáról"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:HU:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_hun" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_ita.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_ita.xsd
new file mode 100644
index 0000000..85502cf
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_ita.xsd
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- edited with XMLSpy v2011 sp1 (http://www.altova.com) by - - (European Commission DG JRC IES) -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<xs:complexType name="inspireTheme_ita">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="Condizioni atmosferiche"/>
+								<xs:enumeration value="Copertura del suolo"/>
+								<xs:enumeration value="Distribuzione della popolazione — demografia"/>
+								<xs:enumeration value="Distribuzione delle specie"/>
+								<xs:enumeration value="Edifici"/>
+								<xs:enumeration value="Elementi geografici meteorologici"/>
+								<xs:enumeration value="Elementi geografici oceanografici"/>
+								<xs:enumeration value="Elevazione"/>
+								<xs:enumeration value="Geologia"/>
+								<xs:enumeration value="Habitat e biotopi"/>
+								<xs:enumeration value="Idrografia"/>
+								<xs:enumeration value="Impianti agricoli e di acquacoltura"/>
+								<xs:enumeration value="Impianti di monitoraggio ambientale"/>
+								<xs:enumeration value="Indirizzi"/>
+								<xs:enumeration value="Nomi geografici"/>
+								<xs:enumeration value="Orto immagini"/>
+								<xs:enumeration value="Parcelle catastali"/>
+								<xs:enumeration value="Produzione e impianti industriali"/>
+								<xs:enumeration value="Regioni biogeografiche"/>
+								<xs:enumeration value="Regioni marine"/>
+								<xs:enumeration value="Reti di trasporto"/>
+								<xs:enumeration value="Risorse energetiche"/>
+								<xs:enumeration value="Risorse minerarie"/>
+								<xs:enumeration value="Salute umana e sicurezza"/>
+								<xs:enumeration value="Servizi di pubblica utilità e servizi amministrativi"/>
+								<xs:enumeration value="Sistemi di coordinate"/>
+								<xs:enumeration value="Sistemi di griglie geografiche"/>
+								<xs:enumeration value="Siti protetti"/>
+								<xs:enumeration value="Suolo"/>
+								<xs:enumeration value="Unità amministrative"/>
+								<xs:enumeration value="Unità statistiche"/>
+								<xs:enumeration value="Utilizzo del territorio"/>
+								<xs:enumeration value="Zone a rischio naturale"/>
+								<xs:enumeration value="Zone sottoposte a gestione/limitazioni/regolamentazione e unità con obbligo di comunicare dati"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_ita">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:IT:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationInspireInteroperabilityRegulation_ita">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="REGOLAMENTO (UE) N. 1089/2010 DELLA COMMISSIONE del 23 novembre 2010 recante attuazione della direttiva 2007/2/CE del Parlamento europeo e del Consiglio per quanto riguarda l'interoperabilità dei set di dati territoriali e dei servizi di dati territoriali"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:IT:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_ita" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_lav.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_lav.xsd
new file mode 100644
index 0000000..d5c415c
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_lav.xsd
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- edited with XMLSpy v2011 sp1 (http://www.altova.com) by - - (European Commission DG JRC IES) -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<xs:complexType name="inspireTheme_lav">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="Administratīvas vienības"/>
+								<xs:enumeration value="Adreses"/>
+								<xs:enumeration value="Aizsargājamas teritorijas"/>
+								<xs:enumeration value="Apgabala pārvaldības/ierobežojumu/reglamentētas zonas un ziņošanas vienības"/>
+								<xs:enumeration value="Atmosfēras apstākļi"/>
+								<xs:enumeration value="Augsne"/>
+								<xs:enumeration value="Augstums"/>
+								<xs:enumeration value="Bioģeogrāfiskie reģioni"/>
+								<xs:enumeration value="Cilvēku veselība un drošība"/>
+								<xs:enumeration value="Dabas apdraudējuma zonas"/>
+								<xs:enumeration value="Derīgo izrakteņu resursi"/>
+								<xs:enumeration value="Dzīvotnes un biotopi"/>
+								<xs:enumeration value="Ēkas"/>
+								<xs:enumeration value="Enerģijas resursi"/>
+								<xs:enumeration value="Ģeogrāfisko koordinātu tīklu sistēmas"/>
+								<xs:enumeration value="Ģeoloģija"/>
+								<xs:enumeration value="Hidrogrāfija"/>
+								<xs:enumeration value="Iedzīvotāju sadalījums – demogrāfija"/>
+								<xs:enumeration value="Jūru reģioni"/>
+								<xs:enumeration value="Kadastrāli zemes gabali"/>
+								<xs:enumeration value="Komunālie un valsts dienesti"/>
+								<xs:enumeration value="Koordinātu atskaites sistēmas"/>
+								<xs:enumeration value="Lauksaimniecības un akvakultūras iekārtas"/>
+								<xs:enumeration value="Meteoroloģiski ģeogrāfiskie raksturlielumi"/>
+								<xs:enumeration value="Okeanogrāfiski ģeogrāfiskie raksturlielumi"/>
+								<xs:enumeration value="Ortofotogrāfija"/>
+								<xs:enumeration value="Ražošanas un rūpniecības iekārtas"/>
+								<xs:enumeration value="Statistikas vienības"/>
+								<xs:enumeration value="Sugu izplatība"/>
+								<xs:enumeration value="Toponīmi"/>
+								<xs:enumeration value="Transporta tīkli"/>
+								<xs:enumeration value="Vides monitoringa iekārtas"/>
+								<xs:enumeration value="Zemes izmantošana"/>
+								<xs:enumeration value="Zemes virsma"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_lav">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:LV:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationInspireInteroperabilityRegulation_lav">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="KOMISIJAS REGULA (ES) Nr. 1089/2010 (2010. gada 23. novembris), ar kuru īsteno Eiropas Parlamenta un Padomes Direktīvu 2007/2/EK attiecībā uz telpisko datu kopu un telpisko datu pakalpojumu savstarpējo izmantojamību"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:LV:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_lav" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_lit.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_lit.xsd
new file mode 100644
index 0000000..7e02f97
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_lit.xsd
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- edited with XMLSpy v2011 sp1 (http://www.altova.com) by - - (European Commission DG JRC IES) -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<xs:complexType name="inspireTheme_lit">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="Administraciniai vienetai"/>
+								<xs:enumeration value="Adresai"/>
+								<xs:enumeration value="Aplinkos stebėsenos priemonės"/>
+								<xs:enumeration value="Atmosferos sąlygos"/>
+								<xs:enumeration value="Aukštis"/>
+								<xs:enumeration value="Biogeografiniai regionai"/>
+								<xs:enumeration value="Buveinės ir biotopai"/>
+								<xs:enumeration value="Dirvožemis"/>
+								<xs:enumeration value="Energijos ištekliai"/>
+								<xs:enumeration value="Gamtinių pavojų zonos"/>
+								<xs:enumeration value="Gamybos ir pramonės įrenginiai"/>
+								<xs:enumeration value="Geografiniai pavadinimai"/>
+								<xs:enumeration value="Geografinio tinklelio sistemos"/>
+								<xs:enumeration value="Geologija"/>
+								<xs:enumeration value="Gyventojų pasiskirstymas – demografija"/>
+								<xs:enumeration value="Hidrografija"/>
+								<xs:enumeration value="Jūrų regionai"/>
+								<xs:enumeration value="Kadastro sklypai"/>
+								<xs:enumeration value="Komunalinės įmonės ir valstybės tarnybos"/>
+								<xs:enumeration value="Koordinačių atskaitos sistemos"/>
+								<xs:enumeration value="Meteorologinės geografinės sąlygos"/>
+								<xs:enumeration value="Naudingosios iškasenos"/>
+								<xs:enumeration value="Okeanografinės geografinės sąlygos"/>
+								<xs:enumeration value="Ortofotografinis vaizdavimas"/>
+								<xs:enumeration value="Pastatai"/>
+								<xs:enumeration value="Rūšių pasiskirstymas"/>
+								<xs:enumeration value="Saugomos teritorijos"/>
+								<xs:enumeration value="Statistiniai vienetai"/>
+								<xs:enumeration value="Transporto tinklai"/>
+								<xs:enumeration value="Tvarkomos teritorijos, ribojamos ir reglamentuojamos zonos bei vienetai, už kuriuos atsiskaitoma"/>
+								<xs:enumeration value="Žemės danga"/>
+								<xs:enumeration value="Žemės naudojimas"/>
+								<xs:enumeration value="Žemės ūkio ir akvakultūros infrastruktūra"/>
+								<xs:enumeration value="Žmonių sveikata ir sauga"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_lit">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:LT:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationInspireInteroperabilityRegulation_lit">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="KOMISIJOS REGLAMENTAS (ES) Nr. 1089/2010 2010 m. lapkričio 23 d. kuriuo įgyvendinamos Europos Parlamento ir Tarybos direktyvos 2007/2/EB nuostatos dėl erdvinių duomenų rinkinių ir paslaugų sąveikumo"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:LT:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_lit" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_mlt.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_mlt.xsd
new file mode 100644
index 0000000..fbfd315
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_mlt.xsd
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- edited with XMLSpy v2011 sp1 (http://www.altova.com) by - - (European Commission DG JRC IES) -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<xs:complexType name="inspireTheme_mlt">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="Addresses"/>
+								<xs:enumeration value="Administrative units"/>
+								<xs:enumeration value="Agricultural and aquaculture facilities"/>
+								<xs:enumeration value="Area management/restriction/regulation zones and reporting units"/>
+								<xs:enumeration value="Atmospheric conditions"/>
+								<xs:enumeration value="Bio-geographical regions"/>
+								<xs:enumeration value="Buildings"/>
+								<xs:enumeration value="Cadastral parcels"/>
+								<xs:enumeration value="Coordinate reference systems"/>
+								<xs:enumeration value="Elevation"/>
+								<xs:enumeration value="Energy resources"/>
+								<xs:enumeration value="Environmental monitoring facilities"/>
+								<xs:enumeration value="Geographical grid systems"/>
+								<xs:enumeration value="Geographical names"/>
+								<xs:enumeration value="Geology"/>
+								<xs:enumeration value="Habitats and biotopes"/>
+								<xs:enumeration value="Human health and safety"/>
+								<xs:enumeration value="Hydrography"/>
+								<xs:enumeration value="Land cover"/>
+								<xs:enumeration value="Land use"/>
+								<xs:enumeration value="Meteorological geographical features"/>
+								<xs:enumeration value="Mineral resources"/>
+								<xs:enumeration value="Natural risk zones"/>
+								<xs:enumeration value="Oceanographic geographical features"/>
+								<xs:enumeration value="Orthoimagery"/>
+								<xs:enumeration value="Population distribution — demography"/>
+								<xs:enumeration value="Production and industrial facilities"/>
+								<xs:enumeration value="Protected sites"/>
+								<xs:enumeration value="Sea regions"/>
+								<xs:enumeration value="Soil"/>
+								<xs:enumeration value="Species distribution"/>
+								<xs:enumeration value="Statistical units"/>
+								<xs:enumeration value="Transport networks"/>
+								<xs:enumeration value="Utility and governmental services"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_mlt">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:MT:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationInspireInteroperabilityRegulation_mlt">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="REGOLAMENT TAL-KUMMISSJONI (UE) Nru 1089/2010 tat-23 ta' Novembru 2010 li jimplimenta d-Direttiva 2007/2/KE tal-Parlament Ewropew u tal-Kunsill fir-rigward tal- interoperabbiltà tas-settijiet ta’ dejta u servizzi ġeografiċi"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:MT:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_mlt" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_pol.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_pol.xsd
new file mode 100644
index 0000000..1dbbc43
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_pol.xsd
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- edited with XMLSpy v2011 sp1 (http://www.altova.com) by - - (European Commission DG JRC IES) -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<xs:complexType name="inspireTheme_pol">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="Adresy"/>
+								<xs:enumeration value="Budynki"/>
+								<xs:enumeration value="Działki katastralne"/>
+								<xs:enumeration value="Geologia"/>
+								<xs:enumeration value="Gleba"/>
+								<xs:enumeration value="Gospodarowanie obszarem/strefy ograniczone/regulacyjne oraz jednostki sprawozdawcze"/>
+								<xs:enumeration value="Hydrografia"/>
+								<xs:enumeration value="Jednostki administracyjne"/>
+								<xs:enumeration value="Jednostki statystyczne"/>
+								<xs:enumeration value="Nazwy geograficzne"/>
+								<xs:enumeration value="Obiekty produkcyjne i przemysłowe"/>
+								<xs:enumeration value="Obiekty rolnicze oraz akwakultury"/>
+								<xs:enumeration value="Obszary chronione"/>
+								<xs:enumeration value="Regiony biogeograficzne"/>
+								<xs:enumeration value="Regiony morskie"/>
+								<xs:enumeration value="Rozmieszczenie gatunków"/>
+								<xs:enumeration value="Rozmieszczenie ludności – demografia"/>
+								<xs:enumeration value="Sieci transportowe"/>
+								<xs:enumeration value="Siedliska i obszary przyrodniczo jednorodne"/>
+								<xs:enumeration value="Sporządzanie ortoobrazów"/>
+								<xs:enumeration value="Strefy zagrożenia naturalnego"/>
+								<xs:enumeration value="Systemy odniesienia za pomocą współrzędnych"/>
+								<xs:enumeration value="Systemy siatek geograficznych"/>
+								<xs:enumeration value="Ukształtowanie terenu"/>
+								<xs:enumeration value="Urządzenia do monitorowania środowiska"/>
+								<xs:enumeration value="Usługi użyteczności publicznej i służby państwowe"/>
+								<xs:enumeration value="Użytkowanie terenu"/>
+								<xs:enumeration value="Warunki atmosferyczne"/>
+								<xs:enumeration value="Warunki meteorologiczno-geograficzne"/>
+								<xs:enumeration value="Warunki oceanograficzno-geograficzne"/>
+								<xs:enumeration value="Zagospodarowanie przestrzenne"/>
+								<xs:enumeration value="Zasoby energetyczne"/>
+								<xs:enumeration value="Zasoby mineralne"/>
+								<xs:enumeration value="Zdrowie i bezpieczeństwo ludzi"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_pol">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:PL:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationInspireInteroperabilityRegulation_pol">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="ROZPORZĄDZENIE KOMISJI (UE) NR 1089/2010 z dnia 23 listopada 2010 r. w sprawie wykonania dyrektywy 2007/2/WE Parlamentu Europejskiego i Rady w zakresie interoperacyjności zbiorów i usług danych przestrzennych"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:PL:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_pol" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_por.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_por.xsd
new file mode 100644
index 0000000..22c1191
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_por.xsd
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- edited with XMLSpy v2011 sp1 (http://www.altova.com) by - - (European Commission DG JRC IES) -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<xs:complexType name="inspireTheme_por">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="Altitude"/>
+								<xs:enumeration value="Características geometeorológicas"/>
+								<xs:enumeration value="Características oceanográficas"/>
+								<xs:enumeration value="Condições atmosféricas"/>
+								<xs:enumeration value="Distribuição da população — demografia"/>
+								<xs:enumeration value="Distribuição das espécies"/>
+								<xs:enumeration value="Edifícios"/>
+								<xs:enumeration value="Endereços"/>
+								<xs:enumeration value="Geologia"/>
+								<xs:enumeration value="Habitats e biótopos"/>
+								<xs:enumeration value="Hidrografia"/>
+								<xs:enumeration value="Instalações agrícolas e aquícolas"/>
+								<xs:enumeration value="Instalações de monitorização do ambiente"/>
+								<xs:enumeration value="Instalações industriais e de produção"/>
+								<xs:enumeration value="Ocupação do solo"/>
+								<xs:enumeration value="Ortoimagens"/>
+								<xs:enumeration value="Parcelas cadastrais"/>
+								<xs:enumeration value="Recursos energéticos"/>
+								<xs:enumeration value="Recursos minerais"/>
+								<xs:enumeration value="Redes de transporte"/>
+								<xs:enumeration value="Regiões biogeográficas"/>
+								<xs:enumeration value="Regiões marinhas"/>
+								<xs:enumeration value="Saúde humana e segurança"/>
+								<xs:enumeration value="Serviços de utilidade pública e do Estado"/>
+								<xs:enumeration value="Sistemas de quadrículas geográficas"/>
+								<xs:enumeration value="Sistemas de referencia"/>
+								<xs:enumeration value="Sítios protegidos"/>
+								<xs:enumeration value="Solo"/>
+								<xs:enumeration value="Toponímia"/>
+								<xs:enumeration value="Unidades administrativas"/>
+								<xs:enumeration value="Unidades estatísticas"/>
+								<xs:enumeration value="Uso do solo"/>
+								<xs:enumeration value="Zonas de gestão/restrição/regulamentação e unidades de referência"/>
+								<xs:enumeration value="Zonas de risco natural"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_por">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:PT:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationInspireInteroperabilityRegulation_por">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="REGULAMENTO (UE) N. o 1089/2010 DA COMISSÃO de 23 de Novembro de 2010 que estabelece as disposições de execução da Directiva 2007/2/CE do Parlamento Europeu e do Conselho relativamente à interoperabilidade dos conjuntos e serviços de dados geográficos"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:PT:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_por" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_rum.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_rum.xsd
new file mode 100644
index 0000000..aa2264a
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_rum.xsd
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- edited with XMLSpy v2011 sp1 (http://www.altova.com) by - - (European Commission DG JRC IES) -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<xs:complexType name="inspireTheme_rum">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="Acoperire terestră"/>
+								<xs:enumeration value="Adrese"/>
+								<xs:enumeration value="Caracteristici geografice meteorologice"/>
+								<xs:enumeration value="Caracteristici geografice oceanografice"/>
+								<xs:enumeration value="Clădiri"/>
+								<xs:enumeration value="Condiţii atmosferice"/>
+								<xs:enumeration value="Denumiri geografice"/>
+								<xs:enumeration value="Elevaţie"/>
+								<xs:enumeration value="Geologie"/>
+								<xs:enumeration value="Habitate şi biotopuri"/>
+								<xs:enumeration value="Hidrografie"/>
+								<xs:enumeration value="Instalaţii agricole şi pentru acvacultură"/>
+								<xs:enumeration value="Instalaţii de producţie şi industriale"/>
+								<xs:enumeration value="Instalaţii de supraveghere a mediului"/>
+								<xs:enumeration value="Ortoimagini"/>
+								<xs:enumeration value="Parcele cadastrale"/>
+								<xs:enumeration value="Regiuni biogeografice"/>
+								<xs:enumeration value="Regiuni maritime"/>
+								<xs:enumeration value="Repartizarea populaţiei – demografie"/>
+								<xs:enumeration value="Repartizarea speciilor"/>
+								<xs:enumeration value="Resurse energetice"/>
+								<xs:enumeration value="Resurse minerale"/>
+								<xs:enumeration value="Reţele de transport"/>
+								<xs:enumeration value="Sănătate şi siguranţă umană"/>
+								<xs:enumeration value="Servicii de utilitate publică şi servicii publice"/>
+								<xs:enumeration value="Sisteme de caroiaj geografic"/>
+								<xs:enumeration value="Sisteme de coordonate de referinţă"/>
+								<xs:enumeration value="Soluri"/>
+								<xs:enumeration value="Unităţi administrative"/>
+								<xs:enumeration value="Unităţi statistice"/>
+								<xs:enumeration value="Utilizarea terenului"/>
+								<xs:enumeration value="Zone de administrare/restricţie/reglementare şi unităţi de raportare"/>
+								<xs:enumeration value="Zone de risc natural"/>
+								<xs:enumeration value="Zone protejate"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_rum">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:RO:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationInspireInteroperabilityRegulation_rum">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="REGULAMENTUL (UE) NR. 1089/2010 AL COMISIEI din 23 noiembrie 2010 de punere în aplicare a Directivei 2007/2/CE a Parlamentului European şi a Consiliului în ceea ce priveşte interoperabilitatea seturilor şi serviciilor de date spaţiale"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:RO:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_rum" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_slo.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_slo.xsd
new file mode 100644
index 0000000..aee2d89
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_slo.xsd
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- edited with XMLSpy v2011 sp1 (http://www.altova.com) by - - (European Commission DG JRC IES) -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<xs:complexType name="inspireTheme_slo">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="Adresy"/>
+								<xs:enumeration value="Atmosférické podmienky"/>
+								<xs:enumeration value="Biogeografické regióny"/>
+								<xs:enumeration value="Chránené územia"/>
+								<xs:enumeration value="Dopravné siete"/>
+								<xs:enumeration value="Geografické systémy sietí"/>
+								<xs:enumeration value="Geológia"/>
+								<xs:enumeration value="Habitaty a biotopy"/>
+								<xs:enumeration value="Hydrografia"/>
+								<xs:enumeration value="Katastrálne parcely"/>
+								<xs:enumeration value="Krajinná pokrývka (land cover)"/>
+								<xs:enumeration value="Ľudské zdravie a bezpečnosť"/>
+								<xs:enumeration value="Meteorologické geografické prvky"/>
+								<xs:enumeration value="Morské regióny"/>
+								<xs:enumeration value="Oceánografické geografické prvky"/>
+								<xs:enumeration value="Ortometria"/>
+								<xs:enumeration value="Pôda"/>
+								<xs:enumeration value="Poľnohospodárske zariadenia a zariadenia akvakultúry"/>
+								<xs:enumeration value="Rozmiestnenie obyvateľstva – demografia"/>
+								<xs:enumeration value="Správne jednotky"/>
+								<xs:enumeration value="Spravované/obmedzené/regulované zóny a jednotky podávajúce správy"/>
+								<xs:enumeration value="Štatistické jednotky"/>
+								<xs:enumeration value="Stavby"/>
+								<xs:enumeration value="Súradnicové referenčné systémy"/>
+								<xs:enumeration value="Verejné a štátne služby"/>
+								<xs:enumeration value="Výrobné a priemyselné zariadenia"/>
+								<xs:enumeration value="Výška"/>
+								<xs:enumeration value="Výskyt druhov"/>
+								<xs:enumeration value="Využitie územia"/>
+								<xs:enumeration value="Zariadenia na monitorovanie životného prostredia"/>
+								<xs:enumeration value="Zdroje energie"/>
+								<xs:enumeration value="Zdroje nerastných surovín"/>
+								<xs:enumeration value="Zemepisné názvy"/>
+								<xs:enumeration value="Zóny prírodného rizika"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_slo">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:SK:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationInspireInteroperabilityRegulation_slo">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="NARIADENIE KOMISIE (EÚ) č. 1089/2010 z 23. novembra 2010, ktorým sa vykonáva smernica Európskeho parlamentu a Rady 2007/2/ES, pokiaľ ide o interoperabilitu súborov a služieb priestorových údajov"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:SK:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_slo" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_slv.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_slv.xsd
new file mode 100644
index 0000000..3d3f414
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_slv.xsd
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- edited with XMLSpy v2011 sp1 (http://www.altova.com) by - - (European Commission DG JRC IES) -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<xs:complexType name="inspireTheme_slv">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="Biogeografske regije"/>
+								<xs:enumeration value="Digitalni model višin"/>
+								<xs:enumeration value="Energetski viri"/>
+								<xs:enumeration value="Geografska koordinatna mreža"/>
+								<xs:enumeration value="Geologija"/>
+								<xs:enumeration value="Habitati in biotopi"/>
+								<xs:enumeration value="Hidrografija"/>
+								<xs:enumeration value="Katastrske parcele"/>
+								<xs:enumeration value="Komunalne in javne storitve"/>
+								<xs:enumeration value="Meteorološke značilnosti"/>
+								<xs:enumeration value="Mineralni viri"/>
+								<xs:enumeration value="Morske regije"/>
+								<xs:enumeration value="Namenska raba tal"/>
+								<xs:enumeration value="Naprave in objekti za monitoring okolja"/>
+								<xs:enumeration value="Naslovi"/>
+								<xs:enumeration value="Objekti in naprave za kmetijstvo in ribogojstvo"/>
+								<xs:enumeration value="Območja nevarnosti naravnih nesreč"/>
+								<xs:enumeration value="Območja upravljanja/zaprta območja/regulirana območja in poročevalske enote"/>
+								<xs:enumeration value="Oceanogeografske značilnosti"/>
+								<xs:enumeration value="Ortofoto"/>
+								<xs:enumeration value="Ozračje"/>
+								<xs:enumeration value="Pokrovnost tal"/>
+								<xs:enumeration value="Porazdelitev prebivalstva – demografski podatki"/>
+								<xs:enumeration value="Porazdelitev vrst"/>
+								<xs:enumeration value="Proizvodni in industrijski objekti in naprave"/>
+								<xs:enumeration value="Prometna omrežja"/>
+								<xs:enumeration value="Referenčni koordinatni sistemi"/>
+								<xs:enumeration value="Statistični okoliši"/>
+								<xs:enumeration value="Stavbe"/>
+								<xs:enumeration value="Tla"/>
+								<xs:enumeration value="Upravne enote"/>
+								<xs:enumeration value="Zavarovana območja"/>
+								<xs:enumeration value="Zdravje in varnost prebivalstva"/>
+								<xs:enumeration value="Zemljepisna imena"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_slv">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:SL:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationInspireInteroperabilityRegulation_slv">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="UREDBA KOMISIJE (EU) št. 1089/2010 z dne 23. novembra 2010 o izvajanju Direktive 2007/2/ES Evropskega parlamenta in Sveta glede medopravilnosti zbirk prostorskih podatkov in storitev v zvezi s prostorskimi podatki"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:SL:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_slv" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_spa.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_spa.xsd
new file mode 100644
index 0000000..e1ef516
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_spa.xsd
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- edited with XMLSpy v2011 sp1 (http://www.altova.com) by - - (European Commission DG JRC IES) -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<xs:complexType name="inspireTheme_spa">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="Aspectos geográficos de carácter meteorológico"/>
+								<xs:enumeration value="Condiciones atmosféricas"/>
+								<xs:enumeration value="Cubierta terrestre"/>
+								<xs:enumeration value="Direcciones"/>
+								<xs:enumeration value="Distribución de la población — demografía"/>
+								<xs:enumeration value="Distribución de las especies"/>
+								<xs:enumeration value="Edificios"/>
+								<xs:enumeration value="Elevaciones"/>
+								<xs:enumeration value="Geología"/>
+								<xs:enumeration value="Hábitats y biotopos"/>
+								<xs:enumeration value="Hidrografía"/>
+								<xs:enumeration value="Instalaciones agrícolas y de acuicultura"/>
+								<xs:enumeration value="Instalaciones de observación del medio ambiente"/>
+								<xs:enumeration value="Instalaciones de producción e industriales"/>
+								<xs:enumeration value="Lugares protegidos"/>
+								<xs:enumeration value="Nombres geográficos"/>
+								<xs:enumeration value="Ortoimágenes"/>
+								<xs:enumeration value="Parcelas catastrales"/>
+								<xs:enumeration value="Rasgos geográficos oceanográficos"/>
+								<xs:enumeration value="Recursos energéticos"/>
+								<xs:enumeration value="Recursos minerales"/>
+								<xs:enumeration value="Redes de transporte"/>
+								<xs:enumeration value="Regiones biogeográficas"/>
+								<xs:enumeration value="Regiones marinas"/>
+								<xs:enumeration value="Salud y seguridad humanas"/>
+								<xs:enumeration value="Servicios de utilidad pública y estatales"/>
+								<xs:enumeration value="Sistema de cuadrículas geográficas"/>
+								<xs:enumeration value="Sistemas de coordenadas de referencia"/>
+								<xs:enumeration value="Suelo"/>
+								<xs:enumeration value="Unidades administrativas"/>
+								<xs:enumeration value="Unidades estadísticas"/>
+								<xs:enumeration value="Uso del suelo"/>
+								<xs:enumeration value="Zonas de riesgos naturales"/>
+								<xs:enumeration value="Zonas sujetas a ordenación, a restricciones o reglamentaciones y unidades de notificación"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="citationInspireInteroperabilityRegulation_spa">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="REGLAMENTO (UE) N o 1089/2010 DE LA COMISIÓN de 23 de noviembre de 2010 por el que se aplica la Directiva 2007/2/CE del Parlamento Europeo y del Consejo en lo que se refiere a la interoperabilidad de los conjuntos y los servicios de datos espaciales"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:ES:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_spa" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_spa">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:EN:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/enums/enum_swe.xsd b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_swe.xsd
new file mode 100644
index 0000000..939d0a8
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/enums/enum_swe.xsd
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- edited with XMLSpy v2011 sp1 (http://www.altova.com) by - - (European Commission DG JRC IES) -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<xs:complexType name="inspireTheme_swe">
+		<xs:complexContent>
+			<xs:restriction base="inspireTheme">
+				<xs:sequence>
+					<xs:element name="OriginatingControlledVocabulary" type="originatingControlledVocabularyGemetInspireThemes"/>
+					<xs:element name="KeywordValue">
+						<xs:simpleType>
+							<xs:restriction base="keywordValue">
+								<xs:enumeration value="Administrativa enheter"/>
+								<xs:enumeration value="Adresser"/>
+								<xs:enumeration value="Allmännyttiga och offentliga tjänster"/>
+								<xs:enumeration value="Anläggningar för miljöövervakning"/>
+								<xs:enumeration value="Arters utbredning"/>
+								<xs:enumeration value="Atmosfäriska förhållanden"/>
+								<xs:enumeration value="Befolkningsfördelning – demografi"/>
+								<xs:enumeration value="Biogeografiska regioner"/>
+								<xs:enumeration value="Byggnader"/>
+								<xs:enumeration value="Energiresurser"/>
+								<xs:enumeration value="Fastighetsområden"/>
+								<xs:enumeration value="Geografiska meteorologiska förhållanden"/>
+								<xs:enumeration value="Geografiska namn"/>
+								<xs:enumeration value="Geografiska oceanografiska förhållanden"/>
+								<xs:enumeration value="Geografiska rutnätssystem"/>
+								<xs:enumeration value="Geologi"/>
+								<xs:enumeration value="Havsområden"/>
+								<xs:enumeration value="Hydrografi"/>
+								<xs:enumeration value="Höjd"/>
+								<xs:enumeration value="Jordbruks- och vattenbruksanläggningar"/>
+								<xs:enumeration value="Landtäcke"/>
+								<xs:enumeration value="Mark"/>
+								<xs:enumeration value="Markanvändning"/>
+								<xs:enumeration value="Mineralfyndigheter"/>
+								<xs:enumeration value="Människors hälsa och säkerhet"/>
+								<xs:enumeration value="Naturliga riskområden"/>
+								<xs:enumeration value="Naturtyper och biotoper"/>
+								<xs:enumeration value="Områden med särskild förvaltning/begränsningar/reglering samt enheter för rapportering"/>
+								<xs:enumeration value="Ortofoto"/>
+								<xs:enumeration value="Produktions- och industrianläggningar"/>
+								<xs:enumeration value="Referenskoordinatsystem"/>
+								<xs:enumeration value="Skyddade områden"/>
+								<xs:enumeration value="Statistiska enheter"/>
+								<xs:enumeration value="Transportnät"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Interoperability-->
+	<xs:complexType name="resLocInspireInteroperabilityRegulation_swe">
+		<xs:complexContent>
+			<xs:restriction base="resourceLocatorType">
+				<xs:sequence>
+					<xs:element name="URL">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:SV:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="MediaType">
+						<xs:simpleType>
+							<xs:restriction base="mediaType">
+								<xs:enumeration value="application/pdf"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="citationInspireInteroperabilityRegulation_swe">
+		<xs:complexContent>
+			<xs:restriction base="citationConformity">
+				<xs:sequence>
+					<xs:element name="Title">
+						<xs:simpleType>
+							<xs:restriction base="notEmptyString">
+								<xs:enumeration value="KOMMISSIONENS FÖRORDNING (EU) nr 1089/2010 av den 23 november 2010 om genomförande av Europaparlamentets och rådets direktiv 2007/2/EG vad gäller interoperabilitet för rumsliga datamängder och datatjänster"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:choice>
+						<xs:element name="DateOfPublication">
+							<xs:simpleType>
+								<xs:restriction base="iso8601Date">
+									<xs:enumeration value="2010-12-08"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:element>
+					</xs:choice>
+					<xs:element name="URI" minOccurs="1" maxOccurs="1">
+						<xs:simpleType>
+							<xs:restriction base="xs:anyURI">
+								<xs:enumeration value="OJ:L:2010:323:0011:0102:SV:PDF"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:element>
+					<xs:element name="ResourceLocator" type="resLocInspireInteroperabilityRegulation_swe" minOccurs="1" maxOccurs="1"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/examples/inspireresourcemddataset.xml b/mapproxy/test/schemas/inspire/common/1.0/examples/inspireresourcemddataset.xml
new file mode 100644
index 0000000..85ebd77
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/examples/inspireresourcemddataset.xml
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+26-APR-2011 1.0.1 Conformity element:
+                    Restricted allowed citations to "INSPIRE interoperability of spatial data sets and services", according to the MD regulation,
+                    requirements for the conformity element (page 17 of the MD regulation).
+                  Inspire Themes:
+                    Removed leading blank for German, Bulgarian, Czech and Danish translations
+-->
+<SpatialDataSet xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://inspire.ec.europa.eu/schemas/common/1.0 http://inspire.ec.europa.eu/schemas/common/1.0/common.xsd">
+	<ResourceTitle>resourcetitle2</ResourceTitle>
+	<ResourceAbstract>resourceabstract</ResourceAbstract>
+	<ResourceType>dataset</ResourceType>
+	<ResourceLocator>
+		<URL>http://resourcelocator1.com</URL>
+		<MediaType>text/html</MediaType>
+	</ResourceLocator>
+	<ResourceLocator>
+		<URL>http://resourcelocator2.com</URL>
+		<MediaType>text/html</MediaType>
+	</ResourceLocator>
+	<MandatoryKeyword xsi:type="inspireTheme_eng">
+		<OriginatingControlledVocabulary>
+			<Title>GEMET - INSPIRE themes</Title>
+			<DateOfPublication>2008-06-01</DateOfPublication>
+		</OriginatingControlledVocabulary>
+		<KeywordValue>Addresses</KeywordValue>
+	</MandatoryKeyword>
+	<Keyword>
+		<KeywordValue>satellite imagery</KeywordValue>
+	</Keyword>
+	<GeographicBoundingBox>
+		<East>1.11</East>
+		<West>-2.22</West>
+		<North>3.33</North>
+		<South>-4.44</South>
+	</GeographicBoundingBox>
+	<GeographicBoundingBox>
+		<East>1.11</East>
+		<West>-2.22</West>
+		<North>3.33</North>
+		<South>-4.44</South>
+	</GeographicBoundingBox>
+	<TemporalReference>
+		<TemporalExtent>
+			<IntervalOfDates>
+				<StartingDate>2010-11-11T00:00:00</StartingDate>
+				<EndDate>2010-11-19T00:00:00</EndDate>
+			</IntervalOfDates>
+		</TemporalExtent>
+	</TemporalReference>
+	<TemporalReference>
+		<TemporalExtent>
+			<IndividualDate>2010-11-19T00:00:00</IndividualDate>
+		</TemporalExtent>
+	</TemporalReference>
+	<TemporalReference>
+		<TemporalExtent>
+			<IndividualDate>2010-11-19T10:20:30</IndividualDate>
+		</TemporalExtent>
+	</TemporalReference>
+	<TemporalReference>
+		<TemporalExtent>
+			<IntervalOfDates>
+				<StartingDate>2011-11-11T00:00:00</StartingDate>
+				<EndDate>2011-11-19T00:00:00</EndDate>
+			</IntervalOfDates>
+		</TemporalExtent>
+	</TemporalReference>
+	<TemporalReference>
+		<DateOfCreation>2000-11-22</DateOfCreation>
+	</TemporalReference>
+	<TemporalReference>
+		<DateOfLastRevision>2010-11-19T10:20:30</DateOfLastRevision>
+	</TemporalReference>
+	<TemporalReference>
+		<DateOfPublication>2020-11-19T00:00:00</DateOfPublication>
+	</TemporalReference>
+	<SpatialResolution>
+		<EquivalentScale>10</EquivalentScale>
+	</SpatialResolution>
+	<SpatialResolution abstract="testabstractres2">
+		<EquivalentScale>2</EquivalentScale>
+	</SpatialResolution>
+	<SpatialResolution abstract="testabstractres3">
+		<ResolutionDistance>333</ResolutionDistance>
+		<UnitOfMeasure>myuom</UnitOfMeasure>
+	</SpatialResolution>
+	<Conformity>
+				<Specification xsi:type="citationInspireInteroperabilityRegulation_eng">
+					<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</Title>
+					<DateOfPublication>2010-12-08</DateOfPublication>
+					<URI>OJ:L:2010:323:0011:0102:EN:PDF</URI>
+					<ResourceLocator>
+						<URL>http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:EN:PDF</URL>
+						<MediaType>application/pdf</MediaType>
+					</ResourceLocator>
+				</Specification>
+		<Degree>notEvaluated</Degree>
+	</Conformity>
+	<ConditionsForAccessAndUse>no conditions apply</ConditionsForAccessAndUse>
+	<LimitationsOnPublicAccess>copyright</LimitationsOnPublicAccess>
+	<ResponsibleOrganisation>
+		<ResponsibleParty>
+			<OrganisationName>resppartyorgname1</OrganisationName>
+			<EmailAddress>jrc1 at jrc.it</EmailAddress>
+		</ResponsibleParty>
+		<ResponsiblePartyRole>pointOfContact</ResponsiblePartyRole>
+	</ResponsibleOrganisation>
+	<MetadataPointOfContact>
+		<OrganisationName>MDPointOfContact1</OrganisationName>
+		<EmailAddress>MDPointOfContac12 at orgemail1.com</EmailAddress>
+	</MetadataPointOfContact>
+	<MetadataDate>2010-11-10</MetadataDate>
+	<MetadataLanguage>eng</MetadataLanguage>
+	<UniqueResourceIdentifier metadataURL="ciao">
+		<Code>code1</Code>
+		<Namespace>namespace1</Namespace>
+	</UniqueResourceIdentifier>
+	<UniqueResourceIdentifier>
+		<Code>code2</Code>
+		<Namespace>namespace2</Namespace>
+	</UniqueResourceIdentifier>
+	<ResourceLanguage>por</ResourceLanguage>
+	<ResourceLanguage>ita</ResourceLanguage>
+	<TopicCategory>biota</TopicCategory>
+	<TopicCategory>inlandWaters</TopicCategory>
+	<Lineage>lineage statement</Lineage>
+</SpatialDataSet>
\ No newline at end of file
diff --git a/mapproxy/test/schemas/inspire/common/1.0/examples/inspireresourcemdseries.xml b/mapproxy/test/schemas/inspire/common/1.0/examples/inspireresourcemdseries.xml
new file mode 100644
index 0000000..a9b9499
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/examples/inspireresourcemdseries.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+26-APR-2011 1.0.1 Conformity element:
+                    Restricted allowed citations to "INSPIRE interoperability of spatial data sets and services", according to the MD regulation,
+                    requirements for the conformity element (page 17 of the MD regulation).
+                  Inspire Themes:
+                    Removed leading blank for German, Bulgarian, Czech and Danish translations
+-->
+<SpatialDataSetSeries xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://inspire.ec.europa.eu/schemas/common/1.0 http://inspire.ec.europa.eu/schemas/common/1.0/common.xsd">
+	<ResourceTitle>EDGAR V4.1 Activity Data</ResourceTitle>
+	<ResourceAbstract>The Emissions Database for Global Atmospheric Research (EDGAR) provides global past and present day Human Activity Data classified following the EDGAR Source Categories classification</ResourceAbstract>
+	<ResourceType>series</ResourceType>
+	<ResourceLocator>
+		<URL>http://edgar.jrc.ec.europa.eu/index.php</URL>
+		<MediaType>text/html</MediaType>
+	</ResourceLocator>
+	<MandatoryKeyword xsi:type="inspireTheme_eng">
+		<OriginatingControlledVocabulary>
+			<Title>GEMET - INSPIRE themes</Title>
+			<DateOfPublication>2008-06-01</DateOfPublication>
+		</OriginatingControlledVocabulary>
+		<KeywordValue>Energy resources</KeywordValue>
+	</MandatoryKeyword>
+	<Keyword>
+		<KeywordValue>satellite imagery</KeywordValue>
+	</Keyword>
+	<GeographicBoundingBox>
+		<East>180.00</East>
+		<West>-180.00</West>
+		<North>80.00</North>
+		<South>-80.00</South>
+	</GeographicBoundingBox>
+	<TemporalReference>
+		<TemporalExtent>
+			<IntervalOfDates>
+				<StartingDate>1970-01-01T00:00:00</StartingDate>
+				<EndDate>2005-12-31T00:00:00</EndDate>
+			</IntervalOfDates>
+		</TemporalExtent>
+	</TemporalReference>
+	<TemporalReference>
+		<DateOfCreation>2010-05-01</DateOfCreation>
+	</TemporalReference>
+	<TemporalReference>
+		<DateOfPublication>2010-05-01</DateOfPublication>
+	</TemporalReference>
+	<TemporalReference>
+		<DateOfLastRevision>2010-05-01</DateOfLastRevision>
+	</TemporalReference>
+	<SpatialResolution>
+		<EquivalentScale>1</EquivalentScale>
+	</SpatialResolution>
+	<SpatialResolution>
+		<ResolutionDistance>222</ResolutionDistance>
+		<UnitOfMeasure>myuom</UnitOfMeasure>
+	</SpatialResolution>
+	<Conformity>
+				<Specification xsi:type="citationInspireInteroperabilityRegulation_eng">
+					<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</Title>
+					<DateOfPublication>2010-12-08</DateOfPublication>
+					<URI>OJ:L:2010:323:0011:0102:EN:PDF</URI>
+					<ResourceLocator>
+						<URL>http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:EN:PDF</URL>
+						<MediaType>application/pdf</MediaType>
+					</ResourceLocator>
+				</Specification>
+		<Degree>notEvaluated</Degree>
+	</Conformity>
+	<Conformity>
+		<Specification>
+			<Title>spec2</Title>
+			<DateOfCreation>2010-11-09</DateOfCreation>
+		</Specification>
+		<Degree>notConformant</Degree>
+	</Conformity>
+	<Conformity>
+		<Specification>
+			<Title>spec2</Title>
+			<DateOfCreation>2010-11-09</DateOfCreation>
+		</Specification>
+		<Degree>conformant</Degree>
+	</Conformity>
+	<ConditionsForAccessAndUse>no conditions apply</ConditionsForAccessAndUse>
+	<LimitationsOnPublicAccess>free access provided a credentials required 2</LimitationsOnPublicAccess>
+	<ResponsibleOrganisation>
+		<ResponsibleParty>
+			<OrganisationName>EC/JRC/IES</OrganisationName>
+			<EmailAddress>greet.maenhout at jrc.ec.europa.eu</EmailAddress>
+		</ResponsibleParty>
+		<ResponsiblePartyRole>principalInvestigator</ResponsiblePartyRole>
+	</ResponsibleOrganisation>
+	<ResponsibleOrganisation>
+		<ResponsibleParty>
+			<OrganisationName>EC/JRC/IES</OrganisationName>
+			<EmailAddress>greet.maenhout at jrc.ec.europa.eu</EmailAddress>
+		</ResponsibleParty>
+		<ResponsiblePartyRole>pointOfContact</ResponsiblePartyRole>
+	</ResponsibleOrganisation>
+	<MetadataPointOfContact>
+		<OrganisationName>EC/JRC/IES</OrganisationName>
+		<EmailAddress>valerio.pagliari at jrc.it</EmailAddress>
+	</MetadataPointOfContact>
+	<MetadataDate>2010-10-11</MetadataDate>
+	<MetadataLanguage>eng</MetadataLanguage>
+	<UniqueResourceIdentifier>
+		<Code>edgar.jrc.ec.europa.eu</Code>
+		<Namespace>http://edgar.jrc.ec.europa.eu</Namespace>
+	</UniqueResourceIdentifier>
+	<ResourceLanguage>eng</ResourceLanguage>
+	<TopicCategory>environment</TopicCategory>
+	<Lineage>IEA for Energy statistics
+FAO for agricoltural data
+</Lineage>
+</SpatialDataSetSeries>
\ No newline at end of file
diff --git a/mapproxy/test/schemas/inspire/common/1.0/examples/inspireresourcemdservice.xml b/mapproxy/test/schemas/inspire/common/1.0/examples/inspireresourcemdservice.xml
new file mode 100644
index 0000000..7cbeb47
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/examples/inspireresourcemdservice.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+26-APR-2011 1.0.1 Conformity element:
+                    Restricted allowed citations to "INSPIRE interoperability of spatial data sets and services", according to the MD regulation,
+                    requirements for the conformity element (page 17 of the MD regulation).
+                  Inspire Themes:
+                    Removed leading blank for German, Bulgarian, Czech and Danish translations
+-->
+<SpatialDataService xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://inspire.ec.europa.eu/schemas/common/1.0 http://inspire.ec.europa.eu/schemas/common/1.0/common.xsd">
+<!--31-JAN-2011 INSPIRE schema updated to 0.19 (Removed language dependent schema implementations)-->
+	<ResourceTitle>Europe Mapping Service based on the Image2000 European mosaic (multispectral)</ResourceTitle>
+	<ResourceAbstract>Map Service of a European Mosaic. The mosaic is a harmonised dataset and is based on the individual orthorectified scenes from the Image 2000 project. This service covers the Landsat 7 multispectral bands 3, 2 and 1.</ResourceAbstract>
+	<ResourceType>service</ResourceType>
+	<ResourceLocator>
+		<URL>http://edgar.jrc.ec.europa.eu/index.php</URL>
+		<MediaType>text/html</MediaType>
+	</ResourceLocator>
+	<MandatoryKeyword>
+		<KeywordValue>humanGeographicViewer</KeywordValue>
+	</MandatoryKeyword>
+	<Keyword>
+		<KeywordValue>satellite imagery</KeywordValue>
+	</Keyword>
+	<TemporalReference>
+		<TemporalExtent>
+			<IntervalOfDates>
+				<StartingDate>2010-11-11T00:00:00</StartingDate>
+				<EndDate>2010-11-19T00:00:00</EndDate>
+			</IntervalOfDates>
+		</TemporalExtent>
+	</TemporalReference>
+	<TemporalReference>
+		<TemporalExtent>
+			<IndividualDate>2010-11-19T00:00:00</IndividualDate>
+		</TemporalExtent>
+	</TemporalReference>
+	<TemporalReference>
+		<TemporalExtent>
+			<IntervalOfDates>
+				<StartingDate>2011-11-11T00:00:00</StartingDate>
+				<EndDate>2011-11-19T00:00:00</EndDate>
+			</IntervalOfDates>
+		</TemporalExtent>
+	</TemporalReference>
+	<TemporalReference>
+		<DateOfCreation>2005-12-12</DateOfCreation>
+	</TemporalReference>
+	<SpatialResolution abstract="Map Service of a European Mosaic. The mosaic is a harmonised dataset and is based on the individual orthorectified scenes from the Image 2000 project. This service covers the Landsat 7 multispectral bands 3, 2 and 1.">
+		<EquivalentScale>1</EquivalentScale>
+	</SpatialResolution>
+	<Conformity>
+				<Specification xsi:type="citationInspireInteroperabilityRegulation_eng">
+					<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</Title>
+					<DateOfPublication>2010-12-08</DateOfPublication>
+					<URI>OJ:L:2010:323:0011:0102:EN:PDF</URI>
+					<ResourceLocator>
+						<URL>http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:EN:PDF</URL>
+						<MediaType>application/pdf</MediaType>
+					</ResourceLocator>
+				</Specification>
+		<Degree>notConformant</Degree>
+	</Conformity>
+	<ConditionsForAccessAndUse>no conditions apply</ConditionsForAccessAndUse>
+	<ConditionsForAccessAndUse>no conditions apply</ConditionsForAccessAndUse>
+	<LimitationsOnPublicAccess>no limitations</LimitationsOnPublicAccess>
+	<ResponsibleOrganisation>
+		<ResponsibleParty>
+			<OrganisationName>(R Service)Spatial Data Infrastructure Unit, Institute for Environment & Sustainability, Joint Research Centre, European Commission</OrganisationName>
+			<EmailAddress>(r)image2000 at jrc.ec.europa.eu</EmailAddress>
+		</ResponsibleParty>
+		<ResponsiblePartyRole>resourceProvider</ResponsiblePartyRole>
+	</ResponsibleOrganisation>
+	<ResponsibleOrganisation>
+		<ResponsibleParty>
+			<OrganisationName>(R Service)Spatial Data Infrastructure Unit, Institute for Environment & Sustainability, Joint Research Centre, European Commission</OrganisationName>
+			<EmailAddress>(r)image2000 at jrc.ec.europa.eu</EmailAddress>
+		</ResponsibleParty>
+		<ResponsiblePartyRole>publisher</ResponsiblePartyRole>
+	</ResponsibleOrganisation>
+	<MetadataPointOfContact>
+		<OrganisationName>MC Spatial Data Infrastructure Unit, Institute for Environment & Sustainability, Joint Research Centre, European Commission</OrganisationName>
+		<EmailAddress>(MC)image2000 at jrc.ec.europa.eu</EmailAddress>
+	</MetadataPointOfContact>
+	<MetadataDate>2010-06-15</MetadataDate>
+	<MetadataLanguage>eng</MetadataLanguage>
+	<CoupledResource>
+		<Code>sfdsdf</Code>
+		<Namespace>adsasd</Namespace>
+	</CoupledResource>
+	<SpatialDataServiceType>view</SpatialDataServiceType>
+</SpatialDataService>
diff --git a/mapproxy/test/schemas/inspire/common/1.0/network.xsd b/mapproxy/test/schemas/inspire/common/1.0/network.xsd
new file mode 100644
index 0000000..4679fd9
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/common/1.0/network.xsd
@@ -0,0 +1,521 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+26-APR-2011 1.0.1 Conformity element:
+                    Restricted allowed citations to "INSPIRE interoperability of spatial data sets and services", according to the MD regulation,
+                    requirements for the conformity element (page 17 of the MD regulation).
+                  Inspire Themes:
+                    Removed leading blank for German, Bulgarian, Czech and Danish translations
+18-FEB-2011 Added types for encoding for Language Elements in Network Services
+18-DEC-2010 ResourceLocator made mandatory since we are dealing with Network Services
+-->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://inspire.ec.europa.eu/schemas/common/1.0" xmlns:jaxb="http://java.sun.com/xml/ns/jaxb" targetNamespace="http://inspire.ec.europa.eu/schemas/common/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1" jaxb:version="2.0">
+	<xs:import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="http://www.w3.org/2001/xml.xsd"/>
+	<xs:complexType name="ExtendedCapabilitiesType">
+		<xs:annotation>
+			<xs:documentation>Extended capabilities for ISO 19128 , OGC CSW, OGC OWS services</xs:documentation>
+		</xs:annotation>
+		<xs:choice>
+			<xs:sequence>
+				<xs:annotation>
+					<xs:documentation>Scenario 1: Mandatory MetadataUrl element pointing to an INSPIRE Compliant ISO metadata document plus language parameters </xs:documentation>
+				</xs:annotation>
+				<xs:element name="MetadataUrl" type="resourceLocatorType"/>
+				<xs:element name="SupportedLanguages" type="supportedLanguagesType"/>
+				<xs:element name="ResponseLanguage" type="languageElementISO6392B"/>
+			</xs:sequence>
+			<xs:sequence>
+				<xs:annotation>
+					<xs:documentation>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</xs:documentation>
+				</xs:annotation>
+				<xs:element name="ResourceLocator" type="resourceLocatorType" maxOccurs="unbounded">
+					<xs:annotation>
+						<xs:documentation xml:lang="en">Mandatory linkage to the network service</xs:documentation>
+					</xs:annotation>
+				</xs:element>
+				<xs:element name="ResourceType" type="serviceSpatialDataResourceType"/>
+				<xs:element name="TemporalReference" type="temporalReference" maxOccurs="unbounded"/>
+				<xs:element name="Conformity" type="conformity" maxOccurs="unbounded"/>
+				<xs:element name="MetadataPointOfContact" type="metadataPointOfContact" maxOccurs="unbounded"/>
+				<xs:element name="MetadataDate" type="iso8601Date"/>
+				<xs:element name="SpatialDataServiceType" type="spatialDataServiceType"/>
+				<xs:element name="MandatoryKeyword" type="classificationOfSpatialDataService" maxOccurs="unbounded"/>
+				<xs:element name="Keyword" type="keyword" minOccurs="0" maxOccurs="unbounded">
+					<xs:annotation>
+						<xs:documentation xml:lang="en">If the resource is a spatial data service, at least one keyword from Part D.4 shall be provided.</xs:documentation>
+					</xs:annotation>
+				</xs:element>
+				<xs:element name="SupportedLanguages" type="supportedLanguagesType"/>
+				<xs:element name="ResponseLanguage" type="languageElementISO6392B"/>
+				<xs:element name="MetadataUrl" type="resourceLocatorType" minOccurs="0"/>
+			</xs:sequence>
+		</xs:choice>
+	</xs:complexType>
+	<xs:complexType name="supportedLanguagesType">
+		<xs:sequence>
+			<xs:element name="DefaultLanguage" type="languageElementISO6392B"/>
+			<xs:element name="SupportedLanguage" type="languageElementISO6392B" minOccurs="0" maxOccurs="unbounded">
+				<xs:annotation>
+					<xs:documentation>It is not necessary to repeat the default
+language</xs:documentation>
+				</xs:annotation>
+			</xs:element>
+		</xs:sequence>
+	</xs:complexType>
+	<xs:element name="DiscoveryService" type="discoveryService"/>
+	<xs:element name="ViewService" type="viewService"/>
+	<xs:element name="DownloadService" type="downloadService"/>
+	<xs:element name="TransformationService" type="transformationService"/>
+	<xs:element name="InvokeService" type="invokeService"/>
+	<xs:element name="OtherService" type="otherService"/>
+	<!--	<xs:element name="ViewServiceClient" type="viewServiceClient"/> -->
+	<!--Type definitions-->
+	<!--Discovery  Service-->
+	<xs:complexType name="discoveryService_ext">
+		<xs:complexContent>
+			<xs:extension base="service">
+				<xs:sequence/>
+			</xs:extension>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="discoveryService">
+		<xs:complexContent>
+			<xs:restriction base="discoveryService_ext">
+				<xs:sequence>
+					<xs:element name="ResourceTitle" type="notEmptyString"/>
+					<xs:element name="ResourceAbstract" type="notEmptyString"/>
+					<xs:element name="ResourceType" type="serviceSpatialDataResourceType"/>
+					<xs:element name="ResourceLocator" type="resourceLocatorType" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory if linkage to the service is available</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="MandatoryKeyword" type="classificationOfSpatialDataService" minOccurs="1" maxOccurs="unbounded"/>
+					<xs:element name="Keyword" type="keyword" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">If the resource is a spatial data service, at least one keyword from Part D.4 shall be provided.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="GeographicBoundingBox" type="geographicBoundingBox" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory for services with an explicit geographic extent.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="TemporalReference" type="temporalReference" maxOccurs="unbounded"/>
+					<xs:element name="SpatialResolution" type="spatialResolution" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory when there is a restriction on the spatial resolution for this service.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="Conformity" type="conformity" maxOccurs="unbounded"/>
+					<xs:element name="ConditionsForAccessAndUse" type="notEmptyString" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation>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.
+			</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="LimitationsOnPublicAccess" type="notEmptyString" maxOccurs="unbounded"/>
+					<xs:element name="ResponsibleOrganisation" type="responsibleOrganisation" maxOccurs="unbounded"/>
+					<xs:element name="MetadataPointOfContact" type="metadataPointOfContact" maxOccurs="unbounded"/>
+					<xs:element name="MetadataDate" type="iso8601Date"/>
+					<xs:element name="MetadataLanguage" type="euLanguageISO6392B"/>
+					<xs:element name="CoupledResource" type="uniqueResourceIdentifier" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory if linkage to data sets on which the service operates are available.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="SpatialDataServiceType" type="discoverySpatialDataServiceType"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--View Service-->
+	<xs:complexType name="layers">
+		<xs:sequence>
+			<xs:element name="Layer">
+				<xs:complexType>
+					<xs:sequence>
+						<xs:element name="Name" type="xs:string">
+							<xs:annotation>
+								<xs:documentation>Harmonised name of the layer</xs:documentation>
+							</xs:annotation>
+						</xs:element>
+						<xs:element name="ResourceTitle" type="xs:string">
+							<xs:annotation>
+								<xs:documentation>Layer Title</xs:documentation>
+							</xs:annotation>
+						</xs:element>
+						<xs:element name="ResourceAbstract" type="xs:string"/>
+						<xs:element name="Keyword" type="keyword" minOccurs="0" maxOccurs="unbounded"/>
+						<xs:element name="GeographicBoundingBox" type="geographicBoundingBox" maxOccurs="unbounded"/>
+						<xs:element name="UniqueResourceIdentifier" type="uniqueResourceIdentifier" minOccurs="1" maxOccurs="unbounded"/>
+						<xs:element name="CoordinateReferenceSystems">
+							<xs:annotation>
+								<xs:documentation>List of Coordinate Reference Systems in which the layer is available</xs:documentation>
+							</xs:annotation>
+							<xs:complexType>
+								<xs:sequence>
+									<xs:element name="CRS" maxOccurs="unbounded">
+										<xs:complexType>
+											<xs:sequence>
+												<xs:element name="CRSName" type="xs:string"/>
+												<xs:element name="CRSLabel" type="xs:string"/>
+											</xs:sequence>
+										</xs:complexType>
+									</xs:element>
+								</xs:sequence>
+							</xs:complexType>
+						</xs:element>
+						<xs:element name="Styles">
+							<xs:complexType>
+								<xs:sequence>
+									<xs:element name="Style">
+										<xs:complexType>
+											<xs:sequence>
+												<xs:element name="StyleName" type="xs:string"/>
+												<xs:element name="StyleTitle" type="xs:string"/>
+												<xs:element name="StyleAbstract">
+													<xs:complexType/>
+												</xs:element>
+												<xs:element name="StyleFormat">
+													<xs:complexType/>
+												</xs:element>
+												<xs:element name="StyleURL">
+													<xs:complexType/>
+												</xs:element>
+												<xs:element name="LegendList">
+													<xs:complexType>
+														<xs:sequence>
+															<xs:element name="Legend">
+																<xs:complexType>
+																	<xs:sequence>
+																		<xs:element name="LegendFormat">
+																			<xs:complexType/>
+																		</xs:element>
+																		<xs:element name="LegendHeight" type="xs:byte"/>
+																		<xs:element name="LegendWidth" type="xs:byte"/>
+																		<xs:element name="LegendURL">
+																			<xs:complexType/>
+																		</xs:element>
+																	</xs:sequence>
+																</xs:complexType>
+															</xs:element>
+														</xs:sequence>
+													</xs:complexType>
+												</xs:element>
+											</xs:sequence>
+											<xs:attribute name="name" type="xs:string" use="required"/>
+										</xs:complexType>
+									</xs:element>
+								</xs:sequence>
+							</xs:complexType>
+						</xs:element>
+					</xs:sequence>
+					<xs:attribute name="layerName" type="xs:string"/>
+					<xs:attribute name="queryable" type="xs:boolean"/>
+				</xs:complexType>
+			</xs:element>
+		</xs:sequence>
+	</xs:complexType>
+	<xs:complexType name="viewService_ext">
+		<xs:complexContent>
+			<xs:extension base="service">
+				<xs:sequence>
+					<xs:element name="Layers" type="layers"/>
+				</xs:sequence>
+			</xs:extension>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="viewService">
+		<xs:complexContent>
+			<xs:restriction base="viewService_ext">
+				<xs:sequence>
+					<xs:element name="ResourceTitle" type="notEmptyString"/>
+					<xs:element name="ResourceAbstract" type="notEmptyString"/>
+					<xs:element name="ResourceType" type="serviceSpatialDataResourceType"/>
+					<xs:element name="ResourceLocator" type="resourceLocatorType" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory if linkage to the service is available</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="MandatoryKeyword" type="classificationOfSpatialDataService" minOccurs="1" maxOccurs="unbounded"/>
+					<xs:element name="Keyword" type="keyword" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">If the resource is a spatial data service, at least one keyword from Part D.4 shall be provided.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="GeographicBoundingBox" type="geographicBoundingBox" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory for services with an explicit geographic extent.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="TemporalReference" type="temporalReference" maxOccurs="unbounded"/>
+					<xs:element name="SpatialResolution" type="spatialResolution" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory when there is a restriction on the spatial resolution for this service.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="Conformity" type="conformity" maxOccurs="unbounded"/>
+					<xs:element name="ConditionsForAccessAndUse" type="notEmptyString" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation>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.
+			</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="LimitationsOnPublicAccess" type="notEmptyString" maxOccurs="unbounded"/>
+					<xs:element name="ResponsibleOrganisation" type="responsibleOrganisation" maxOccurs="unbounded"/>
+					<xs:element name="MetadataPointOfContact" type="metadataPointOfContact" maxOccurs="unbounded"/>
+					<xs:element name="MetadataDate" type="iso8601Date"/>
+					<xs:element name="MetadataLanguage" type="euLanguageISO6392B"/>
+					<xs:element name="CoupledResource" type="uniqueResourceIdentifier" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory if linkage to data sets on which the service operates are available.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="SpatialDataServiceType" type="viewSpatialDataServiceType"/>
+					<xs:element name="Layers" type="layers"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Download Service-->
+	<xs:complexType name="downloadService_ext">
+		<xs:complexContent>
+			<xs:extension base="service">
+				<xs:sequence/>
+			</xs:extension>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="downloadService">
+		<xs:complexContent>
+			<xs:restriction base="downloadService_ext">
+				<xs:sequence>
+					<xs:element name="ResourceTitle" type="notEmptyString"/>
+					<xs:element name="ResourceAbstract" type="notEmptyString"/>
+					<xs:element name="ResourceType" type="serviceSpatialDataResourceType"/>
+					<xs:element name="ResourceLocator" type="resourceLocatorType" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory if linkage to the service is available</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="MandatoryKeyword" type="classificationOfSpatialDataService" minOccurs="1" maxOccurs="unbounded"/>
+					<xs:element name="Keyword" type="keyword" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">If the resource is a spatial data service, at least one keyword from Part D.4 shall be provided.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="GeographicBoundingBox" type="geographicBoundingBox" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory for services with an explicit geographic extent.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="TemporalReference" type="temporalReference" maxOccurs="unbounded"/>
+					<xs:element name="SpatialResolution" type="spatialResolution" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory when there is a restriction on the spatial resolution for this service.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="Conformity" type="conformity" maxOccurs="unbounded"/>
+					<xs:element name="ConditionsForAccessAndUse" type="notEmptyString" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation>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.
+			</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="LimitationsOnPublicAccess" type="notEmptyString" maxOccurs="unbounded"/>
+					<xs:element name="ResponsibleOrganisation" type="responsibleOrganisation" maxOccurs="unbounded"/>
+					<xs:element name="MetadataPointOfContact" type="metadataPointOfContact" maxOccurs="unbounded"/>
+					<xs:element name="MetadataDate" type="iso8601Date"/>
+					<xs:element name="MetadataLanguage" type="euLanguageISO6392B"/>
+					<xs:element name="CoupledResource" type="uniqueResourceIdentifier" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory if linkage to data sets on which the service operates are available.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="SpatialDataServiceType" type="transformationSpatialDataServiceType"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Transformation  Service-->
+	<xs:complexType name="transformationService_ext">
+		<xs:complexContent>
+			<xs:extension base="service">
+				<xs:sequence/>
+			</xs:extension>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="transformationService">
+		<xs:complexContent>
+			<xs:restriction base="transformationService_ext">
+				<xs:sequence>
+					<xs:element name="ResourceTitle" type="notEmptyString"/>
+					<xs:element name="ResourceAbstract" type="notEmptyString"/>
+					<xs:element name="ResourceType" type="serviceSpatialDataResourceType"/>
+					<xs:element name="ResourceLocator" type="resourceLocatorType" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory if linkage to the service is available</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="MandatoryKeyword" type="classificationOfSpatialDataService" minOccurs="1" maxOccurs="unbounded"/>
+					<xs:element name="Keyword" type="keyword" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">If the resource is a spatial data service, at least one keyword from Part D.4 shall be provided.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="GeographicBoundingBox" type="geographicBoundingBox" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory for services with an explicit geographic extent.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="TemporalReference" type="temporalReference" maxOccurs="unbounded"/>
+					<xs:element name="SpatialResolution" type="spatialResolution" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory when there is a restriction on the spatial resolution for this service.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="Conformity" type="conformity" maxOccurs="unbounded"/>
+					<xs:element name="ConditionsForAccessAndUse" type="notEmptyString" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation>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.
+			</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="LimitationsOnPublicAccess" type="notEmptyString" maxOccurs="unbounded"/>
+					<xs:element name="ResponsibleOrganisation" type="responsibleOrganisation" maxOccurs="unbounded"/>
+					<xs:element name="MetadataPointOfContact" type="metadataPointOfContact" maxOccurs="unbounded"/>
+					<xs:element name="MetadataDate" type="iso8601Date"/>
+					<xs:element name="MetadataLanguage" type="euLanguageISO6392B"/>
+					<xs:element name="CoupledResource" type="uniqueResourceIdentifier" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory if linkage to data sets on which the service operates are available.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="SpatialDataServiceType" type="transformationSpatialDataServiceType"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Invoke Service-->
+	<xs:complexType name="invokeService_ext">
+		<xs:complexContent>
+			<xs:extension base="service">
+				<xs:sequence/>
+			</xs:extension>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="invokeService">
+		<xs:complexContent>
+			<xs:restriction base="invokeService_ext">
+				<xs:sequence>
+					<xs:element name="ResourceTitle" type="notEmptyString"/>
+					<xs:element name="ResourceAbstract" type="notEmptyString"/>
+					<xs:element name="ResourceType" type="serviceSpatialDataResourceType"/>
+					<xs:element name="ResourceLocator" type="resourceLocatorType" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory if linkage to the service is available</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="MandatoryKeyword" type="classificationOfSpatialDataService" minOccurs="1" maxOccurs="unbounded"/>
+					<xs:element name="Keyword" type="keyword" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">If the resource is a spatial data service, at least one keyword from Part D.4 shall be provided.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="GeographicBoundingBox" type="geographicBoundingBox" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory for services with an explicit geographic extent.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="TemporalReference" type="temporalReference" maxOccurs="unbounded"/>
+					<xs:element name="SpatialResolution" type="spatialResolution" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory when there is a restriction on the spatial resolution for this service.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="Conformity" type="conformity" maxOccurs="unbounded"/>
+					<xs:element name="ConditionsForAccessAndUse" type="notEmptyString" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation>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.
+			</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="LimitationsOnPublicAccess" type="notEmptyString" maxOccurs="unbounded"/>
+					<xs:element name="ResponsibleOrganisation" type="responsibleOrganisation" maxOccurs="unbounded"/>
+					<xs:element name="MetadataPointOfContact" type="metadataPointOfContact" maxOccurs="unbounded"/>
+					<xs:element name="MetadataDate" type="iso8601Date"/>
+					<xs:element name="MetadataLanguage" type="euLanguageISO6392B"/>
+					<xs:element name="CoupledResource" type="uniqueResourceIdentifier" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory if linkage to data sets on which the service operates are available.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="SpatialDataServiceType" type="discoverySpatialDataServiceType"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+	<!--Other  Service-->
+	<xs:complexType name="otherService_ext">
+		<xs:complexContent>
+			<xs:extension base="service">
+				<xs:sequence/>
+			</xs:extension>
+		</xs:complexContent>
+	</xs:complexType>
+	<xs:complexType name="otherService">
+		<xs:complexContent>
+			<xs:restriction base="otherService_ext">
+				<xs:sequence>
+					<xs:element name="ResourceTitle" type="notEmptyString"/>
+					<xs:element name="ResourceAbstract" type="notEmptyString"/>
+					<xs:element name="ResourceType" type="serviceSpatialDataResourceType"/>
+					<xs:element name="ResourceLocator" type="resourceLocatorType" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory if linkage to the service is available</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="MandatoryKeyword" type="classificationOfSpatialDataService" minOccurs="1" maxOccurs="unbounded"/>
+					<xs:element name="Keyword" type="keyword" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">If the resource is a spatial data service, at least one keyword from Part D.4 shall be provided.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="GeographicBoundingBox" type="geographicBoundingBox" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory for services with an explicit geographic extent.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="TemporalReference" type="temporalReference" maxOccurs="unbounded"/>
+					<xs:element name="SpatialResolution" type="spatialResolution" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory when there is a restriction on the spatial resolution for this service.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="Conformity" type="conformity" maxOccurs="unbounded"/>
+					<xs:element name="ConditionsForAccessAndUse" type="notEmptyString" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation>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.
+			</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="LimitationsOnPublicAccess" type="notEmptyString" maxOccurs="unbounded"/>
+					<xs:element name="ResponsibleOrganisation" type="responsibleOrganisation" maxOccurs="unbounded"/>
+					<xs:element name="MetadataPointOfContact" type="metadataPointOfContact" maxOccurs="unbounded"/>
+					<xs:element name="MetadataDate" type="iso8601Date"/>
+					<xs:element name="MetadataLanguage" type="euLanguageISO6392B"/>
+					<xs:element name="CoupledResource" type="uniqueResourceIdentifier" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation xml:lang="en">Mandatory if linkage to data sets on which the service operates are available.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="SpatialDataServiceType" type="discoverySpatialDataServiceType"/>
+				</xs:sequence>
+			</xs:restriction>
+		</xs:complexContent>
+	</xs:complexType>
+</xs:schema>
diff --git a/mapproxy/test/schemas/inspire/inspire_vs/1.0/examples/WMS_Image2000GetCapabilities_InspireSchema.xml b/mapproxy/test/schemas/inspire/inspire_vs/1.0/examples/WMS_Image2000GetCapabilities_InspireSchema.xml
new file mode 100644
index 0000000..4ccd8fe
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/inspire_vs/1.0/examples/WMS_Image2000GetCapabilities_InspireSchema.xml
@@ -0,0 +1,271 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+26-APR-2011 1.0.1 Conformity element:
+                    Restricted allowed citations to "INSPIRE interoperability of spatial data sets and services", according to the MD regulation,
+                    requirements for the conformity element (page 17 of the MD regulation).
+                  Inspire Themes:
+                    Removed leading blank for German, Bulgarian, Czech and Danish translations
+17-MAR-2011 Changed alias inspire_common and inspire_vs
+18-FEB-2011 Changed encoding for Language Elements in Network Services
+16-FEB-2011 Introduced namespace "common"
+                    Removed unnecessary namespace references
+20-DEC-2010 Restored ins_com alias
+16-DEC-2010 Switched to INSPIRE Schema
+-->
+<WMS_Capabilities version="1.3.0" xmlns="http://www.opengis.net/wms" xmlns:inspire_common="http://inspire.ec.europa.eu/schemas/common/1.0" xmlns:inspire_vs="http://inspire.ec.europa.eu/schemas/inspire_vs/1.0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://inspire.ec.europa.eu/schemas/inspire_vs/1.0 http://inspire.ec.europa.eu/schemas/inspire_vs/1.0/inspire_vs.xsd">
+	<Service>
+		<Name>WMS</Name>
+		<Title>Europe Mapping Service based on the Image2000 European mosaic (panchromatic)</Title>
+		<Abstract>Map Service of a European Mosaic. The mosaic is a harmonised dataset and is based on the individual orthorectified scenes from the Image 2000 project. This service is based on the panchromatic sensor of the Landsat 7 satellite.</Abstract>
+		<KeywordList>
+			<Keyword vocabulary="ISO">humanGeographicViewer</Keyword>
+			<Keyword vocabulary="ISO">infoMapAccessService</Keyword>
+			<Keyword>satellite imagery</Keyword>
+			<Keyword vocabulary="GEMET - INSPIRE themes">orthoimagery</Keyword>
+		</KeywordList>
+		<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://ags-sdi-public.jrc.ec.europa.eu/arcgis/services/image2000_pan/Mapserver/WMSServer?request=GetCapabilities&service=wms&version=1.3.0"/>
+		<ContactInformation>
+			<ContactPersonPrimary>
+				<ContactPerson>Hildegard Gerlach</ContactPerson>
+				<ContactOrganization>Spatial Data Infrastructure Unit, Institute for Environment & Sustainability, Joint Research Centre, European Commission</ContactOrganization>
+			</ContactPersonPrimary>
+			<ContactPosition>pointOfContact</ContactPosition>
+			<ContactAddress>
+				<AddressType>Postal</AddressType>
+				<Address>Via Fermi 2749</Address>
+				<City>Ispra</City>
+				<StateOrProvince>VA</StateOrProvince>
+				<PostCode>21027</PostCode>
+				<Country>Italy</Country>
+			</ContactAddress>
+			<ContactVoiceTelephone>+390332785638</ContactVoiceTelephone>
+			<ContactFacsimileTelephone>+390332785638</ContactFacsimileTelephone>
+			<ContactElectronicMailAddress>hildegard.gerlach at jrc.ec.europa.eu</ContactElectronicMailAddress>
+		</ContactInformation>
+		<Fees>no conditions apply</Fees>
+		<AccessConstraints>no limitations</AccessConstraints>
+		<MaxWidth>2048</MaxWidth>
+		<MaxHeight>2048</MaxHeight>
+	</Service>
+	<Capability>
+		<Request>
+			<GetCapabilities>
+				<Format>application/vnd.ogc.wms_xml</Format>
+				<Format>text/xml</Format>
+				<DCPType>
+					<HTTP>
+						<Get>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://ags-sdi-public.jrc.ec.europa.eu/arcgis/services/image2000_pan/MapServer/WMSServer"/>
+						</Get>
+					</HTTP>
+				</DCPType>
+			</GetCapabilities>
+			<GetMap>
+				<Format>image/bmp</Format>
+				<Format>image/jpeg</Format>
+				<Format>image/tiff</Format>
+				<Format>image/png</Format>
+				<Format>image/gif</Format>
+				<Format>image/svg+xml</Format>
+				<DCPType>
+					<HTTP>
+						<Get>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://ags-sdi-public.jrc.ec.europa.eu/arcgis/services/image2000_pan/MapServer/WMSServer"/>
+						</Get>
+					</HTTP>
+				</DCPType>
+			</GetMap>
+			<GetFeatureInfo>
+				<Format>application/vnd.ogc.wms_xml</Format>
+				<Format>text/xml</Format>
+				<Format>text/html</Format>
+				<Format>text/plain</Format>
+				<DCPType>
+					<HTTP>
+						<Get>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://ags-sdi-public.jrc.ec.europa.eu/arcgis/services/image2000_pan/MapServer/WMSServer"/>
+						</Get>
+					</HTTP>
+				</DCPType>
+			</GetFeatureInfo>
+		</Request>
+		<Exception>
+			<Format>application/vnd.ogc.se_xml</Format>
+			<Format>application/vnd.ogc.se_inimage</Format>
+			<Format>application/vnd.ogc.se_blank</Format>
+			<Format>text/xml</Format>
+			<Format>XML</Format>
+		</Exception>
+		<inspire_vs:ExtendedCapabilities>
+			<inspire_common:ResourceLocator>
+				<inspire_common:URL>http://image2000.jrc.ec.europa.eu</inspire_common:URL>
+				<inspire_common:MediaType>text/html</inspire_common:MediaType>
+			</inspire_common:ResourceLocator>
+			<inspire_common:ResourceLocator>
+				<inspire_common:URL>http://ags-sdi-public.jrc.ec.europa.eu/arcgis/services/image2000_pan/Mapserver/WMSServer?request=GetCapabilities&service=wms&version=1.3.0</inspire_common:URL>
+				<inspire_common:MediaType>application/vnd.ogc.wms_xml</inspire_common:MediaType>
+			</inspire_common:ResourceLocator>
+			<inspire_common:ResourceType>service</inspire_common:ResourceType>
+			<inspire_common:TemporalReference>
+				<inspire_common:DateOfCreation>2005-12-12</inspire_common:DateOfCreation>
+				<inspire_common:DateOfLastRevision>2005-12-13</inspire_common:DateOfLastRevision>
+				<inspire_common:DateOfPublication>2005-12-13</inspire_common:DateOfPublication>
+			</inspire_common:TemporalReference>
+			<inspire_common:TemporalReference>
+				<inspire_common:TemporalExtent>
+					<inspire_common:IndividualDate>2005-01-01T00:00:00</inspire_common:IndividualDate>
+					<inspire_common:IntervalOfDates>
+						<inspire_common:StartingDate>2005-01-01T00:00:00</inspire_common:StartingDate>
+						<inspire_common:EndDate>2005-12-31T00:00:00</inspire_common:EndDate>
+					</inspire_common:IntervalOfDates>
+				</inspire_common:TemporalExtent>
+			</inspire_common:TemporalReference>
+			<inspire_common:Conformity>
+				<inspire_common:Specification xsi:type="inspire_common:citationInspireInteroperabilityRegulation_eng">
+					<inspire_common: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</inspire_common:Title>
+					<inspire_common:DateOfPublication>2010-12-08</inspire_common:DateOfPublication>
+					<inspire_common:URI>OJ:L:2010:323:0011:0102:EN:PDF</inspire_common:URI>
+					<inspire_common:ResourceLocator>
+						<inspire_common:URL>http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:EN:PDF</inspire_common:URL>
+						<inspire_common:MediaType>application/pdf</inspire_common:MediaType>
+					</inspire_common:ResourceLocator>
+				</inspire_common:Specification>
+				<inspire_common:Degree>conformant</inspire_common:Degree>
+			</inspire_common:Conformity>
+			<inspire_common:MetadataPointOfContact>
+				<inspire_common:OrganisationName>Spatial Data Infrastructure Unit, Institute for Environment & Sustainability, Joint Research Centre, European Commission</inspire_common:OrganisationName>
+				<inspire_common:EmailAddress>image2000 at jrc.ec.europa.eu</inspire_common:EmailAddress>
+			</inspire_common:MetadataPointOfContact>
+			<inspire_common:MetadataDate>2010-06-15</inspire_common:MetadataDate>
+			<inspire_common:SpatialDataServiceType>view</inspire_common:SpatialDataServiceType>
+			<inspire_common:MandatoryKeyword>
+				<inspire_common:KeywordValue>humanGeographicViewer</inspire_common:KeywordValue>
+			</inspire_common:MandatoryKeyword>
+			<inspire_common:MandatoryKeyword>
+				<inspire_common:KeywordValue>infoMapAccessService</inspire_common:KeywordValue>
+			</inspire_common:MandatoryKeyword>
+			<inspire_common:Keyword xsi:type="inspire_common:inspireTheme_eng">
+				<inspire_common:OriginatingControlledVocabulary>
+					<inspire_common:Title>GEMET - INSPIRE themes</inspire_common:Title>
+					<inspire_common:DateOfPublication>2008-06-01</inspire_common:DateOfPublication>
+				</inspire_common:OriginatingControlledVocabulary>
+				<inspire_common:KeywordValue>Orthoimagery</inspire_common:KeywordValue>
+			</inspire_common:Keyword>
+			<inspire_common:Keyword>
+				<inspire_common:KeywordValue>satellite imagery</inspire_common:KeywordValue>
+			</inspire_common:Keyword>
+			<inspire_common:SupportedLanguages>
+				<inspire_common:DefaultLanguage>
+					<inspire_common:Language>eng</inspire_common:Language>
+				</inspire_common:DefaultLanguage>
+				<inspire_common:SupportedLanguage>
+					<inspire_common:Language>cze</inspire_common:Language>
+				</inspire_common:SupportedLanguage>
+			</inspire_common:SupportedLanguages>
+			<inspire_common:ResponseLanguage>
+				<inspire_common:Language>eng</inspire_common:Language>
+			</inspire_common:ResponseLanguage>
+			<inspire_common:MetadataUrl>
+				<inspire_common:URL>	http://www.inspire-geoportal.eu/discovery/csw?Service=CSW&Request=GetRecordById&Version=2.0.2&id=jrc_img2k_service_pan&outputSchema=http://www.isotc211.org/2005/gmd&elementSetName=full</inspire_common:URL>
+				<inspire_common:MediaType>application/vnd.iso.19139+xml</inspire_common:MediaType>
+			</inspire_common:MetadataUrl>
+		</inspire_vs:ExtendedCapabilities>
+		<Layer>
+			<!-- Each layer in the capabilities that has a <Name> element can be requested to the service, therefore we have put it out (it is not physical layer of WMS, but logical. 
+			<Name>OI.IM2K_PAN.Layers</Name>-->
+			<Title>Image 2000 Panchromatic Mosaic Layers</Title>
+			<CRS>CRS:84</CRS>
+			<CRS>EPSG:4326</CRS>
+			<CRS>EPSG:4258</CRS>
+			<CRS>EPSG:3035</CRS>
+			<CRS>EPSG:3034</CRS>
+			<EX_GeographicBoundingBox>
+				<westBoundLongitude>-14.317500</westBoundLongitude>
+				<eastBoundLongitude>35.017500</eastBoundLongitude>
+				<southBoundLatitude>32.688750</southBoundLatitude>
+				<northBoundLatitude>72.536250</northBoundLatitude>
+			</EX_GeographicBoundingBox>
+			<BoundingBox CRS="CRS:84" minx="-14.317500" miny="32.688750" maxx="35.017500" maxy="72.536250"/>
+			<BoundingBox CRS="EPSG:4326" minx="32.688750" miny="-14.317500" maxx="72.536250" maxy="35.017500"/>
+			<BoundingBox CRS="EPSG:4258" minx="32.688750" miny="-14.317500" maxx="72.536250" maxy="35.017500"/>
+			<BoundingBox CRS="EPSG:3035" minx="1065289.448885" miny="2096360.959822" maxx="5634890.300005" maxy="6607617.368400"/>
+			<BoundingBox CRS="EPSG:3034" minx="695310.772226" miny="1795068.846538" maxx="5204512.451326" maxy="6266414.527296"/>
+			<Layer>
+				<Name>0</Name>
+				<Title>Mosaic</Title>
+				<Abstract>Image 2000 Panchromatic Mosaic</Abstract>
+				<KeywordList>
+					<Keyword>Orthoimagery</Keyword>
+				</KeywordList>
+				<CRS>CRS:84</CRS>
+				<CRS>EPSG:4326</CRS>
+				<CRS>EPSG:4258</CRS>
+				<CRS>EPSG:3035</CRS>
+				<CRS>EPSG:3034</CRS>
+				<EX_GeographicBoundingBox>
+					<westBoundLongitude>-12.075000</westBoundLongitude>
+					<eastBoundLongitude>32.775000</eastBoundLongitude>
+					<southBoundLatitude>34.500000</southBoundLatitude>
+					<northBoundLatitude>70.725000</northBoundLatitude>
+				</EX_GeographicBoundingBox>
+				<BoundingBox CRS="CRS:84" minx="-12.075000" miny="34.500000" maxx="32.775000" maxy="70.725000"/>
+				<BoundingBox CRS="EPSG:4326" minx="34.500000" miny="-12.075000" maxx="70.725000" maxy="32.775000"/>
+				<BoundingBox CRS="EPSG:4258" minx="34.500000" miny="-12.075000" maxx="70.725000" maxy="32.775000"/>
+				<BoundingBox CRS="EPSG:3035" minx="1272998.578481" miny="2301418.069303" maxx="5427181.170408" maxy="6402560.258919"/>
+				<BoundingBox CRS="EPSG:3034" minx="900274.484912" miny="1998311.832027" maxx="4999548.738640" maxy="6063171.541807"/>
+				<MetadataURL type="ISO19115:2003 ">
+					<Format>text/xml</Format>
+					<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://www.inspire-geoportal.eu/discovery/csw?Service=CSW&Request=GetRecordById&Version=2.0.2&id=jrc_img2k_pr5_mosaic_PAN&outputSchema=http://www.isotc211.org/2005/gmd&elementSetName=full#jrc_img2k_pr5_mosaic_PAN"/>
+				</MetadataURL>
+				<Style>
+					<Name>inspire_common:DEFAULT</Name>
+					<Title>Image 2000 Panchromatic Mosaic</Title>
+					<LegendURL width="100" height="40">
+						<Format>image/png</Format>
+						<OnlineResource xlink:href="http://ags-sdi-public.jrc.ec.europa.eu/arcgis/wms/image2000_pan/default0.png" xlink:type="simple" xmlns:xlink="http://www.w3.org/1999/xlink"/>
+					</LegendURL>
+				</Style>
+				<Layer>
+					<Name>0.1</Name>
+					<Title>RecursionTest</Title>
+					<Abstract>Image 2000 Panchromatic Mosaic</Abstract>
+					<KeywordList>
+						<Keyword>Orthoimagery</Keyword>
+					</KeywordList>
+					<CRS>CRS:84</CRS>
+					<CRS>EPSG:4326</CRS>
+					<CRS>EPSG:4258</CRS>
+					<CRS>EPSG:3035</CRS>
+					<CRS>EPSG:3034</CRS>
+					<EX_GeographicBoundingBox>
+						<westBoundLongitude>-12.075000</westBoundLongitude>
+						<eastBoundLongitude>32.775000</eastBoundLongitude>
+						<southBoundLatitude>34.500000</southBoundLatitude>
+						<northBoundLatitude>70.725000</northBoundLatitude>
+					</EX_GeographicBoundingBox>
+					<BoundingBox CRS="CRS:84" minx="-12.075000" miny="34.500000" maxx="32.775000" maxy="70.725000"/>
+					<BoundingBox CRS="EPSG:4326" minx="34.500000" miny="-12.075000" maxx="70.725000" maxy="32.775000"/>
+					<BoundingBox CRS="EPSG:4258" minx="34.500000" miny="-12.075000" maxx="70.725000" maxy="32.775000"/>
+					<BoundingBox CRS="EPSG:3035" minx="1272998.578481" miny="2301418.069303" maxx="5427181.170408" maxy="6402560.258919"/>
+					<BoundingBox CRS="EPSG:3034" minx="900274.484912" miny="1998311.832027" maxx="4999548.738640" maxy="6063171.541807"/>
+					<MetadataURL type="ISO19115:2003 ">
+						<Format>text/xml</Format>
+						<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://www.inspire-geoportal.eu/discovery/csw?Service=CSW&Request=GetRecordById&Version=2.0.2&id=jrc_img2k_pr5_mosaic_PAN&outputSchema=http://www.isotc211.org/2005/gmd&elementSetName=full#jrc_img2k_pr5_mosaic_PAN21"/>
+					</MetadataURL>
+					<MetadataURL type="ISO19115:2003 ">
+						<Format>text/xml</Format>
+						<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://www.inspire-geoportal.eu/discovery/csw?Service=CSW&Request=GetRecordById&Version=2.0.2&id=jrc_img2k_pr5_mosaic_PAN&outputSchema=http://www.isotc211.org/2005/gmd&elementSetName=full#jrc_img2k_pr5_mosaic_PAN22"/>
+					</MetadataURL>
+					<Style>
+						<Name>inspire_common:DEFAULT</Name>
+						<Title>Image 2000 Panchromatic Mosaic</Title>
+						<LegendURL width="100" height="40">
+							<Format>image/png</Format>
+							<OnlineResource xlink:href="http://ags-sdi-public.jrc.ec.europa.eu/arcgis/wms/image2000_pan/default0.png" xlink:type="simple" xmlns:xlink="http://www.w3.org/1999/xlink"/>
+						</LegendURL>
+					</Style>
+				</Layer>
+			</Layer>
+		</Layer>
+	</Capability>
+</WMS_Capabilities>
diff --git a/mapproxy/test/schemas/inspire/inspire_vs/1.0/examples/wms_at.xml b/mapproxy/test/schemas/inspire/inspire_vs/1.0/examples/wms_at.xml
new file mode 100644
index 0000000..66656d9
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/inspire_vs/1.0/examples/wms_at.xml
@@ -0,0 +1,358 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!--
+26-APR-2011 1.0.1 Conformity element:
+                    Restricted allowed citations to "INSPIRE interoperability of spatial data sets and services", according to the MD regulation,
+                    requirements for the conformity element (page 17 of the MD regulation).
+                  Inspire Themes:
+                    Removed leading blank for German, Bulgarian, Czech and Danish translations
+17-MAR-2011 Changed alias inspire_common and inspire_vs
+18-FEB-2011 Changed encoding for Language Elements in Network Services
+16-FEB-2011 Introduced namespace "common"
+                    Removed unnecessary namespace references
+                    Switched to short form of INSPIRE namespace alises
+-->
+<WMS_Capabilities xmlns="http://www.opengis.net/wms" xmlns:sld="http://www.opengis.net/sld" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ms="http://mapserver.gis.umn.edu/mapserver" xmlns:inspire_common="http://inspire.ec.europa.eu/schemas/common/1.0" xmlns:inspire_vs="http://inspire.ec.europa.eu/schemas/inspire_vs/1.0" version="1.3.0" xsi:schemaLocation="http://www.opengis.net/wms http://schemas.opengis.net/wms/1.3.0/capabilities_1_3_0.xsd  http://www.opengis.net/sld http: [...]
+	<!-- MapServer version 5.6.5 OUTPUT=GIF OUTPUT=PNG OUTPUT=JPEG OUTPUT=WBMP OUTPUT=PDF OUTPUT=SVG SUPPORTS=PROJ SUPPORTS=AGG SUPPORTS=CAIRO SUPPORTS=FREETYPE SUPPORTS=ICONV SUPPORTS=FRIBIDI SUPPORTS=WMS_SERVER SUPPORTS=WMS_CLIENT SUPPORTS=WFS_SERVER SUPPORTS=WFS_CLIENT SUPPORTS=WCS_SERVER SUPPORTS=SOS_SERVER SUPPORTS=FASTCGI SUPPORTS=THREADS SUPPORTS=GEOS SUPPORTS=RGBA_PNG INPUT=JPEG INPUT=POSTGIS INPUT=OGR INPUT=GDAL INPUT=SHAPEFILE -->
+	<Service>
+		<Name>WMS</Name>
+		<Title>Geoimage_1m</Title>
+		<Abstract>This service provides starting from October 2010 middle resolution orthoimagery of Austria and is intended to support open Data communities such as openstreetmap.org in creating new or enhancing free spatial data. This service is provided for the time being, there is no guarantee for long-term availability.</Abstract>
+		<KeywordList>
+			<Keyword>GEOIMAGE</Keyword>
+			<Keyword>ORTHOPHOTO</Keyword>
+			<Keyword>ORTHOFOTO</Keyword>
+			<Keyword>REMOTE SENSING</Keyword>
+			<Keyword>AUSTRIA</Keyword>
+			<Keyword>LFRZ</Keyword>
+		</KeywordList>
+		<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta110223?"/>
+		<ContactInformation>
+			<ContactPersonPrimary>
+				<ContactPerson>Wolfgang Tinkl</ContactPerson>
+				<ContactOrganization>http://www.geoimage.at</ContactOrganization>
+			</ContactPersonPrimary>
+			<ContactElectronicMailAddress>office at geoimage.at</ContactElectronicMailAddress>
+		</ContactInformation>
+		<Fees>no conditions apply</Fees>
+		<AccessConstraints>The content of this service is copyright by GEOIMAGE-AUSTRIA(R) and protected by law. This WMS can be used by anybody for private, governmental and non-profit purposes. Use for Commercial purposes is NOT allowed, however secondary products (created on basis of this service) are not subject to this non-commercial restriction as long as these secondary products are made freely available e.g. under the Creative Commons Attribution-ShareAlike 2.0 (CC-BY-SA) license. Syst [...]
+		<MaxWidth>2048</MaxWidth>
+		<MaxHeight>2048</MaxHeight>
+	</Service>
+	<Capability>
+		<Request>
+			<GetCapabilities>
+				<Format>text/xml</Format>
+				<DCPType>
+					<HTTP>
+						<Get>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta110223?"/>
+						</Get>
+						<Post>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta110223?"/>
+						</Post>
+					</HTTP>
+				</DCPType>
+			</GetCapabilities>
+			<GetMap>
+				<Format>image/png</Format>
+				<Format>image/jpg</Format>
+				<Format>image/gif</Format>
+				<Format>image/png; mode=24bit</Format>
+				<Format>image/vnd.wap.wbmp</Format>
+				<Format>image/jpeg</Format>
+				<Format>image/tiff</Format>
+				<Format>image/svg+xml</Format>
+				<DCPType>
+					<HTTP>
+						<Get>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta110223?"/>
+						</Get>
+						<Post>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta110223?"/>
+						</Post>
+					</HTTP>
+				</DCPType>
+			</GetMap>
+			<GetFeatureInfo>
+				<Format>text/plain</Format>
+				<Format>application/vnd.ogc.gml</Format>
+				<DCPType>
+					<HTTP>
+						<Get>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta110223?"/>
+						</Get>
+						<Post>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta110223?"/>
+						</Post>
+					</HTTP>
+				</DCPType>
+			</GetFeatureInfo>
+			<sld:DescribeLayer>
+				<Format>text/xml</Format>
+				<DCPType>
+					<HTTP>
+						<Get>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta110223?"/>
+						</Get>
+						<Post>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta110223?"/>
+						</Post>
+					</HTTP>
+				</DCPType>
+			</sld:DescribeLayer>
+			<sld:GetLegendGraphic>
+				<Format>image/png</Format>
+				<Format>image/jpg</Format>
+				<Format>image/gif</Format>
+				<Format>image/png; mode=24bit</Format>
+				<Format>image/vnd.wap.wbmp</Format>
+				<Format>image/jpeg</Format>
+				<DCPType>
+					<HTTP>
+						<Get>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta110223?"/>
+						</Get>
+						<Post>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta110223?"/>
+						</Post>
+					</HTTP>
+				</DCPType>
+			</sld:GetLegendGraphic>
+			<ms:GetStyles>
+				<Format>text/xml</Format>
+				<DCPType>
+					<HTTP>
+						<Get>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta110223?"/>
+						</Get>
+						<Post>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta110223?"/>
+						</Post>
+					</HTTP>
+				</DCPType>
+			</ms:GetStyles>
+		</Request>
+		<Exception>
+			<Format>XML</Format>
+			<Format>INIMAGE</Format>
+			<Format>BLANK</Format>
+		</Exception>
+		<inspire_vs:ExtendedCapabilities>
+			<inspire_common:ResourceLocator>
+				<inspire_common:URL>http://www.geoimage.at</inspire_common:URL>
+				<inspire_common:MediaType>text/html</inspire_common:MediaType>
+			</inspire_common:ResourceLocator>
+			<inspire_common:ResourceLocator>
+				<inspire_common:URL>http://gis.lebensministerium.at/dop-1mfree-beta110223%5C?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetCapabilities</inspire_common:URL>
+				<inspire_common:MediaType>application/vnd.ogc.csw.capabilities.response_xml</inspire_common:MediaType>
+			</inspire_common:ResourceLocator>
+			<inspire_common:ResourceType>service</inspire_common:ResourceType>
+			<inspire_common:TemporalReference>
+				<inspire_common:DateOfCreation>2011-02-23</inspire_common:DateOfCreation>
+				<inspire_common:DateOfLastRevision>2011-02-23</inspire_common:DateOfLastRevision>
+				<inspire_common:DateOfPublication>2011-02-23</inspire_common:DateOfPublication>
+			</inspire_common:TemporalReference>
+			<inspire_common:Conformity>
+				<inspire_common:Specification xsi:type="inspire_common:citationInspireInteroperabilityRegulation_eng">
+					<inspire_common: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</inspire_common:Title>
+					<inspire_common:DateOfPublication>2010-12-08</inspire_common:DateOfPublication>
+					<inspire_common:URI>OJ:L:2010:323:0011:0102:EN:PDF</inspire_common:URI>
+					<inspire_common:ResourceLocator>
+						<inspire_common:URL>http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:EN:PDF</inspire_common:URL>
+						<inspire_common:MediaType>application/pdf</inspire_common:MediaType>
+					</inspire_common:ResourceLocator>
+				</inspire_common:Specification>
+				<inspire_common:Degree>notEvaluated</inspire_common:Degree>
+			</inspire_common:Conformity>
+			<inspire_common:MetadataPointOfContact>
+				<inspire_common:OrganisationName>www.geoimage.at</inspire_common:OrganisationName>
+				<inspire_common:EmailAddress>office at geoimage.at</inspire_common:EmailAddress>
+			</inspire_common:MetadataPointOfContact>
+			<inspire_common:MetadataDate>2011-02-23</inspire_common:MetadataDate>
+			<inspire_common:SpatialDataServiceType>view</inspire_common:SpatialDataServiceType>
+			<inspire_common:MandatoryKeyword>
+				<inspire_common:KeywordValue>humanGeographicViewer</inspire_common:KeywordValue>
+			</inspire_common:MandatoryKeyword>
+			<inspire_common:MandatoryKeyword>
+				<inspire_common:KeywordValue>infoMapAccessService</inspire_common:KeywordValue>
+			</inspire_common:MandatoryKeyword>
+			<inspire_common:Keyword xsi:type="inspire_common:inspireTheme_eng">
+				<inspire_common:OriginatingControlledVocabulary>
+					<inspire_common:Title>GEMET - INSPIRE themes</inspire_common:Title>
+					<inspire_common:DateOfPublication>2008-06-01</inspire_common:DateOfPublication>
+				</inspire_common:OriginatingControlledVocabulary>
+				<inspire_common:KeywordValue>Orthoimagery</inspire_common:KeywordValue>
+			</inspire_common:Keyword>
+			<inspire_common:SupportedLanguages>
+				<inspire_common:DefaultLanguage>
+					<inspire_common:Language>eng</inspire_common:Language>
+				</inspire_common:DefaultLanguage>
+			</inspire_common:SupportedLanguages>
+			<inspire_common:ResponseLanguage>
+				<inspire_common:Language>eng</inspire_common:Language>
+			</inspire_common:ResponseLanguage>
+			<inspire_common:MetadataUrl>
+				<inspire_common:URL>http://www.geoimage.at/test/metadata/</inspire_common:URL>
+				<inspire_common:MediaType>application/vnd.iso.19139+xml</inspire_common:MediaType>
+			</inspire_common:MetadataUrl>
+		</inspire_vs:ExtendedCapabilities>
+		<sld:UserDefinedSymbolization SupportSLD="1" UserLayer="0" UserStyle="1" RemoteWFS="0" InlineFeature="0" RemoteWCS="0"/>
+		<Layer>
+			<Name>DOP</Name>
+			<Title>Geoimage_1m</Title>
+			<Abstract>This service provides starting from October 2010 middle resolution orthoimagery of Austria and is intended to support open Data communities such as openstreetmap.org in creating new or enhancing free spatial data. This service is provided for the time being, there is no guarantee for long-term availability.</Abstract>
+			<KeywordList>
+				<Keyword>GEOIMAGE</Keyword>
+				<Keyword>ORTHOPHOTO</Keyword>
+				<Keyword>ORTHOFOTO</Keyword>
+				<Keyword>REMOTE SENSING</Keyword>
+				<Keyword>AUSTRIA</Keyword>
+				<Keyword>LFRZ</Keyword>
+			</KeywordList>
+			<CRS>EPSG:31287</CRS>
+			<CRS>EPSG:4326</CRS>
+			<CRS>EPSG:3857</CRS>
+			<CRS>EPSG:900913</CRS>
+			<CRS>EPSG:31255</CRS>
+			<EX_GeographicBoundingBox>
+				<westBoundLongitude>9.35647</westBoundLongitude>
+				<eastBoundLongitude>17.3085</eastBoundLongitude>
+				<southBoundLatitude>46.0867</southBoundLatitude>
+				<northBoundLatitude>49.2085</northBoundLatitude>
+			</EX_GeographicBoundingBox>
+			<BoundingBox CRS="EPSG:31287" minx="110000" miny="250000" maxx="690000" maxy="590000"/>
+			<Layer>
+				<Name>Orthophoto</Name>
+				<Title>Orthophoto</Title>
+				<Abstract>Orthophoto</Abstract>
+				<Layer queryable="0" opaque="0" cascaded="0">
+					<Name>Luftbild_1m</Name>
+					<Title>Luftbild_1m_LB</Title>
+					<CRS>EPSG:31287</CRS>
+					<CRS>EPSG:4326</CRS>
+					<CRS>EPSG:3857</CRS>
+					<CRS>EPSG:900913</CRS>
+					<CRS>EPSG:31255</CRS>
+					<EX_GeographicBoundingBox>
+						<westBoundLongitude>9.35647</westBoundLongitude>
+						<eastBoundLongitude>17.3085</eastBoundLongitude>
+						<southBoundLatitude>46.0867</southBoundLatitude>
+						<northBoundLatitude>49.2085</northBoundLatitude>
+					</EX_GeographicBoundingBox>
+					<BoundingBox CRS="EPSG:31287" minx="110000" miny="250000" maxx="690000" maxy="590000"/>
+					<MaxScaleDenominator>15500</MaxScaleDenominator>
+				</Layer>
+				<Layer queryable="0" opaque="0" cascaded="0">
+					<Name>Message</Name>
+					<Title>Message</Title>
+					<CRS>EPSG:31287</CRS>
+					<CRS>EPSG:4326</CRS>
+					<CRS>EPSG:3857</CRS>
+					<CRS>EPSG:900913</CRS>
+					<CRS>EPSG:31255</CRS>
+					<EX_GeographicBoundingBox>
+						<westBoundLongitude>9.35647</westBoundLongitude>
+						<eastBoundLongitude>17.3085</eastBoundLongitude>
+						<southBoundLatitude>46.0867</southBoundLatitude>
+						<northBoundLatitude>49.2085</northBoundLatitude>
+					</EX_GeographicBoundingBox>
+					<BoundingBox CRS="EPSG:31287" minx="110000" miny="250000" maxx="690000" maxy="590000"/>
+					<MaxScaleDenominator>7500</MaxScaleDenominator>
+				</Layer>
+				<Layer queryable="0" opaque="0" cascaded="0">
+					<Name>Luftbild_4m</Name>
+					<Title>Luftbild_4m_LB</Title>
+					<CRS>EPSG:31287</CRS>
+					<CRS>EPSG:4326</CRS>
+					<CRS>EPSG:3857</CRS>
+					<CRS>EPSG:900913</CRS>
+					<CRS>EPSG:31255</CRS>
+					<EX_GeographicBoundingBox>
+						<westBoundLongitude>9.35647</westBoundLongitude>
+						<eastBoundLongitude>17.3085</eastBoundLongitude>
+						<southBoundLatitude>46.0867</southBoundLatitude>
+						<northBoundLatitude>49.2085</northBoundLatitude>
+					</EX_GeographicBoundingBox>
+					<BoundingBox CRS="EPSG:31287" minx="110000" miny="250000" maxx="690000" maxy="590000"/>
+					<MinScaleDenominator>15001</MinScaleDenominator>
+					<MaxScaleDenominator>150500</MaxScaleDenominator>
+				</Layer>
+				<Layer queryable="0" opaque="0" cascaded="0">
+					<Name>Satellitenbild_30m</Name>
+					<Title>Satellitenbild_30m</Title>
+					<CRS>EPSG:31287</CRS>
+					<CRS>EPSG:4326</CRS>
+					<CRS>EPSG:3857</CRS>
+					<CRS>EPSG:900913</CRS>
+					<CRS>EPSG:31255</CRS>
+					<EX_GeographicBoundingBox>
+						<westBoundLongitude>9.35647</westBoundLongitude>
+						<eastBoundLongitude>17.3085</eastBoundLongitude>
+						<southBoundLatitude>46.0867</southBoundLatitude>
+						<northBoundLatitude>49.2085</northBoundLatitude>
+					</EX_GeographicBoundingBox>
+					<BoundingBox CRS="EPSG:31287" minx="110000" miny="250000" maxx="690000" maxy="590000"/>
+					<MinScaleDenominator>150001</MinScaleDenominator>
+					<MaxScaleDenominator>1e+007</MaxScaleDenominator>
+				</Layer>
+			</Layer>
+			<Layer>
+				<Name>Metadata</Name>
+				<Title>Metadata</Title>
+				<Abstract>Metadata</Abstract>
+				<Layer queryable="0" opaque="0" cascaded="0">
+					<Name>Metadata_Tiles</Name>
+					<Title>Metadata_Tiles</Title>
+					<CRS>EPSG:31287</CRS>
+					<CRS>EPSG:4326</CRS>
+					<CRS>EPSG:3857</CRS>
+					<CRS>EPSG:900913</CRS>
+					<CRS>EPSG:31255</CRS>
+					<EX_GeographicBoundingBox>
+						<westBoundLongitude>9.35647</westBoundLongitude>
+						<eastBoundLongitude>17.3085</eastBoundLongitude>
+						<southBoundLatitude>46.0867</southBoundLatitude>
+						<northBoundLatitude>49.2085</northBoundLatitude>
+					</EX_GeographicBoundingBox>
+					<BoundingBox CRS="EPSG:31287" minx="110000" miny="250000" maxx="690000" maxy="590000"/>
+					<Style>
+						<Name>inspire_vs:DEFAULT</Name>
+						<Title>inspire_vs:DEFAULT</Title>
+						<LegendURL width="102" height="25">
+							<Format>image/png</Format>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://wms.geoimage.at/dop-1mfree-beta110223?version=1.3.0&service=WMS&request=GetLegendGraphic&sld_version=1.1.0&layer=Metadata_Tiles&format=image/png&STYLE=inspire_vs:DEFAULT"/>
+						</LegendURL>
+					</Style>
+					<MinScaleDenominator>100</MinScaleDenominator>
+					<MaxScaleDenominator>150500</MaxScaleDenominator>
+				</Layer>
+				<Layer queryable="0" opaque="0" cascaded="0">
+					<Name>Metadata-dissolve</Name>
+					<Title>Metadata-dissolve</Title>
+					<CRS>EPSG:31287</CRS>
+					<CRS>EPSG:4326</CRS>
+					<CRS>EPSG:3857</CRS>
+					<CRS>EPSG:900913</CRS>
+					<CRS>EPSG:31255</CRS>
+					<EX_GeographicBoundingBox>
+						<westBoundLongitude>9.35647</westBoundLongitude>
+						<eastBoundLongitude>17.3085</eastBoundLongitude>
+						<southBoundLatitude>46.0867</southBoundLatitude>
+						<northBoundLatitude>49.2085</northBoundLatitude>
+					</EX_GeographicBoundingBox>
+					<BoundingBox CRS="EPSG:31287" minx="110000" miny="250000" maxx="690000" maxy="590000"/>
+					<Style>
+						<Name>inspire_vs:DEFAULT</Name>
+						<Title>inspire_vs:DEFAULT</Title>
+						<LegendURL width="86" height="25">
+							<Format>image/png</Format>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://wms.geoimage.at/dop-1mfree-beta110223?version=1.3.0&service=WMS&request=GetLegendGraphic&sld_version=1.1.0&layer=Metadata-dissolve&format=image/png&STYLE=inspire_vs:DEFAULT"/>
+						</LegendURL>
+					</Style>
+					<MinScaleDenominator>100</MinScaleDenominator>
+					<MaxScaleDenominator>1e+007</MaxScaleDenominator>
+				</Layer>
+			</Layer>
+		</Layer>
+	</Capability>
+</WMS_Capabilities>
diff --git a/mapproxy/test/schemas/inspire/inspire_vs/1.0/examples/wms_geoimage.xml b/mapproxy/test/schemas/inspire/inspire_vs/1.0/examples/wms_geoimage.xml
new file mode 100644
index 0000000..5557833
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/inspire_vs/1.0/examples/wms_geoimage.xml
@@ -0,0 +1,346 @@
+<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?>
+<!--
+26-APR-2011 1.0.1 Conformity element:
+                    Restricted allowed citations to "INSPIRE interoperability of spatial data sets and services", according to the MD regulation,
+                    requirements for the conformity element (page 17 of the MD regulation).
+                  Inspire Themes:
+                    Removed leading blank for German, Bulgarian, Czech and Danish translations
+17-MAR-2011 Changed alias inspire_common and inspire_vs
+18-FEB-2011 Changed encoding for Language Elements in Network Services
+16-FEB-2011 Introduced namespace "common"
+                    Removed unnecessary namespace references
+                    Switched to short form of INSPIRE namespace alises
+-->
+<WMS_Capabilities xmlns="http://www.opengis.net/wms" xmlns:sld="http://www.opengis.net/sld" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ms="http://mapserver.gis.umn.edu/mapserver" xmlns:inspire_vs="http://inspire.ec.europa.eu/schemas/inspire_vs/1.0" xmlns:inspire_common="http://inspire.ec.europa.eu/schemas/common/1.0" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.3.0" xsi:schemaLocation="http://www.opengis.net/wms http://schemas.opengis.net/wms/1.3.0/capabilities_ [...]
+	<!--31-JAN-2011 INSPIRE schema updated to 0.19 (Removed language dependent schema implementations)-->
+	<!--SGW + MapServer-->
+	<Service>
+		<Name>WMS</Name>
+		<Title>Geoimage_1m</Title>
+		<Abstract>This service provides starting from October 2010 middle resolution orthoimagery of Austria and is intended to support open Data communities such as openstreetmap.org in creating new or enhancing free spatial data. This service is provided for the time being, there is no guarantee for long-term availability.</Abstract>
+		<KeywordList>
+			<Keyword>GEOIMAGE</Keyword>
+			<Keyword>ORTHOPHOTO</Keyword>
+			<Keyword>ORTHOFOTO</Keyword>
+			<Keyword>REMOTE SENSING</Keyword>
+			<Keyword>AUSTRIA</Keyword>
+			<Keyword>LFRZ</Keyword>
+		</KeywordList>
+		<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta101221?"/>
+		<ContactInformation>
+			<ContactPersonPrimary>
+				<ContactPerson>Wolfgang Tinkl</ContactPerson>
+				<ContactOrganization>http://www.geoimage.at</ContactOrganization>
+			</ContactPersonPrimary>
+			<ContactElectronicMailAddress>office at geoimage.at</ContactElectronicMailAddress>
+		</ContactInformation>
+		<Fees>no conditions apply</Fees>
+		<AccessConstraints>The content of this service is copyright by GEOIMAGE-AUSTRIA(R) and protected by law. This WMS can be used by anybody for private, governmental and non-profit purposes. Use for Commercial purposes is NOT allowed, however secondary products (created on basis of this service) are not subject to this non-commercial restriction as long as these secondary products are made freely available e.g. under the Creative Commons Attribution-ShareAlike 2.0 (CC-BY-SA) license. Syst [...]
+		<MaxWidth>2048</MaxWidth>
+		<MaxHeight>2048</MaxHeight>
+	</Service>
+	<Capability>
+		<Request>
+			<GetCapabilities>
+				<Format>text/xml</Format>
+				<DCPType>
+					<HTTP>
+						<Get>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta101221?"/>
+						</Get>
+						<Post>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta101221?"/>
+						</Post>
+					</HTTP>
+				</DCPType>
+			</GetCapabilities>
+			<GetMap>
+				<Format>image/png</Format>
+				<Format>image/jpg</Format>
+				<Format>image/gif</Format>
+				<Format>image/png; mode=24bit</Format>
+				<Format>image/vnd.wap.wbmp</Format>
+				<Format>image/jpeg</Format>
+				<Format>image/tiff</Format>
+				<Format>image/svg+xml</Format>
+				<DCPType>
+					<HTTP>
+						<Get>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta101221?"/>
+						</Get>
+						<Post>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta101221?"/>
+						</Post>
+					</HTTP>
+				</DCPType>
+			</GetMap>
+			<GetFeatureInfo>
+				<Format>text/plain</Format>
+				<Format>application/vnd.ogc.gml</Format>
+				<DCPType>
+					<HTTP>
+						<Get>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta101221?"/>
+						</Get>
+						<Post>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta101221?"/>
+						</Post>
+					</HTTP>
+				</DCPType>
+			</GetFeatureInfo>
+			<sld:DescribeLayer>
+				<Format>text/xml</Format>
+				<DCPType>
+					<HTTP>
+						<Get>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta101221?"/>
+						</Get>
+						<Post>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta101221?"/>
+						</Post>
+					</HTTP>
+				</DCPType>
+			</sld:DescribeLayer>
+			<sld:GetLegendGraphic>
+				<Format>image/png</Format>
+				<Format>image/jpg</Format>
+				<Format>image/gif</Format>
+				<Format>image/png; mode=24bit</Format>
+				<Format>image/vnd.wap.wbmp</Format>
+				<Format>image/jpeg</Format>
+				<DCPType>
+					<HTTP>
+						<Get>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta101221?"/>
+						</Get>
+						<Post>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta101221?"/>
+						</Post>
+					</HTTP>
+				</DCPType>
+			</sld:GetLegendGraphic>
+			<ms:GetStyles>
+				<Format>text/xml</Format>
+				<DCPType>
+					<HTTP>
+						<Get>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta101221?"/>
+						</Get>
+						<Post>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://wms.geoimage.at/dop-1mfree-beta101221?"/>
+						</Post>
+					</HTTP>
+				</DCPType>
+			</ms:GetStyles>
+		</Request>
+		<Exception>
+			<Format>XML</Format>
+			<Format>INIMAGE</Format>
+			<Format>BLANK</Format>
+		</Exception>
+		<inspire_vs:ExtendedCapabilities>
+			<inspire_common:ResourceLocator>
+				<inspire_common:URL>http://www.geoimage.at</inspire_common:URL>
+				<inspire_common:MediaType>text/html</inspire_common:MediaType>
+			</inspire_common:ResourceLocator>
+			<inspire_common:ResourceType>service</inspire_common:ResourceType>
+			<inspire_common:TemporalReference>
+				<inspire_common:DateOfCreation>2010-12-21</inspire_common:DateOfCreation>
+			</inspire_common:TemporalReference>
+			<inspire_common:Conformity>
+				<inspire_common:Specification xsi:type="inspire_common:citationInspireInteroperabilityRegulation_eng">
+					<inspire_common: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</inspire_common:Title>
+					<inspire_common:DateOfPublication>2010-12-08</inspire_common:DateOfPublication>
+					<inspire_common:URI>OJ:L:2010:323:0011:0102:EN:PDF</inspire_common:URI>
+					<inspire_common:ResourceLocator>
+						<inspire_common:URL>http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:EN:PDF</inspire_common:URL>
+						<inspire_common:MediaType>application/pdf</inspire_common:MediaType>
+					</inspire_common:ResourceLocator>
+				</inspire_common:Specification>
+				<inspire_common:Degree>notEvaluated</inspire_common:Degree>
+			</inspire_common:Conformity>
+			<inspire_common:MetadataPointOfContact>
+				<inspire_common:OrganisationName>www.geoimage.at</inspire_common:OrganisationName>
+				<inspire_common:EmailAddress>office at geoimage.at</inspire_common:EmailAddress>
+			</inspire_common:MetadataPointOfContact>
+			<inspire_common:MetadataDate>2010-12-10</inspire_common:MetadataDate>
+			<inspire_common:SpatialDataServiceType>view</inspire_common:SpatialDataServiceType>
+			<inspire_common:MandatoryKeyword>
+				<inspire_common:KeywordValue>humanGeographicViewer</inspire_common:KeywordValue>
+			</inspire_common:MandatoryKeyword>
+			<inspire_common:MandatoryKeyword>
+				<inspire_common:KeywordValue>infoMapAccessService</inspire_common:KeywordValue>
+			</inspire_common:MandatoryKeyword>
+			<inspire_common:Keyword>
+				<inspire_common:OriginatingControlledVocabulary>
+					<inspire_common:Title>GEMET INSPIRE themes</inspire_common:Title>
+					<inspire_common:DateOfPublication>2008-06-01</inspire_common:DateOfPublication>
+				</inspire_common:OriginatingControlledVocabulary>
+				<inspire_common:KeywordValue>Orthoimagery</inspire_common:KeywordValue>
+			</inspire_common:Keyword>
+			<inspire_common:SupportedLanguages>
+				<inspire_common:DefaultLanguage>
+					<inspire_common:Language>eng</inspire_common:Language>
+				</inspire_common:DefaultLanguage>
+			</inspire_common:SupportedLanguages>
+			<inspire_common:ResponseLanguage>
+				<inspire_common:Language>eng</inspire_common:Language>
+			</inspire_common:ResponseLanguage>
+			<inspire_common:MetadataUrl>
+				<inspire_common:URL>http://www.geoimage.at/test/metadata/</inspire_common:URL>
+				<inspire_common:MediaType>application/vnd.iso.19139+xml</inspire_common:MediaType>
+			</inspire_common:MetadataUrl>
+		</inspire_vs:ExtendedCapabilities>
+		<sld:UserDefinedSymbolization SupportSLD="1" UserLayer="0" UserStyle="1" RemoteWFS="0" InlineFeature="0" RemoteWCS="0"/>
+		<Layer>
+			<Name>DOP</Name>
+			<Title>Geoimage_1m</Title>
+			<Abstract>This service provides starting from October 2010 middle resolution orthoimagery of Austria and is intended to support open Data communities such as openstreetmap.org in creating new or enhancing free spatial data. This service is provided for the time being, there is no guarantee for long-term availability.</Abstract>
+			<KeywordList>
+				<Keyword>GEOIMAGE</Keyword>
+				<Keyword>ORTHOPHOTO</Keyword>
+				<Keyword>ORTHOFOTO</Keyword>
+				<Keyword>REMOTE SENSING</Keyword>
+				<Keyword>AUSTRIA</Keyword>
+				<Keyword>LFRZ</Keyword>
+			</KeywordList>
+			<CRS>EPSG:31287</CRS>
+			<CRS>EPSG:4326</CRS>
+			<CRS>EPSG:3857</CRS>
+			<CRS>EPSG:900913</CRS>
+			<EX_GeographicBoundingBox>
+				<westBoundLongitude>9.35647</westBoundLongitude>
+				<eastBoundLongitude>17.3085</eastBoundLongitude>
+				<southBoundLatitude>46.0867</southBoundLatitude>
+				<northBoundLatitude>49.2085</northBoundLatitude>
+			</EX_GeographicBoundingBox>
+			<BoundingBox CRS="EPSG:31287" minx="110000" miny="250000" maxx="690000" maxy="590000"/>
+			<Layer>
+				<Name>Orthophoto</Name>
+				<Title>Orthophoto</Title>
+				<Abstract>Orthophoto</Abstract>
+				<Layer queryable="0" opaque="0" cascaded="0">
+					<Name>Luftbild_1m</Name>
+					<Title>Luftbild_1m_LB</Title>
+					<CRS>EPSG:31287</CRS>
+					<CRS>EPSG:4326</CRS>
+					<CRS>EPSG:3857</CRS>
+					<CRS>EPSG:900913</CRS>
+					<EX_GeographicBoundingBox>
+						<westBoundLongitude>9.35647</westBoundLongitude>
+						<eastBoundLongitude>17.3085</eastBoundLongitude>
+						<southBoundLatitude>46.0867</southBoundLatitude>
+						<northBoundLatitude>49.2085</northBoundLatitude>
+					</EX_GeographicBoundingBox>
+					<BoundingBox CRS="EPSG:31287" minx="110000" miny="250000" maxx="690000" maxy="590000"/>
+					<MaxScaleDenominator>15500</MaxScaleDenominator>
+				</Layer>
+				<Layer queryable="0" opaque="0" cascaded="0">
+					<Name>Message</Name>
+					<Title>Message</Title>
+					<CRS>EPSG:31287</CRS>
+					<CRS>EPSG:4326</CRS>
+					<CRS>EPSG:3857</CRS>
+					<CRS>EPSG:900913</CRS>
+					<EX_GeographicBoundingBox>
+						<westBoundLongitude>9.35647</westBoundLongitude>
+						<eastBoundLongitude>17.3085</eastBoundLongitude>
+						<southBoundLatitude>46.0867</southBoundLatitude>
+						<northBoundLatitude>49.2085</northBoundLatitude>
+					</EX_GeographicBoundingBox>
+					<BoundingBox CRS="EPSG:31287" minx="110000" miny="250000" maxx="690000" maxy="590000"/>
+					<MaxScaleDenominator>7500</MaxScaleDenominator>
+				</Layer>
+				<Layer queryable="0" opaque="0" cascaded="0">
+					<Name>Luftbild_4m</Name>
+					<Title>Luftbild_4m_LB</Title>
+					<CRS>EPSG:31287</CRS>
+					<CRS>EPSG:4326</CRS>
+					<CRS>EPSG:3857</CRS>
+					<CRS>EPSG:900913</CRS>
+					<EX_GeographicBoundingBox>
+						<westBoundLongitude>9.35647</westBoundLongitude>
+						<eastBoundLongitude>17.3085</eastBoundLongitude>
+						<southBoundLatitude>46.0867</southBoundLatitude>
+						<northBoundLatitude>49.2085</northBoundLatitude>
+					</EX_GeographicBoundingBox>
+					<BoundingBox CRS="EPSG:31287" minx="110000" miny="250000" maxx="690000" maxy="590000"/>
+					<MinScaleDenominator>15001</MinScaleDenominator>
+					<MaxScaleDenominator>150500</MaxScaleDenominator>
+				</Layer>
+				<Layer queryable="0" opaque="0" cascaded="0">
+					<Name>Satellitenbild_30m</Name>
+					<Title>Satellitenbild_30m</Title>
+					<CRS>EPSG:31287</CRS>
+					<CRS>EPSG:4326</CRS>
+					<CRS>EPSG:3857</CRS>
+					<CRS>EPSG:900913</CRS>
+					<EX_GeographicBoundingBox>
+						<westBoundLongitude>9.35647</westBoundLongitude>
+						<eastBoundLongitude>17.3085</eastBoundLongitude>
+						<southBoundLatitude>46.0867</southBoundLatitude>
+						<northBoundLatitude>49.2085</northBoundLatitude>
+					</EX_GeographicBoundingBox>
+					<BoundingBox CRS="EPSG:31287" minx="110000" miny="250000" maxx="690000" maxy="590000"/>
+					<MinScaleDenominator>150001</MinScaleDenominator>
+					<MaxScaleDenominator>1e+007</MaxScaleDenominator>
+				</Layer>
+			</Layer>
+			<Layer>
+				<Name>Metadata</Name>
+				<Title>Metadata</Title>
+				<Abstract>Metadata</Abstract>
+				<Layer queryable="0" opaque="0" cascaded="0">
+					<Name>Metadata_Tiles</Name>
+					<Title>Metadata_Tiles</Title>
+					<CRS>EPSG:31287</CRS>
+					<CRS>EPSG:4326</CRS>
+					<CRS>EPSG:3857</CRS>
+					<CRS>EPSG:900913</CRS>
+					<EX_GeographicBoundingBox>
+						<westBoundLongitude>9.35647</westBoundLongitude>
+						<eastBoundLongitude>17.3085</eastBoundLongitude>
+						<southBoundLatitude>46.0867</southBoundLatitude>
+						<northBoundLatitude>49.2085</northBoundLatitude>
+					</EX_GeographicBoundingBox>
+					<BoundingBox CRS="EPSG:31287" minx="110000" miny="250000" maxx="690000" maxy="590000"/>
+					<Style>
+						<Name>inspire_com:DEFAULT</Name>
+						<Title>inspire_com:DEFAULT</Title>
+						<LegendURL width="102" height="25">
+							<Format>image/png</Format>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://wms.geoimage.at/dop-1mfree-beta101221?version=1.3.0&service=WMS&request=GetLegendGraphic&sld_version=1.1.0&layer=Metadata_Tiles&format=image/png&STYLE=inspire_com:DEFAULT"/>
+						</LegendURL>
+					</Style>
+					<MinScaleDenominator>100</MinScaleDenominator>
+					<MaxScaleDenominator>150500</MaxScaleDenominator>
+				</Layer>
+				<Layer queryable="0" opaque="0" cascaded="0">
+					<Name>Metadata-dissolve</Name>
+					<Title>Metadata-dissolve</Title>
+					<CRS>EPSG:31287</CRS>
+					<CRS>EPSG:4326</CRS>
+					<CRS>EPSG:3857</CRS>
+					<CRS>EPSG:900913</CRS>
+					<EX_GeographicBoundingBox>
+						<westBoundLongitude>9.35647</westBoundLongitude>
+						<eastBoundLongitude>17.3085</eastBoundLongitude>
+						<southBoundLatitude>46.0867</southBoundLatitude>
+						<northBoundLatitude>49.2085</northBoundLatitude>
+					</EX_GeographicBoundingBox>
+					<BoundingBox CRS="EPSG:31287" minx="110000" miny="250000" maxx="690000" maxy="590000"/>
+					<Style>
+						<Name>inspire_com:DEFAULT</Name>
+						<Title>inspire_com:DEFAULT</Title>
+						<LegendURL width="86" height="25">
+							<Format>image/png</Format>
+							<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://wms.geoimage.at/dop-1mfree-beta101221?version=1.3.0&service=WMS&request=GetLegendGraphic&sld_version=1.1.0&layer=Metadata-dissolve&format=image/png&STYLE=inspire_com:DEFAULT"/>
+						</LegendURL>
+					</Style>
+					<MinScaleDenominator>100</MinScaleDenominator>
+					<MaxScaleDenominator>1e+007</MaxScaleDenominator>
+				</Layer>
+			</Layer>
+		</Layer>
+	</Capability>
+</WMS_Capabilities>
diff --git a/mapproxy/test/schemas/inspire/inspire_vs/1.0/inspire_vs.xsd b/mapproxy/test/schemas/inspire/inspire_vs/1.0/inspire_vs.xsd
new file mode 100644
index 0000000..0f80c93
--- /dev/null
+++ b/mapproxy/test/schemas/inspire/inspire_vs/1.0/inspire_vs.xsd
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+26-APR-2011 1.0.1 Conformity element:
+                    Restricted allowed citations to "INSPIRE interoperability of spatial data sets and services", according to the MD regulation,
+                    requirements for the conformity element (page 17 of the MD regulation).
+                  Inspire Themes:
+                    Removed leading blank for German, Bulgarian, Czech and Danish translations
+17-MAR-2011 Changed alias inspire_com to inspire_common
+08-FEB-2011 Changed Extended Capabilities type name
+31-JAN-2011 Removed language dependent schema implementations
+20-DEC-2010 Removed unnecessary srv namespace declaration.
+					Restored inspire_vs alias
+16-DEC-2010 Switched to INSPIRE Schema
+-->
+<schema xmlns:inspire_vs="http://inspire.ec.europa.eu/schemas/inspire_vs/1.0" xmlns:inspire_common="http://inspire.ec.europa.eu/schemas/common/1.0" xmlns:wms="http://www.opengis.net/wms" xmlns="http://www.w3.org/2001/XMLSchema" xmlns:xlink="http://www.w3.org/1999/xlink" targetNamespace="http://inspire.ec.europa.eu/schemas/inspire_vs/1.0" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0.1">
+	<import namespace="http://www.opengis.net/wms" schemaLocation="../../../wms/1.3.0/capabilities_1_3_0.xsd"/>
+	<import namespace="http://inspire.ec.europa.eu/schemas/common/1.0" schemaLocation="../../common/1.0/common.xsd"/>
+	<element name="ExtendedCapabilities" type="inspire_common:ExtendedCapabilitiesType" substitutionGroup="wms:_ExtendedCapabilities"/>
+</schema>
diff --git a/mapproxy/test/schemas/kml/2.2.0/ReadMe.txt b/mapproxy/test/schemas/kml/2.2.0/ReadMe.txt
new file mode 100644
index 0000000..b1bc22e
--- /dev/null
+++ b/mapproxy/test/schemas/kml/2.2.0/ReadMe.txt
@@ -0,0 +1,14 @@
+OGC(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.
+
+-----------------------------------------------------------------------
+
diff --git a/mapproxy/test/schemas/kml/2.2.0/atom-author-link.xsd b/mapproxy/test/schemas/kml/2.2.0/atom-author-link.xsd
new file mode 100644
index 0000000..b3d77ad
--- /dev/null
+++ b/mapproxy/test/schemas/kml/2.2.0/atom-author-link.xsd
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema xmlns="http://www.w3.org/2001/XMLSchema"
+  elementFormDefault="qualified"
+  targetNamespace="http://www.w3.org/2005/Atom"
+  xmlns:atom="http://www.w3.org/2005/Atom" version="1.0.0">
+
+  <annotation>
+    <appinfo>atom-author-link.xsd 2008-01-23</appinfo>
+    <documentation>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. </documentation>
+  </annotation>
+
+  <!-- Person Construct -->
+  <complexType name="atomPersonConstruct">
+    <choice minOccurs="0" maxOccurs="unbounded">
+      <element ref="atom:name"/>
+      <element ref="atom:uri"/>
+      <element ref="atom:email"/>
+    </choice>
+  </complexType>
+
+  <element name="name" type="string"/>
+  <element name="uri" type="string"/>
+  <element name="email" type="atom:atomEmailAddress"/>
+
+  <!-- atom:author -->
+  <element name="author" type="atom:atomPersonConstruct"/>
+
+  <!-- atom:link -->
+  <element name="link">
+    <complexType>
+
+      <attribute name="href" use="required"/>
+      <attribute name="rel"/>
+      <attribute name="type" type="atom:atomMediaType"/>
+      <attribute name="hreflang" type="atom:atomLanguageTag"/>
+      <attribute name="title"/>
+      <attribute name="length"/>
+
+    </complexType>
+
+  </element>
+
+  <!-- Whatever a media type is, it contains at least one slash -->
+  <simpleType name="atomMediaType">
+    <restriction base="string">
+      <pattern value=".+/.+"/>
+    </restriction>
+  </simpleType>
+
+  <!-- As defined in RFC 3066 -->
+  <simpleType name="atomLanguageTag">
+    <restriction base="string">
+      <pattern value="[A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})*"/>
+    </restriction>
+  </simpleType>
+
+  <!-- Whatever an email address is, it contains at least one @ -->
+  <simpleType name="atomEmailAddress">
+    <restriction base="string">
+      <pattern value=".+ at .+"/>
+    </restriction>
+  </simpleType>
+
+</schema>
diff --git a/mapproxy/test/schemas/kml/2.2.0/ogckml22.xsd b/mapproxy/test/schemas/kml/2.2.0/ogckml22.xsd
new file mode 100644
index 0000000..e7f9a04
--- /dev/null
+++ b/mapproxy/test/schemas/kml/2.2.0/ogckml22.xsd
@@ -0,0 +1,1646 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema xmlns="http://www.w3.org/2001/XMLSchema"
+  xmlns:kml="http://www.opengis.net/kml/2.2"
+  xmlns:atom="http://www.w3.org/2005/Atom"
+  xmlns:xal="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"
+  targetNamespace="http://www.opengis.net/kml/2.2"
+  elementFormDefault="qualified"
+  version="2.2.0">
+  
+  <annotation>
+    <appinfo>ogckml22.xsd 2008-01-23</appinfo>
+    <documentation>XML Schema Document for OGC KML version 2.2. Copyright (c)
+      2008 Open Geospatial Consortium, Inc. All Rights Reserved.
+    </documentation>
+  </annotation>  
+
+  <!-- import atom:author and atom:link -->
+  <import namespace="http://www.w3.org/2005/Atom"
+    schemaLocation="atom-author-link.xsd"/>
+
+  <!-- import xAL:Address -->
+  <!-- import namespace="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"
+    schemaLocation="http://docs.oasis-open.org/election/external/xAL.xsd"/ -->
+  <!-- olt: we use a local copy -->
+  <import namespace="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"
+    schemaLocation="xAL.xsd"/>
+
+
+  <!-- KML field types (simple content) -->
+
+  <simpleType name="anglepos90Type">
+    <restriction base="double">
+      <minInclusive value="0.0"/>
+      <maxInclusive value="90.0"/>
+    </restriction>
+  </simpleType>
+
+  <simpleType name="angle90Type">
+    <restriction base="double">
+      <minInclusive value="-90"/>
+      <maxInclusive value="90.0"/>
+    </restriction>
+  </simpleType>
+
+  <simpleType name="anglepos180Type">
+    <restriction base="double">
+      <minInclusive value="0.0"/>
+      <maxInclusive value="180.0"/>
+    </restriction>
+  </simpleType>
+
+  <simpleType name="angle180Type">
+    <restriction base="double">
+      <minInclusive value="-180.0"/>
+      <maxInclusive value="180.0"/>
+    </restriction>
+  </simpleType>
+
+  <simpleType name="angle360Type">
+    <restriction base="double">
+      <minInclusive value="-360.0"/>
+      <maxInclusive value="360.0"/>
+    </restriction>
+  </simpleType>
+
+  <simpleType name="altitudeModeEnumType">
+    <restriction base="string">
+      <enumeration value="clampToGround"/>
+      <enumeration value="relativeToGround"/>
+      <enumeration value="absolute"/>
+    </restriction>
+  </simpleType>
+
+  <simpleType name="colorType">
+    <annotation>
+      <documentation><![CDATA[
+        
+        aabbggrr
+        
+        ffffffff: opaque white
+        ff000000: opaque black
+        
+        ]]></documentation>
+    </annotation>
+    <restriction base="hexBinary">
+      <length value="4"/>
+    </restriction>
+  </simpleType>
+
+  <simpleType name="coordinatesType">
+    <list itemType="string"/>
+  </simpleType>
+
+  <simpleType name="colorModeEnumType">
+    <restriction base="string">
+      <enumeration value="normal"/>
+      <enumeration value="random"/>
+    </restriction>
+  </simpleType>
+
+  <simpleType name="dateTimeType">
+    <union memberTypes="dateTime date gYearMonth gYear"/>
+  </simpleType>
+
+  <simpleType name="displayModeEnumType">
+    <restriction base="string">
+      <enumeration value="default"/>
+      <enumeration value="hide"/>
+    </restriction>
+  </simpleType>
+
+  <simpleType name="gridOriginEnumType">
+    <restriction base="string">
+      <enumeration value="lowerLeft"/>
+      <enumeration value="upperLeft"/>
+    </restriction>
+  </simpleType>
+  <simpleType name="itemIconStateType">
+    <list itemType="kml:itemIconStateEnumType"/>
+  </simpleType>
+
+  <simpleType name="itemIconStateEnumType">
+    <restriction base="string">
+      <enumeration value="open"/>
+      <enumeration value="closed"/>
+      <enumeration value="error"/>
+      <enumeration value="fetching0"/>
+      <enumeration value="fetching1"/>
+      <enumeration value="fetching2"/>
+    </restriction>
+  </simpleType>
+
+  <simpleType name="listItemTypeEnumType">
+    <restriction base="string">
+      <enumeration value="radioFolder"/>
+      <enumeration value="check"/>
+      <enumeration value="checkHideChildren"/>
+      <enumeration value="checkOffOnly"/>
+    </restriction>
+  </simpleType>
+
+  <simpleType name="refreshModeEnumType">
+    <restriction base="string">
+      <enumeration value="onChange"/>
+      <enumeration value="onInterval"/>
+      <enumeration value="onExpire"/>
+    </restriction>
+  </simpleType>
+
+  <simpleType name="viewRefreshModeEnumType">
+    <restriction base="string">
+      <enumeration value="never"/>
+      <enumeration value="onRequest"/>
+      <enumeration value="onStop"/>
+      <enumeration value="onRegion"/>
+    </restriction>
+  </simpleType>
+
+  <simpleType name="shapeEnumType">
+    <restriction base="string">
+      <enumeration value="rectangle"/>
+      <enumeration value="cylinder"/>
+      <enumeration value="sphere"/>
+    </restriction>
+  </simpleType>
+
+  <simpleType name="styleStateEnumType">
+    <restriction base="string">
+      <enumeration value="normal"/>
+      <enumeration value="highlight"/>
+    </restriction>
+  </simpleType>
+
+  <simpleType name="unitsEnumType">
+    <restriction base="string">
+      <enumeration value="fraction"/>
+      <enumeration value="pixels"/>
+      <enumeration value="insetPixels"/>
+    </restriction>
+  </simpleType>
+
+  <complexType name="vec2Type" abstract="false">
+    <attribute name="x" type="double" default="1.0"/>
+    <attribute name="y" type="double" default="1.0"/>
+    <attribute name="xunits" type="kml:unitsEnumType" use="optional"
+      default="fraction"/>
+    <attribute name="yunits" type="kml:unitsEnumType" use="optional"
+      default="fraction"/>
+  </complexType>
+
+  <element name="address" type="string"/>
+  <element name="altitude" type="double" default="0.0"/>
+  <element name="altitudeModeGroup" abstract="true"/>
+  <element name="altitudeMode" type="kml:altitudeModeEnumType"
+    default="clampToGround" substitutionGroup="kml:altitudeModeGroup"/>
+  <element name="begin" type="kml:dateTimeType"/>
+  <element name="bgColor" type="kml:colorType" default="ffffffff"/>
+  <element name="bottomFov" type="kml:angle90Type" default="0.0"/>
+  <element name="color" type="kml:colorType" default="ffffffff"/>
+  <element name="colorMode" type="kml:colorModeEnumType" default="normal"/>
+  <element name="cookie" type="string"/>
+  <element name="coordinates" type="kml:coordinatesType"/>
+  <element name="description" type="string"/>
+  <element name="displayName" type="string"/>
+  <element name="displayMode" type="kml:displayModeEnumType" default="default"/>
+  <element name="drawOrder" type="int" default="0"/>
+  <element name="east" type="kml:angle180Type" default="180.0"/>
+  <element name="end" type="kml:dateTimeType"/>
+  <element name="expires" type="kml:dateTimeType"/>
+  <element name="extrude" type="boolean" default="0"/>
+  <element name="fill" type="boolean" default="1"/>
+  <element name="flyToView" type="boolean" default="0"/>
+  <element name="gridOrigin" type="kml:gridOriginEnumType" default="lowerLeft"/>
+  <element name="heading" type="kml:angle360Type" default="0.0"/>
+  <element name="href" type="string">
+    <annotation>
+      <documentation>not anyURI due to $[x] substitution in
+      PhotoOverlay</documentation>
+    </annotation>
+  </element>
+  <element name="httpQuery" type="string"/>
+  <element name="hotSpot" type="kml:vec2Type"/>
+  <element name="key" type="kml:styleStateEnumType" default="normal"/>
+  <element name="latitude" type="kml:angle90Type" default="0.0"/>
+  <element name="leftFov" type="kml:angle180Type" default="0.0"/>
+  <element name="linkDescription" type="string"/>
+  <element name="linkName" type="string"/>
+  <element name="linkSnippet" type="kml:SnippetType"/>
+  <element name="listItemType" type="kml:listItemTypeEnumType" default="check"/>
+  <element name="longitude" type="kml:angle180Type" default="0.0"/>
+  <element name="maxSnippetLines" type="int" default="2"/>
+  <element name="maxSessionLength" type="double" default="-1.0"/>
+  <element name="message" type="string"/>
+  <element name="minAltitude" type="double" default="0.0"/>
+  <element name="minFadeExtent" type="double" default="0.0"/>
+  <element name="minLodPixels" type="double" default="0.0"/>
+  <element name="minRefreshPeriod" type="double" default="0.0"/>
+  <element name="maxAltitude" type="double" default="0.0"/>
+  <element name="maxFadeExtent" type="double" default="0.0"/>
+  <element name="maxLodPixels" type="double" default="-1.0"/>
+  <element name="maxHeight" type="int" default="0"/>
+  <element name="maxWidth" type="int" default="0"/>
+  <element name="name" type="string"/>
+  <element name="near" type="double" default="0.0"/>
+  <element name="north" type="kml:angle180Type" default="180.0"/>
+  <element name="open" type="boolean" default="0"/>
+  <element name="outline" type="boolean" default="1"/>
+  <element name="overlayXY" type="kml:vec2Type"/>
+  <element name="phoneNumber" type="string"/>
+  <element name="range" type="double" default="0.0"/>
+  <element name="refreshMode" type="kml:refreshModeEnumType"
+    default="onChange"/>
+  <element name="refreshInterval" type="double" default="4.0"/>
+  <element name="refreshVisibility" type="boolean" default="0"/>
+  <element name="rightFov" type="kml:angle180Type" default="0.0"/>
+  <element name="roll" type="kml:angle180Type" default="0.0"/>
+  <element name="rotation" type="kml:angle180Type" default="0.0"/>
+  <element name="rotationXY" type="kml:vec2Type"/>
+  <element name="scale" type="double" default="1.0"/>
+  <element name="screenXY" type="kml:vec2Type"/>
+  <element name="shape" type="kml:shapeEnumType" default="rectangle"/>
+  <element name="size" type="kml:vec2Type"/>
+  <element name="south" type="kml:angle180Type" default="-180.0"/>
+  <element name="sourceHref" type="anyURI"/>
+  <element name="snippet" type="string"/>
+  <element name="state" type="kml:itemIconStateType"/>
+  <element name="styleUrl" type="anyURI"/>
+  <element name="targetHref" type="anyURI"/>
+  <element name="tessellate" type="boolean" default="0"/>
+  <element name="text" type="string"/>
+  <element name="textColor" type="kml:colorType" default="ff000000"/>
+  <element name="tileSize" type="int" default="256"/>
+  <element name="tilt" type="kml:anglepos180Type" default="0.0"/>
+  <element name="topFov" type="kml:angle90Type" default="0.0"/>
+  <element name="value" type="string"/>
+  <element name="viewBoundScale" type="double" default="1.0"/>
+  <element name="viewFormat" type="string"/>
+  <element name="viewRefreshMode" type="kml:viewRefreshModeEnumType"
+    default="never"/>
+  <element name="viewRefreshTime" type="double" default="4.0"/>
+  <element name="visibility" type="boolean" default="1"/>
+  <element name="west" type="kml:angle180Type" default="-180.0"/>
+  <element name="when" type="kml:dateTimeType"/>
+  <element name="width" type="double" default="1.0"/>
+  <element name="x" type="double" default="1.0"/>
+  <element name="y" type="double" default="1.0"/>
+  <element name="z" type="double" default="1.0"/>
+
+  <element name="AbstractObjectGroup" type="kml:AbstractObjectType"
+    abstract="true"/>
+  <complexType name="AbstractObjectType" abstract="true">
+    <sequence>
+      <element ref="kml:ObjectSimpleExtensionGroup" minOccurs="0"
+        maxOccurs="unbounded"/>
+    </sequence>
+    <attributeGroup ref="kml:idAttributes"/>
+  </complexType>
+  <element name="ObjectSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+
+  <attributeGroup name="idAttributes">
+    <attribute name="id" type="ID" use="optional"/>
+    <attribute name="targetId" type="NCName" use="optional"/>
+  </attributeGroup>
+
+  <element name="AbstractFeatureGroup" type="kml:AbstractFeatureType"
+    abstract="true" substitutionGroup="kml:AbstractObjectGroup"/>
+  <complexType name="AbstractFeatureType" abstract="true">
+    <complexContent>
+      <extension base="kml:AbstractObjectType">
+        <sequence>
+          <element ref="kml:name" minOccurs="0"/>
+          <element ref="kml:visibility" minOccurs="0"/>
+          <element ref="kml:open" minOccurs="0"/>
+          <element ref="atom:author" minOccurs="0"/>
+          <element ref="atom:link" minOccurs="0"/>
+          <element ref="kml:address" minOccurs="0"/>
+          <element ref="xal:AddressDetails" minOccurs="0"/>
+          <element ref="kml:phoneNumber" minOccurs="0"/>
+          <choice>
+            <annotation>
+              <documentation>Snippet deprecated in 2.2</documentation>
+            </annotation>
+            <element ref="kml:Snippet" minOccurs="0"/>
+            <element ref="kml:snippet" minOccurs="0"/>
+          </choice>
+          <element ref="kml:description" minOccurs="0"/>
+          <element ref="kml:AbstractViewGroup" minOccurs="0"/>
+          <element ref="kml:AbstractTimePrimitiveGroup" minOccurs="0"/>
+          <element ref="kml:styleUrl" minOccurs="0"/>
+          <element ref="kml:AbstractStyleSelectorGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:Region" minOccurs="0"/>
+          <choice>
+            <annotation>
+              <documentation>Metadata deprecated in 2.2</documentation>
+            </annotation>
+            <element ref="kml:Metadata" minOccurs="0"/>
+            <element ref="kml:ExtendedData" minOccurs="0"/>
+          </choice>
+          <element ref="kml:AbstractFeatureSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:AbstractFeatureObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="AbstractFeatureObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+  <element name="AbstractFeatureSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+
+  <element name="Snippet" type="kml:SnippetType"/>
+  <complexType name="SnippetType" final="#all">
+    <simpleContent>
+      <extension base="string">
+        <attribute name="maxLines" type="int" use="optional" default="2"/>
+      </extension>
+    </simpleContent>
+  </complexType>
+
+  <element name="AbstractViewGroup" type="kml:AbstractViewType" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+  <complexType name="AbstractViewType" abstract="true">
+    <complexContent>
+      <extension base="kml:AbstractObjectType">
+        <sequence>
+          <element ref="kml:AbstractViewSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:AbstractViewObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="AbstractViewSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="AbstractViewObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="LookAt" type="kml:LookAtType"
+    substitutionGroup="kml:AbstractViewGroup"/>
+  <complexType name="LookAtType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractViewType">
+        <sequence>
+          <element ref="kml:longitude" minOccurs="0"/>
+          <element ref="kml:latitude" minOccurs="0"/>
+          <element ref="kml:altitude" minOccurs="0"/>
+          <element ref="kml:heading" minOccurs="0"/>
+          <element ref="kml:tilt" minOccurs="0"/>
+          <element ref="kml:range" minOccurs="0"/>
+          <element ref="kml:altitudeModeGroup" minOccurs="0"/>
+          <element ref="kml:LookAtSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:LookAtObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="LookAtSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="LookAtObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="Camera" type="kml:CameraType"
+    substitutionGroup="kml:AbstractViewGroup"/>
+  <complexType name="CameraType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractViewType">
+        <sequence>
+          <element ref="kml:longitude" minOccurs="0"/>
+          <element ref="kml:latitude" minOccurs="0"/>
+          <element ref="kml:altitude" minOccurs="0"/>
+          <element ref="kml:heading" minOccurs="0"/>
+          <element ref="kml:tilt" minOccurs="0"/>
+          <element ref="kml:roll" minOccurs="0"/>
+          <element ref="kml:altitudeModeGroup" minOccurs="0"/>
+          <element ref="kml:CameraSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:CameraObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="CameraSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="CameraObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="Metadata" type="kml:MetadataType">
+    <annotation>
+      <documentation>Metadata deprecated in 2.2</documentation>
+    </annotation>
+  </element>
+
+  <complexType name="MetadataType" final="#all">
+    <annotation>
+      <documentation>MetadataType deprecated in 2.2</documentation>
+    </annotation>
+    <sequence>
+      <any namespace="##any" processContents="lax" minOccurs="0"
+        maxOccurs="unbounded"/>
+    </sequence>
+  </complexType>
+
+  <element name="ExtendedData" type="kml:ExtendedDataType"/>
+  <complexType name="ExtendedDataType" final="#all">
+    <sequence>
+      <element ref="kml:Data" minOccurs="0" maxOccurs="unbounded"/>
+      <element ref="kml:SchemaData" minOccurs="0" maxOccurs="unbounded"/>
+      <any namespace="##other" processContents="lax" minOccurs="0"
+        maxOccurs="unbounded"/>
+    </sequence>
+  </complexType>
+
+  <element name="SchemaData" type="kml:SchemaDataType"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+  <complexType name="SchemaDataType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractObjectType">
+        <sequence>
+          <element ref="kml:SimpleData" minOccurs="0" maxOccurs="unbounded"/>
+          <element ref="kml:SchemaDataExtension" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+        <attribute name="schemaUrl" type="anyURI"/>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="SchemaDataExtension" abstract="true"/>
+
+  <element name="SimpleData" type="kml:SimpleDataType"/>
+  <complexType name="SimpleDataType" final="#all">
+    <simpleContent>
+      <extension base="string">
+        <attribute name="name" type="string" use="required"/>
+      </extension>
+    </simpleContent>
+  </complexType>
+
+  <element name="Data" type="kml:DataType"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+  <complexType name="DataType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractObjectType">
+        <sequence>
+          <element ref="kml:displayName" minOccurs="0"/>
+          <element ref="kml:value"/>
+          <element ref="kml:DataExtension" minOccurs="0" maxOccurs="unbounded"/>
+        </sequence>
+        <attribute name="name" type="string"/>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="DataExtension" abstract="true"/>
+
+  <element name="AbstractContainerGroup" type="kml:AbstractContainerType"
+    abstract="true" substitutionGroup="kml:AbstractFeatureGroup"/>
+  <complexType name="AbstractContainerType" abstract="true">
+    <complexContent>
+      <extension base="kml:AbstractFeatureType">
+        <sequence>
+          <element ref="kml:AbstractContainerSimpleExtensionGroup"
+            minOccurs="0" maxOccurs="unbounded"/>
+          <element ref="kml:AbstractContainerObjectExtensionGroup"
+            minOccurs="0" maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="AbstractContainerSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="AbstractContainerObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="AbstractGeometryGroup" type="kml:AbstractGeometryType"
+    abstract="true" substitutionGroup="kml:AbstractObjectGroup"/>
+  <complexType name="AbstractGeometryType" abstract="true">
+    <complexContent>
+      <extension base="kml:AbstractObjectType">
+        <sequence>
+          <element ref="kml:AbstractGeometrySimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:AbstractGeometryObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="AbstractGeometrySimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="AbstractGeometryObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="AbstractOverlayGroup" type="kml:AbstractOverlayType"
+    abstract="true" substitutionGroup="kml:AbstractFeatureGroup"/>
+  <complexType name="AbstractOverlayType" abstract="true">
+    <complexContent>
+      <extension base="kml:AbstractFeatureType">
+        <sequence>
+          <element ref="kml:color" minOccurs="0"/>
+          <element ref="kml:drawOrder" minOccurs="0"/>
+          <element ref="kml:Icon" minOccurs="0"/>
+          <element ref="kml:AbstractOverlaySimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:AbstractOverlayObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="AbstractOverlaySimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="AbstractOverlayObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="AbstractStyleSelectorGroup"
+    type="kml:AbstractStyleSelectorType" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+  <complexType name="AbstractStyleSelectorType" abstract="true">
+    <complexContent>
+      <extension base="kml:AbstractObjectType">
+        <sequence>
+          <element ref="kml:AbstractStyleSelectorSimpleExtensionGroup"
+            minOccurs="0" maxOccurs="unbounded"/>
+          <element ref="kml:AbstractStyleSelectorObjectExtensionGroup"
+            minOccurs="0" maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="AbstractStyleSelectorSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="AbstractStyleSelectorObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="AbstractTimePrimitiveGroup"
+    type="kml:AbstractTimePrimitiveType" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+  <complexType name="AbstractTimePrimitiveType" abstract="true">
+    <complexContent>
+      <extension base="kml:AbstractObjectType">
+        <sequence>
+          <element ref="kml:AbstractTimePrimitiveSimpleExtensionGroup"
+            minOccurs="0" maxOccurs="unbounded"/>
+          <element ref="kml:AbstractTimePrimitiveObjectExtensionGroup"
+            minOccurs="0" maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="AbstractTimePrimitiveSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="AbstractTimePrimitiveObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="kml" type="kml:KmlType">
+    <annotation>
+      <documentation><![CDATA[
+
+      <kml> is the root element.
+
+      ]]></documentation>
+    </annotation>
+  </element>
+  <complexType name="KmlType" final="#all">
+    <sequence>
+      <element ref="kml:NetworkLinkControl" minOccurs="0"/>
+      <element ref="kml:AbstractFeatureGroup" minOccurs="0"/>
+      <element ref="kml:KmlSimpleExtensionGroup" minOccurs="0"
+        maxOccurs="unbounded"/>
+      <element ref="kml:KmlObjectExtensionGroup" minOccurs="0"
+        maxOccurs="unbounded"/>
+    </sequence>
+    <attribute name="hint" type="string"/>
+  </complexType>
+  <element name="KmlSimpleExtensionGroup" abstract="true" type="anySimpleType"/>
+  <element name="KmlObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="NetworkLinkControl" type="kml:NetworkLinkControlType"/>
+  <complexType name="NetworkLinkControlType" final="#all">
+    <sequence>
+      <element ref="kml:minRefreshPeriod" minOccurs="0"/>
+      <element ref="kml:maxSessionLength" minOccurs="0"/>
+      <element ref="kml:cookie" minOccurs="0"/>
+      <element ref="kml:message" minOccurs="0"/>
+      <element ref="kml:linkName" minOccurs="0"/>
+      <element ref="kml:linkDescription" minOccurs="0"/>
+      <element ref="kml:linkSnippet" minOccurs="0"/>
+      <element ref="kml:expires" minOccurs="0"/>
+      <element ref="kml:Update" minOccurs="0"/>
+      <element ref="kml:AbstractViewGroup" minOccurs="0"/>
+      <element ref="kml:NetworkLinkControlSimpleExtensionGroup" minOccurs="0"
+        maxOccurs="unbounded"/>
+      <element ref="kml:NetworkLinkControlObjectExtensionGroup" minOccurs="0"
+        maxOccurs="unbounded"/>
+    </sequence>
+  </complexType>
+  <element name="NetworkLinkControlSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="NetworkLinkControlObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="Document" type="kml:DocumentType"
+    substitutionGroup="kml:AbstractContainerGroup"/>
+  <complexType name="DocumentType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractContainerType">
+        <sequence>
+          <element ref="kml:Schema" minOccurs="0" maxOccurs="unbounded"/>
+          <element ref="kml:AbstractFeatureGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:DocumentSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:DocumentObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="DocumentSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="DocumentObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="Schema" type="kml:SchemaType"/>
+  <complexType name="SchemaType" final="#all">
+    <sequence>
+      <element ref="kml:SimpleField" minOccurs="0" maxOccurs="unbounded"/>
+      <element ref="kml:SchemaExtension" minOccurs="0" maxOccurs="unbounded"/>
+    </sequence>
+    <attribute name="name" type="string"/>
+    <attribute name="id" type="ID"/>
+  </complexType>
+  <element name="SchemaExtension" abstract="true"/>
+
+  <element name="SimpleField" type="kml:SimpleFieldType"/>
+  <complexType name="SimpleFieldType" final="#all">
+    <sequence>
+      <element ref="kml:displayName" minOccurs="0"/>
+      <element ref="kml:SimpleFieldExtension" minOccurs="0"
+        maxOccurs="unbounded"/>
+    </sequence>
+    <attribute name="type" type="string"/>
+    <attribute name="name" type="string"/>
+  </complexType>
+  <element name="SimpleFieldExtension" abstract="true"/>
+
+  <element name="Folder" type="kml:FolderType"
+    substitutionGroup="kml:AbstractContainerGroup"/>
+  <complexType name="FolderType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractContainerType">
+        <sequence>
+          <element ref="kml:AbstractFeatureGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:FolderSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:FolderObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="FolderSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="FolderObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="Placemark" type="kml:PlacemarkType"
+    substitutionGroup="kml:AbstractFeatureGroup"/>
+  <complexType name="PlacemarkType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractFeatureType">
+        <sequence>
+          <element ref="kml:AbstractGeometryGroup" minOccurs="0"/>
+          <element ref="kml:PlacemarkSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:PlacemarkObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="PlacemarkSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="PlacemarkObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="NetworkLink" type="kml:NetworkLinkType"
+    substitutionGroup="kml:AbstractFeatureGroup"/>
+  <complexType name="NetworkLinkType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractFeatureType">
+        <sequence>
+          <element ref="kml:refreshVisibility" minOccurs="0"/>
+          <element ref="kml:flyToView" minOccurs="0"/>
+          <choice>
+            <annotation>
+              <documentation>Url deprecated in 2.2</documentation>
+            </annotation>
+            <element ref="kml:Url" minOccurs="0"/>
+            <element ref="kml:Link" minOccurs="0"/>
+          </choice>
+          <element ref="kml:NetworkLinkSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:NetworkLinkObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="NetworkLinkSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="NetworkLinkObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="Region" type="kml:RegionType"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+  <complexType name="RegionType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractObjectType">
+        <sequence>
+          <element ref="kml:LatLonAltBox" minOccurs="0"/>
+          <element ref="kml:Lod" minOccurs="0"/>
+          <element ref="kml:RegionSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:RegionObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="RegionSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="RegionObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="LatLonAltBox" type="kml:LatLonAltBoxType"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+  <complexType name="LatLonAltBoxType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractLatLonBoxType">
+        <sequence>
+          <element ref="kml:minAltitude" minOccurs="0"/>
+          <element ref="kml:maxAltitude" minOccurs="0"/>
+          <element ref="kml:altitudeModeGroup" minOccurs="0"/>
+          <element ref="kml:LatLonAltBoxSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:LatLonAltBoxObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="LatLonAltBoxSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="LatLonAltBoxObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="Lod" type="kml:LodType"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+  <complexType name="LodType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractObjectType">
+        <sequence>
+          <element ref="kml:minLodPixels" minOccurs="0"/>
+          <element ref="kml:maxLodPixels" minOccurs="0"/>
+          <element ref="kml:minFadeExtent" minOccurs="0"/>
+          <element ref="kml:maxFadeExtent" minOccurs="0"/>
+          <element ref="kml:LodSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:LodObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="LodSimpleExtensionGroup" abstract="true" type="anySimpleType"/>
+  <element name="LodObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="Icon" type="kml:LinkType"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+  <element name="Link" type="kml:LinkType"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+  <element name="Url" type="kml:LinkType"
+    substitutionGroup="kml:AbstractObjectGroup">
+    <annotation>
+      <documentation>Url deprecated in 2.2</documentation>
+    </annotation>
+  </element>
+  <complexType name="LinkType" final="#all">
+    <complexContent>
+      <extension base="kml:BasicLinkType">
+        <sequence>
+          <element ref="kml:refreshMode" minOccurs="0"/>
+          <element ref="kml:refreshInterval" minOccurs="0"/>
+          <element ref="kml:viewRefreshMode" minOccurs="0"/>
+          <element ref="kml:viewRefreshTime" minOccurs="0"/>
+          <element ref="kml:viewBoundScale" minOccurs="0"/>
+          <element ref="kml:viewFormat" minOccurs="0"/>
+          <element ref="kml:httpQuery" minOccurs="0"/>
+          <element ref="kml:LinkSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:LinkObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="LinkSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="LinkObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="MultiGeometry" type="kml:MultiGeometryType"
+    substitutionGroup="kml:AbstractGeometryGroup"/>
+  <complexType name="MultiGeometryType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractGeometryType">
+        <sequence>
+          <element ref="kml:AbstractGeometryGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:MultiGeometrySimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:MultiGeometryObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="MultiGeometrySimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="MultiGeometryObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="Point" type="kml:PointType"
+    substitutionGroup="kml:AbstractGeometryGroup"/>
+  <complexType name="PointType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractGeometryType">
+        <sequence>
+          <element ref="kml:extrude" minOccurs="0"/>
+          <element ref="kml:altitudeModeGroup" minOccurs="0"/>
+          <element ref="kml:coordinates" minOccurs="0"/>
+          <element ref="kml:PointSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:PointObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="PointSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="PointObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="LineString" type="kml:LineStringType"
+    substitutionGroup="kml:AbstractGeometryGroup"/>
+  <complexType name="LineStringType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractGeometryType">
+        <sequence>
+          <element ref="kml:extrude" minOccurs="0"/>
+          <element ref="kml:tessellate" minOccurs="0"/>
+          <element ref="kml:altitudeModeGroup" minOccurs="0"/>
+          <element ref="kml:coordinates" minOccurs="0"/>
+          <element ref="kml:LineStringSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:LineStringObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="LineStringSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="LineStringObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="LinearRing" type="kml:LinearRingType"
+    substitutionGroup="kml:AbstractGeometryGroup"/>
+  <complexType name="LinearRingType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractGeometryType">
+        <sequence>
+          <element ref="kml:extrude" minOccurs="0"/>
+          <element ref="kml:tessellate" minOccurs="0"/>
+          <element ref="kml:altitudeModeGroup" minOccurs="0"/>
+          <element ref="kml:coordinates" minOccurs="0"/>
+          <element ref="kml:LinearRingSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:LinearRingObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="LinearRingSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="LinearRingObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="Polygon" type="kml:PolygonType"
+    substitutionGroup="kml:AbstractGeometryGroup"/>
+  <complexType name="PolygonType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractGeometryType">
+        <sequence>
+          <element ref="kml:extrude" minOccurs="0"/>
+          <element ref="kml:tessellate" minOccurs="0"/>
+          <element ref="kml:altitudeModeGroup" minOccurs="0"/>
+          <element ref="kml:outerBoundaryIs" minOccurs="0"/>
+          <element ref="kml:innerBoundaryIs" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:PolygonSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:PolygonObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="PolygonSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="PolygonObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="outerBoundaryIs" type="kml:BoundaryType"/>
+  <element name="innerBoundaryIs" type="kml:BoundaryType"/>
+  <complexType name="BoundaryType" final="#all">
+    <sequence>
+      <element ref="kml:LinearRing" minOccurs="0"/>
+      <element ref="kml:BoundarySimpleExtensionGroup" minOccurs="0"
+        maxOccurs="unbounded"/>
+      <element ref="kml:BoundaryObjectExtensionGroup" minOccurs="0"
+        maxOccurs="unbounded"/>
+    </sequence>
+  </complexType>
+  <element name="BoundarySimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="BoundaryObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="Model" type="kml:ModelType"
+    substitutionGroup="kml:AbstractGeometryGroup"/>
+  <complexType name="ModelType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractGeometryType">
+        <sequence>
+          <element ref="kml:altitudeModeGroup" minOccurs="0"/>
+          <element ref="kml:Location" minOccurs="0"/>
+          <element ref="kml:Orientation" minOccurs="0"/>
+          <element ref="kml:Scale" minOccurs="0"/>
+          <element ref="kml:Link" minOccurs="0"/>
+          <element ref="kml:ResourceMap" minOccurs="0"/>
+          <element ref="kml:ModelSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:ModelObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="ModelSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="ModelObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="Location" type="kml:LocationType"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+  <complexType name="LocationType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractObjectType">
+        <sequence>
+          <element ref="kml:longitude" minOccurs="0"/>
+          <element ref="kml:latitude" minOccurs="0"/>
+          <element ref="kml:altitude" minOccurs="0"/>
+          <element ref="kml:LocationSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:LocationObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="LocationSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="LocationObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="Orientation" type="kml:OrientationType"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+  <complexType name="OrientationType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractObjectType">
+        <sequence>
+          <element ref="kml:heading" minOccurs="0"/>
+          <element ref="kml:tilt" minOccurs="0"/>
+          <element ref="kml:roll" minOccurs="0"/>
+          <element ref="kml:OrientationSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:OrientationObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="OrientationSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="OrientationObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="Scale" type="kml:ScaleType"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+  <complexType name="ScaleType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractObjectType">
+        <sequence>
+          <element ref="kml:x" minOccurs="0"/>
+          <element ref="kml:y" minOccurs="0"/>
+          <element ref="kml:z" minOccurs="0"/>
+          <element ref="kml:ScaleSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:ScaleObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="ScaleSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="ScaleObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="ResourceMap" type="kml:ResourceMapType"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+  <complexType name="ResourceMapType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractObjectType">
+        <sequence>
+          <element ref="kml:Alias" minOccurs="0" maxOccurs="unbounded"/>
+          <element ref="kml:ResourceMapSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:ResourceMapObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="ResourceMapSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="ResourceMapObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="Alias" type="kml:AliasType"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+  <complexType name="AliasType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractObjectType">
+        <sequence>
+          <element ref="kml:targetHref" minOccurs="0"/>
+          <element ref="kml:sourceHref" minOccurs="0"/>
+          <element ref="kml:AliasSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:AliasObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="AliasSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="AliasObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="GroundOverlay" type="kml:GroundOverlayType"
+    substitutionGroup="kml:AbstractOverlayGroup"/>
+  <complexType name="GroundOverlayType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractOverlayType">
+        <sequence>
+          <element ref="kml:altitude" minOccurs="0"/>
+          <element ref="kml:altitudeModeGroup" minOccurs="0"/>
+          <element ref="kml:LatLonBox" minOccurs="0"/>
+          <element ref="kml:GroundOverlaySimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:GroundOverlayObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="GroundOverlaySimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="GroundOverlayObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <complexType name="AbstractLatLonBoxType" abstract="true">
+    <complexContent>
+      <extension base="kml:AbstractObjectType">
+        <sequence>
+          <element ref="kml:north" minOccurs="0"/>
+          <element ref="kml:south" minOccurs="0"/>
+          <element ref="kml:east" minOccurs="0"/>
+          <element ref="kml:west" minOccurs="0"/>
+          <element ref="kml:AbstractLatLonBoxSimpleExtensionGroup"
+            minOccurs="0" maxOccurs="unbounded"/>
+          <element ref="kml:AbstractLatLonBoxObjectExtensionGroup"
+            minOccurs="0" maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="AbstractLatLonBoxSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="AbstractLatLonBoxObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="LatLonBox" type="kml:LatLonBoxType"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+  <complexType name="LatLonBoxType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractLatLonBoxType">
+        <sequence>
+          <element ref="kml:rotation" minOccurs="0"/>
+          <element ref="kml:LatLonBoxSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:LatLonBoxObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="LatLonBoxSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="LatLonBoxObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="ScreenOverlay" type="kml:ScreenOverlayType"
+    substitutionGroup="kml:AbstractOverlayGroup"/>
+  <complexType name="ScreenOverlayType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractOverlayType">
+        <sequence>
+          <element ref="kml:overlayXY" minOccurs="0"/>
+          <element ref="kml:screenXY" minOccurs="0"/>
+          <element ref="kml:rotationXY" minOccurs="0"/>
+          <element ref="kml:size" minOccurs="0"/>
+          <element ref="kml:rotation" minOccurs="0"/>
+          <element ref="kml:ScreenOverlaySimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:ScreenOverlayObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="ScreenOverlaySimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="ScreenOverlayObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="PhotoOverlay" type="kml:PhotoOverlayType"
+    substitutionGroup="kml:AbstractOverlayGroup"/>
+  <complexType name="PhotoOverlayType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractOverlayType">
+        <sequence>
+          <element ref="kml:rotation" minOccurs="0"/>
+          <element ref="kml:ViewVolume" minOccurs="0"/>
+          <element ref="kml:ImagePyramid" minOccurs="0"/>
+          <element ref="kml:Point" minOccurs="0"/>
+          <element ref="kml:shape" minOccurs="0"/>
+          <element ref="kml:PhotoOverlaySimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:PhotoOverlayObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="PhotoOverlaySimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="PhotoOverlayObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="ViewVolume" type="kml:ViewVolumeType"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+  <complexType name="ViewVolumeType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractObjectType">
+        <sequence>
+          <element ref="kml:leftFov" minOccurs="0"/>
+          <element ref="kml:rightFov" minOccurs="0"/>
+          <element ref="kml:bottomFov" minOccurs="0"/>
+          <element ref="kml:topFov" minOccurs="0"/>
+          <element ref="kml:near" minOccurs="0"/>
+          <element ref="kml:ViewVolumeSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:ViewVolumeObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="ViewVolumeSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="ViewVolumeObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="ImagePyramid" type="kml:ImagePyramidType"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+  <complexType name="ImagePyramidType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractObjectType">
+        <sequence>
+          <element ref="kml:tileSize" minOccurs="0"/>
+          <element ref="kml:maxWidth" minOccurs="0"/>
+          <element ref="kml:maxHeight" minOccurs="0"/>
+          <element ref="kml:gridOrigin" minOccurs="0"/>
+          <element ref="kml:ImagePyramidSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:ImagePyramidObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="ImagePyramidSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="ImagePyramidObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="Style" type="kml:StyleType"
+    substitutionGroup="kml:AbstractStyleSelectorGroup"/>
+  <complexType name="StyleType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractStyleSelectorType">
+        <sequence>
+          <element ref="kml:IconStyle" minOccurs="0"/>
+          <element ref="kml:LabelStyle" minOccurs="0"/>
+          <element ref="kml:LineStyle" minOccurs="0"/>
+          <element ref="kml:PolyStyle" minOccurs="0"/>
+          <element ref="kml:BalloonStyle" minOccurs="0"/>
+          <element ref="kml:ListStyle" minOccurs="0"/>
+          <element ref="kml:StyleSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:StyleObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="StyleSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="StyleObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="StyleMap" type="kml:StyleMapType"
+    substitutionGroup="kml:AbstractStyleSelectorGroup"/>
+  <complexType name="StyleMapType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractStyleSelectorType">
+        <sequence>
+          <element ref="kml:Pair" minOccurs="0" maxOccurs="unbounded"/>
+          <element ref="kml:StyleMapSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:StyleMapObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="StyleMapSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="StyleMapObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="Pair" type="kml:PairType"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+  <complexType name="PairType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractObjectType">
+        <sequence>
+          <element ref="kml:key" minOccurs="0"/>
+          <element ref="kml:styleUrl" minOccurs="0"/>
+          <element ref="kml:AbstractStyleSelectorGroup" minOccurs="0"/>
+          <element ref="kml:PairSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:PairObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="PairSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="PairObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="AbstractSubStyleGroup" type="kml:AbstractSubStyleType"
+    abstract="true" substitutionGroup="kml:AbstractObjectGroup"/>
+  <complexType name="AbstractSubStyleType" abstract="true">
+    <complexContent>
+      <extension base="kml:AbstractObjectType">
+        <sequence>
+          <element ref="kml:AbstractSubStyleSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:AbstractSubStyleObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="AbstractSubStyleSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="AbstractSubStyleObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="AbstractColorStyleGroup" type="kml:AbstractColorStyleType"
+    abstract="true" substitutionGroup="kml:AbstractSubStyleGroup"/>
+  <complexType name="AbstractColorStyleType" abstract="true">
+    <complexContent>
+      <extension base="kml:AbstractSubStyleType">
+        <sequence>
+          <element ref="kml:color" minOccurs="0"/>
+          <element ref="kml:colorMode" minOccurs="0"/>
+          <element ref="kml:AbstractColorStyleSimpleExtensionGroup"
+            minOccurs="0" maxOccurs="unbounded"/>
+          <element ref="kml:AbstractColorStyleObjectExtensionGroup"
+            minOccurs="0" maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="AbstractColorStyleObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+  <element name="AbstractColorStyleSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+
+  <element name="IconStyle" type="kml:IconStyleType"
+    substitutionGroup="kml:AbstractColorStyleGroup"/>
+  <complexType name="IconStyleType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractColorStyleType">
+        <sequence>
+          <element ref="kml:scale" minOccurs="0"/>
+          <element ref="kml:heading" minOccurs="0"/>
+          <element name="Icon" type="kml:BasicLinkType" minOccurs="0"/>
+          <element ref="kml:hotSpot" minOccurs="0"/>
+          <element ref="kml:IconStyleSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:IconStyleObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="IconStyleSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="IconStyleObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <complexType name="BasicLinkType">
+    <complexContent>
+      <extension base="kml:AbstractObjectType">
+        <sequence>
+          <element ref="kml:href" minOccurs="0"/>
+          <element ref="kml:BasicLinkSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:BasicLinkObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="BasicLinkSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="BasicLinkObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="LabelStyle" type="kml:LabelStyleType"
+    substitutionGroup="kml:AbstractColorStyleGroup"/>
+  <complexType name="LabelStyleType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractColorStyleType">
+        <sequence>
+          <element ref="kml:scale" minOccurs="0"/>
+          <element ref="kml:LabelStyleSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:LabelStyleObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="LabelStyleSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="LabelStyleObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="LineStyle" type="kml:LineStyleType"
+    substitutionGroup="kml:AbstractColorStyleGroup"/>
+  <complexType name="LineStyleType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractColorStyleType">
+        <sequence>
+          <element ref="kml:width" minOccurs="0"/>
+          <element ref="kml:LineStyleSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:LineStyleObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="LineStyleSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="LineStyleObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="PolyStyle" type="kml:PolyStyleType"
+    substitutionGroup="kml:AbstractColorStyleGroup"/>
+  <complexType name="PolyStyleType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractColorStyleType">
+        <sequence>
+          <element ref="kml:fill" minOccurs="0"/>
+          <element ref="kml:outline" minOccurs="0"/>
+          <element ref="kml:PolyStyleSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:PolyStyleObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="PolyStyleSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="PolyStyleObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="BalloonStyle" type="kml:BalloonStyleType"
+    substitutionGroup="kml:AbstractSubStyleGroup"/>
+  <complexType name="BalloonStyleType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractSubStyleType">
+        <sequence>
+          <choice>
+            <annotation>
+              <documentation>color deprecated in 2.1</documentation>
+            </annotation>
+            <element ref="kml:color" minOccurs="0"/>
+            <element ref="kml:bgColor" minOccurs="0"/>
+          </choice>
+          <element ref="kml:textColor" minOccurs="0"/>
+          <element ref="kml:text" minOccurs="0"/>
+          <element ref="kml:displayMode" minOccurs="0"/>
+          <element ref="kml:BalloonStyleSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:BalloonStyleObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="BalloonStyleSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="BalloonStyleObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="ListStyle" type="kml:ListStyleType"
+    substitutionGroup="kml:AbstractSubStyleGroup"/>
+  <complexType name="ListStyleType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractSubStyleType">
+        <sequence>
+          <element ref="kml:listItemType" minOccurs="0"/>
+          <element ref="kml:bgColor" minOccurs="0"/>
+          <element ref="kml:ItemIcon" minOccurs="0" maxOccurs="unbounded"/>
+          <element ref="kml:maxSnippetLines" minOccurs="0"/>
+          <element ref="kml:ListStyleSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:ListStyleObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="ListStyleSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="ListStyleObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="ItemIcon" type="kml:ItemIconType"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+  <complexType name="ItemIconType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractObjectType">
+        <sequence>
+          <element ref="kml:state" minOccurs="0"/>
+          <element ref="kml:href" minOccurs="0"/>
+          <element ref="kml:ItemIconSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:ItemIconObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="ItemIconSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="ItemIconObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="TimeStamp" type="kml:TimeStampType"
+    substitutionGroup="kml:AbstractTimePrimitiveGroup"/>
+  <complexType name="TimeStampType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractTimePrimitiveType">
+        <sequence>
+          <element ref="kml:when" minOccurs="0"/>
+          <element ref="kml:TimeStampSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:TimeStampObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="TimeStampSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="TimeStampObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="TimeSpan" type="kml:TimeSpanType"
+    substitutionGroup="kml:AbstractTimePrimitiveGroup"/>
+  <complexType name="TimeSpanType" final="#all">
+    <complexContent>
+      <extension base="kml:AbstractTimePrimitiveType">
+        <sequence>
+          <element ref="kml:begin" minOccurs="0"/>
+          <element ref="kml:end" minOccurs="0"/>
+          <element ref="kml:TimeSpanSimpleExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+          <element ref="kml:TimeSpanObjectExtensionGroup" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="TimeSpanSimpleExtensionGroup" abstract="true"
+    type="anySimpleType"/>
+  <element name="TimeSpanObjectExtensionGroup" abstract="true"
+    substitutionGroup="kml:AbstractObjectGroup"/>
+
+  <element name="Update" type="kml:UpdateType"/>
+  <complexType name="UpdateType" final="#all">
+    <sequence>
+      <element ref="kml:targetHref"/>
+      <choice maxOccurs="unbounded">
+        <element ref="kml:Create"/>
+        <element ref="kml:Delete"/>
+        <element ref="kml:Change"/>
+        <element ref="kml:UpdateOpExtensionGroup"/>
+      </choice>
+      <element ref="kml:UpdateExtensionGroup" minOccurs="0"
+        maxOccurs="unbounded"/>
+    </sequence>
+  </complexType>
+  <element name="UpdateOpExtensionGroup" abstract="true"/>
+  <element name="UpdateExtensionGroup" abstract="true"/>
+
+  <element name="Create" type="kml:CreateType"/>
+  <complexType name="CreateType">
+    <sequence>
+      <element ref="kml:AbstractContainerGroup" minOccurs="0"
+        maxOccurs="unbounded"/>
+    </sequence>
+  </complexType>
+
+  <element name="Delete" type="kml:DeleteType"/>
+  <complexType name="DeleteType">
+    <sequence>
+      <element ref="kml:AbstractFeatureGroup" minOccurs="0"
+        maxOccurs="unbounded"/>
+    </sequence>
+  </complexType>
+
+  <element name="Change" type="kml:ChangeType"/>
+  <complexType name="ChangeType">
+    <sequence>
+      <element ref="kml:AbstractObjectGroup" minOccurs="0"
+        maxOccurs="unbounded"/>
+    </sequence>
+  </complexType>
+
+</schema>
diff --git a/mapproxy/test/schemas/kml/2.2.0/xAL.xsd b/mapproxy/test/schemas/kml/2.2.0/xAL.xsd
new file mode 100644
index 0000000..b652731
--- /dev/null
+++ b/mapproxy/test/schemas/kml/2.2.0/xAL.xsd
@@ -0,0 +1,1680 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--Modified by Ram Kumar (MSI) on 24 July 2002-->
+<xs:schema targetNamespace="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0" elementFormDefault="qualified">
+	<xs:annotation>
+		<xs:documentation>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</xs:documentation>
+	</xs:annotation>
+	<xs:annotation>
+		<xs:documentation>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</xs:documentation>
+	</xs:annotation>
+	<xs:attributeGroup name="grPostal">
+		<xs:attribute name="Code">
+			<xs:annotation>
+				<xs:documentation>Used by postal services to encode the name of the element.</xs:documentation>
+			</xs:annotation>
+		</xs:attribute>
+	</xs:attributeGroup>
+	<xs:element name="xAL">
+		<xs:annotation>
+			<xs:documentation>Root element for a list of addresses</xs:documentation>
+		</xs:annotation>
+		<xs:complexType>
+			<xs:sequence>
+				<xs:element ref="AddressDetails" maxOccurs="unbounded"/>
+				<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+			</xs:sequence>
+			<xs:attribute name="Version">
+				<xs:annotation>
+					<xs:documentation>Specific to DTD to specify the version number of DTD</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:anyAttribute namespace="##other"/>
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="AddressDetails" type="AddressDetails">
+		<xs:annotation>
+			<xs:documentation>This container defines the details of the address. Can define multiple addresses including tracking address history</xs:documentation>
+		</xs:annotation>
+	</xs:element>
+	<xs:complexType name="AddressDetails">
+		<xs:sequence>
+			<xs:element name="PostalServiceElements" minOccurs="0">
+				<xs:annotation>
+					<xs:documentation>Postal authorities use specific postal service data to expedient delivery of mail</xs:documentation>
+				</xs:annotation>
+				<xs:complexType>
+					<xs:sequence>
+						<xs:element name="AddressIdentifier" minOccurs="0" maxOccurs="unbounded">
+							<xs:annotation>
+								<xs:documentation>A unique identifier of an address assigned by postal authorities. Example: DPID in Australia</xs:documentation>
+							</xs:annotation>
+							<xs:complexType mixed="true">
+								<xs:attribute name="IdentifierType">
+									<xs:annotation>
+										<xs:documentation>Type of identifier. eg. DPID as in Australia</xs:documentation>
+									</xs:annotation>
+								</xs:attribute>
+								<xs:attribute name="Type"/>
+								<xs:attributeGroup ref="grPostal"/>
+								<xs:anyAttribute namespace="##other"/>
+							</xs:complexType>
+						</xs:element>
+						<xs:element name="EndorsementLineCode" minOccurs="0">
+							<xs:annotation>
+								<xs:documentation>Directly affects postal service distribution</xs:documentation>
+							</xs:annotation>
+							<xs:complexType mixed="true">
+								<xs:attribute name="Type">
+									<xs:annotation>
+										<xs:documentation>Specific to postal service</xs:documentation>
+									</xs:annotation>
+								</xs:attribute>
+								<xs:attributeGroup ref="grPostal"/>
+								<xs:anyAttribute namespace="##other"/>
+							</xs:complexType>
+						</xs:element>
+						<xs:element name="KeyLineCode" minOccurs="0">
+							<xs:annotation>
+								<xs:documentation>Required for some postal services</xs:documentation>
+							</xs:annotation>
+							<xs:complexType mixed="true">
+								<xs:attribute name="Type">
+									<xs:annotation>
+										<xs:documentation>Specific to postal service</xs:documentation>
+									</xs:annotation>
+								</xs:attribute>
+								<xs:attributeGroup ref="grPostal"/>
+								<xs:anyAttribute namespace="##other"/>
+							</xs:complexType>
+						</xs:element>
+						<xs:element name="Barcode" minOccurs="0">
+							<xs:annotation>
+								<xs:documentation>Required for some postal services</xs:documentation>
+							</xs:annotation>
+							<xs:complexType mixed="true">
+								<xs:attribute name="Type">
+									<xs:annotation>
+										<xs:documentation>Specific to postal service</xs:documentation>
+									</xs:annotation>
+								</xs:attribute>
+								<xs:attributeGroup ref="grPostal"/>
+								<xs:anyAttribute namespace="##other"/>
+							</xs:complexType>
+						</xs:element>
+						<xs:element name="SortingCode" minOccurs="0">
+							<xs:annotation>
+								<xs:documentation>Used for sorting addresses. Values may for example be CEDEX 16 (France)</xs:documentation>
+							</xs:annotation>
+							<xs:complexType>
+								<xs:attribute name="Type">
+									<xs:annotation>
+										<xs:documentation>Specific to postal service</xs:documentation>
+									</xs:annotation>
+								</xs:attribute>
+								<xs:attributeGroup ref="grPostal"/>
+							</xs:complexType>
+						</xs:element>
+						<xs:element name="AddressLatitude" minOccurs="0">
+							<xs:annotation>
+								<xs:documentation>Latitude of delivery address</xs:documentation>
+							</xs:annotation>
+							<xs:complexType mixed="true">
+								<xs:attribute name="Type">
+									<xs:annotation>
+										<xs:documentation>Specific to postal service</xs:documentation>
+									</xs:annotation>
+								</xs:attribute>
+								<xs:attributeGroup ref="grPostal"/>
+								<xs:anyAttribute namespace="##other"/>
+							</xs:complexType>
+						</xs:element>
+						<xs:element name="AddressLatitudeDirection" minOccurs="0">
+							<xs:annotation>
+								<xs:documentation>Latitude direction of delivery address;N = North and S = South</xs:documentation>
+							</xs:annotation>
+							<xs:complexType mixed="true">
+								<xs:annotation>
+									<xs:documentation>Specific to postal service</xs:documentation>
+								</xs:annotation>
+								<xs:attribute name="Type"/>
+								<xs:attributeGroup ref="grPostal"/>
+								<xs:anyAttribute namespace="##other"/>
+							</xs:complexType>
+						</xs:element>
+						<xs:element name="AddressLongitude" minOccurs="0">
+							<xs:annotation>
+								<xs:documentation>Longtitude of delivery address</xs:documentation>
+							</xs:annotation>
+							<xs:complexType mixed="true">
+								<xs:attribute name="Type">
+									<xs:annotation>
+										<xs:documentation>Specific to postal service</xs:documentation>
+									</xs:annotation>
+								</xs:attribute>
+								<xs:attributeGroup ref="grPostal"/>
+								<xs:anyAttribute namespace="##other"/>
+							</xs:complexType>
+						</xs:element>
+						<xs:element name="AddressLongitudeDirection" minOccurs="0">
+							<xs:annotation>
+								<xs:documentation>Longtitude direction of delivery address;N=North and S=South</xs:documentation>
+							</xs:annotation>
+							<xs:complexType mixed="true">
+								<xs:attribute name="Type">
+									<xs:annotation>
+										<xs:documentation>Specific to postal service</xs:documentation>
+									</xs:annotation>
+								</xs:attribute>
+								<xs:attributeGroup ref="grPostal"/>
+								<xs:anyAttribute namespace="##other"/>
+							</xs:complexType>
+						</xs:element>
+						<xs:element name="SupplementaryPostalServiceData" minOccurs="0" maxOccurs="unbounded">
+							<xs:annotation>
+								<xs:documentation>any postal service elements not covered by the container can be represented using this element</xs:documentation>
+							</xs:annotation>
+							<xs:complexType mixed="true">
+								<xs:attribute name="Type">
+									<xs:annotation>
+										<xs:documentation>Specific to postal service</xs:documentation>
+									</xs:annotation>
+								</xs:attribute>
+								<xs:attributeGroup ref="grPostal"/>
+								<xs:anyAttribute namespace="##other"/>
+							</xs:complexType>
+						</xs:element>
+						<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+					</xs:sequence>
+					<xs:attribute name="Type">
+						<xs:annotation>
+							<xs:documentation>USPS, ECMA, UN/PROLIST, etc</xs:documentation>
+						</xs:annotation>
+					</xs:attribute>
+					<xs:anyAttribute namespace="##other"/>
+				</xs:complexType>
+			</xs:element>
+			<xs:choice minOccurs="0">
+				<xs:annotation>
+					<xs:documentation>Use the most suitable option. Country contains the most detailed information while Locality is missing Country and AdminArea</xs:documentation>
+				</xs:annotation>
+				<xs:element name="Address">
+					<xs:annotation>
+						<xs:documentation>Address as one line of free text</xs:documentation>
+					</xs:annotation>
+					<xs:complexType mixed="true">
+						<xs:attribute name="Type">
+							<xs:annotation>
+								<xs:documentation>Postal, residential, corporate, etc</xs:documentation>
+							</xs:annotation>
+						</xs:attribute>
+						<xs:attributeGroup ref="grPostal"/>
+						<xs:anyAttribute namespace="##other"/>
+					</xs:complexType>
+				</xs:element>
+				<xs:element name="AddressLines" type="AddressLinesType">
+					<xs:annotation>
+						<xs:documentation>Container for Address lines</xs:documentation>
+					</xs:annotation>
+				</xs:element>
+				<xs:element name="Country">
+					<xs:annotation>
+						<xs:documentation>Specification of a country</xs:documentation>
+					</xs:annotation>
+					<xs:complexType>
+						<xs:sequence>
+							<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+							<xs:element name="CountryNameCode" minOccurs="0" maxOccurs="unbounded">
+								<xs:annotation>
+									<xs:documentation>A country code according to the specified scheme</xs:documentation>
+								</xs:annotation>
+								<xs:complexType mixed="true">
+									<xs:attribute name="Scheme">
+										<xs:annotation>
+											<xs:documentation>Country code scheme possible values, but not limited to: iso.3166-2, iso.3166-3 for two and three character country codes.</xs:documentation>
+										</xs:annotation>
+									</xs:attribute>
+									<xs:attributeGroup ref="grPostal"/>
+									<xs:anyAttribute namespace="##other"/>
+								</xs:complexType>
+							</xs:element>
+							<xs:element ref="CountryName" minOccurs="0" maxOccurs="unbounded"/>
+							<xs:choice minOccurs="0">
+								<xs:element ref="AdministrativeArea"/>
+								<xs:element ref="Locality"/>
+								<xs:element ref="Thoroughfare"/>
+							</xs:choice>
+							<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+						</xs:sequence>
+						<xs:anyAttribute namespace="##other"/>
+					</xs:complexType>
+				</xs:element>
+				<xs:element ref="AdministrativeArea"/>
+				<xs:element ref="Locality"/>
+				<xs:element ref="Thoroughfare"/>
+			</xs:choice>
+			<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+		</xs:sequence>
+		<xs:attribute name="AddressType">
+			<xs:annotation>
+				<xs:documentation>Type of address. Example: Postal, residential,business, primary, secondary, etc</xs:documentation>
+			</xs:annotation>
+		</xs:attribute>
+		<xs:attribute name="CurrentStatus">
+			<xs:annotation>
+				<xs:documentation>Moved, Living, Investment, Deceased, etc..</xs:documentation>
+			</xs:annotation>
+		</xs:attribute>
+		<xs:attribute name="ValidFromDate">
+			<xs:annotation>
+				<xs:documentation>Start Date of the validity of address</xs:documentation>
+			</xs:annotation>
+		</xs:attribute>
+		<xs:attribute name="ValidToDate">
+			<xs:annotation>
+				<xs:documentation>End date of the validity of address</xs:documentation>
+			</xs:annotation>
+		</xs:attribute>
+		<xs:attribute name="Usage">
+			<xs:annotation>
+				<xs:documentation>Communication, Contact, etc.</xs:documentation>
+			</xs:annotation>
+		</xs:attribute>
+		<xs:attributeGroup ref="grPostal"/>
+		<xs:attribute name="AddressDetailsKey">
+			<xs:annotation>
+				<xs:documentation>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.</xs:documentation>
+			</xs:annotation>
+		</xs:attribute>
+		<xs:anyAttribute namespace="##other"/>
+	</xs:complexType>
+	<xs:complexType name="AddressLinesType">
+		<xs:sequence>
+			<xs:element ref="AddressLine" maxOccurs="unbounded"/>
+			<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+		</xs:sequence>
+		<xs:anyAttribute namespace="##other"/>
+	</xs:complexType>
+	<xs:complexType name="BuildingNameType" mixed="true">
+		<xs:attribute name="Type"/>
+		<xs:attribute name="TypeOccurrence">
+			<xs:annotation>
+				<xs:documentation>Occurrence of the building name before/after the type. eg. EGIS BUILDING where name appears before type</xs:documentation>
+			</xs:annotation>
+			<xs:simpleType>
+				<xs:restriction base="xs:NMTOKEN">
+					<xs:enumeration value="Before"/>
+					<xs:enumeration value="After"/>
+				</xs:restriction>
+			</xs:simpleType>
+		</xs:attribute>
+		<xs:attributeGroup ref="grPostal"/>
+		<xs:anyAttribute namespace="##other"/>
+	</xs:complexType>
+	<xs:complexType name="DependentLocalityType">
+		<xs:sequence>
+			<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+			<xs:element name="DependentLocalityName" minOccurs="0" maxOccurs="unbounded">
+				<xs:annotation>
+					<xs:documentation>Name of the dependent locality</xs:documentation>
+				</xs:annotation>
+				<xs:complexType mixed="true">
+					<xs:attribute name="Type"/>
+					<xs:attributeGroup ref="grPostal"/>
+					<xs:anyAttribute namespace="##other"/>
+				</xs:complexType>
+			</xs:element>
+			<xs:element name="DependentLocalityNumber" minOccurs="0">
+				<xs:annotation>
+					<xs:documentation>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</xs:documentation>
+				</xs:annotation>
+				<xs:complexType mixed="true">
+					<xs:attribute name="NameNumberOccurrence">
+						<xs:annotation>
+							<xs:documentation>Eg. SECTOR occurs before 5 in SECTOR 5</xs:documentation>
+						</xs:annotation>
+						<xs:simpleType>
+							<xs:restriction base="xs:NMTOKEN">
+								<xs:enumeration value="Before"/>
+								<xs:enumeration value="After"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:attribute>
+					<xs:attributeGroup ref="grPostal"/>
+					<xs:anyAttribute namespace="##other"/>
+				</xs:complexType>
+			</xs:element>
+			<xs:choice minOccurs="0">
+				<xs:element ref="PostBox"/>
+				<xs:element name="LargeMailUser" type="LargeMailUserType">
+					<xs:annotation>
+						<xs:documentation>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</xs:documentation>
+					</xs:annotation>
+				</xs:element>
+				<xs:element ref="PostOffice"/>
+				<xs:element name="PostalRoute" type="PostalRouteType">
+					<xs:annotation>
+						<xs:documentation> A Postal van is specific for a route as in Is`rael, Rural route</xs:documentation>
+					</xs:annotation>
+				</xs:element>
+			</xs:choice>
+			<xs:element ref="Thoroughfare" minOccurs="0"/>
+			<xs:element ref="Premise" minOccurs="0"/>
+			<xs:element name="DependentLocality" type="DependentLocalityType" minOccurs="0">
+				<xs:annotation>
+					<xs:documentation>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).</xs:documentation>
+				</xs:annotation>
+			</xs:element>
+			<xs:element ref="PostalCode" minOccurs="0"/>
+			<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+		</xs:sequence>
+		<xs:attribute name="Type">
+			<xs:annotation>
+				<xs:documentation>City or IndustrialEstate, etc</xs:documentation>
+			</xs:annotation>
+		</xs:attribute>
+		<xs:attribute name="UsageType">
+			<xs:annotation>
+				<xs:documentation>Postal or Political - Sometimes locations must be distinguished between postal system, and physical locations as defined by a political system</xs:documentation>
+			</xs:annotation>
+		</xs:attribute>
+		<xs:attribute name="Connector">
+			<xs:annotation>
+				<xs:documentation>"VIA" as in Hill Top VIA Parish where Parish is a locality and Hill Top is a dependent locality</xs:documentation>
+			</xs:annotation>
+		</xs:attribute>
+		<xs:attribute name="Indicator">
+			<xs:annotation>
+				<xs:documentation>Eg. Erode (Dist) where (Dist) is the Indicator</xs:documentation>
+			</xs:annotation>
+		</xs:attribute>
+		<xs:anyAttribute namespace="##other"/>
+	</xs:complexType>
+	<xs:complexType name="FirmType">
+		<xs:sequence>
+			<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+			<xs:element name="FirmName" minOccurs="0" maxOccurs="unbounded">
+				<xs:annotation>
+					<xs:documentation>Name of the firm</xs:documentation>
+				</xs:annotation>
+				<xs:complexType mixed="true">
+					<xs:attribute name="Type"/>
+					<xs:attributeGroup ref="grPostal"/>
+					<xs:anyAttribute namespace="##other"/>
+				</xs:complexType>
+			</xs:element>
+			<xs:element ref="Department" minOccurs="0" maxOccurs="unbounded"/>
+			<xs:element name="MailStop" type="MailStopType" minOccurs="0">
+				<xs:annotation>
+					<xs:documentation>A MailStop is where the the mail is delivered to within a premise/subpremise/firm or a facility.</xs:documentation>
+				</xs:annotation>
+			</xs:element>
+			<xs:element ref="PostalCode" minOccurs="0"/>
+			<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+		</xs:sequence>
+		<xs:attribute name="Type"/>
+		<xs:anyAttribute namespace="##other"/>
+	</xs:complexType>
+	<xs:complexType name="LargeMailUserType">
+		<xs:sequence>
+			<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+			<xs:element name="LargeMailUserName" minOccurs="0" maxOccurs="unbounded">
+				<xs:annotation>
+					<xs:documentation>Name of the large mail user. eg. Smith Ford International airport</xs:documentation>
+				</xs:annotation>
+				<xs:complexType mixed="true">
+					<xs:attribute name="Type" type="xs:string">
+						<xs:annotation>
+							<xs:documentation>Airport, Hospital, etc</xs:documentation>
+						</xs:annotation>
+					</xs:attribute>
+					<xs:attribute name="Code" type="xs:string"/>
+					<xs:anyAttribute namespace="##other"/>
+				</xs:complexType>
+			</xs:element>
+			<xs:element name="LargeMailUserIdentifier" minOccurs="0">
+				<xs:annotation>
+					<xs:documentation>Specification of the identification number of a large mail user. An example are the Cedex codes in France.</xs:documentation>
+				</xs:annotation>
+				<xs:complexType mixed="true">
+					<xs:attribute name="Type" type="xs:string">
+						<xs:annotation>
+							<xs:documentation>CEDEX Code</xs:documentation>
+						</xs:annotation>
+					</xs:attribute>
+					<xs:attribute name="Indicator">
+						<xs:annotation>
+							<xs:documentation>eg. Building 429 in which Building is the Indicator</xs:documentation>
+						</xs:annotation>
+					</xs:attribute>
+					<xs:attributeGroup ref="grPostal"/>
+					<xs:anyAttribute namespace="##other"/>
+				</xs:complexType>
+			</xs:element>
+			<xs:element name="BuildingName" type="BuildingNameType" minOccurs="0" maxOccurs="unbounded">
+				<xs:annotation>
+					<xs:documentation>Name of the building</xs:documentation>
+				</xs:annotation>
+			</xs:element>
+			<xs:element ref="Department" minOccurs="0"/>
+			<xs:element ref="PostBox" minOccurs="0"/>
+			<xs:element ref="Thoroughfare" minOccurs="0"/>
+			<xs:element ref="PostalCode" minOccurs="0"/>
+			<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+		</xs:sequence>
+		<xs:attribute name="Type" type="xs:string"/>
+		<xs:anyAttribute namespace="##other"/>
+	</xs:complexType>
+	<xs:complexType name="MailStopType">
+		<xs:sequence>
+			<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+			<xs:element name="MailStopName" minOccurs="0">
+				<xs:annotation>
+					<xs:documentation>Name of the the Mail Stop. eg. MSP, MS, etc</xs:documentation>
+				</xs:annotation>
+				<xs:complexType mixed="true">
+					<xs:attribute name="Type"/>
+					<xs:attributeGroup ref="grPostal"/>
+					<xs:anyAttribute namespace="##other"/>
+				</xs:complexType>
+			</xs:element>
+			<xs:element name="MailStopNumber" minOccurs="0">
+				<xs:annotation>
+					<xs:documentation>Number of the Mail stop. eg. 123 in MS 123</xs:documentation>
+				</xs:annotation>
+				<xs:complexType mixed="true">
+					<xs:attribute name="NameNumberSeparator">
+						<xs:annotation>
+							<xs:documentation>"-" in MS-123</xs:documentation>
+						</xs:annotation>
+					</xs:attribute>
+					<xs:attributeGroup ref="grPostal"/>
+					<xs:anyAttribute namespace="##other"/>
+				</xs:complexType>
+			</xs:element>
+			<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+		</xs:sequence>
+		<xs:attribute name="Type"/>
+		<xs:anyAttribute namespace="##other"/>
+	</xs:complexType>
+	<xs:complexType name="PostalRouteType">
+		<xs:sequence>
+			<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+			<xs:choice>
+				<xs:element name="PostalRouteName" maxOccurs="unbounded">
+					<xs:annotation>
+						<xs:documentation> Name of the Postal Route</xs:documentation>
+					</xs:annotation>
+					<xs:complexType mixed="true">
+						<xs:attribute name="Type"/>
+						<xs:attributeGroup ref="grPostal"/>
+						<xs:anyAttribute namespace="##other"/>
+					</xs:complexType>
+				</xs:element>
+				<xs:element name="PostalRouteNumber">
+					<xs:annotation>
+						<xs:documentation> Number of the Postal Route</xs:documentation>
+					</xs:annotation>
+					<xs:complexType mixed="true">
+						<xs:attributeGroup ref="grPostal"/>
+						<xs:anyAttribute namespace="##other"/>
+					</xs:complexType>
+				</xs:element>
+			</xs:choice>
+			<xs:element ref="PostBox" minOccurs="0"/>
+			<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+		</xs:sequence>
+		<xs:attribute name="Type"/>
+		<xs:anyAttribute namespace="##other"/>
+	</xs:complexType>
+	<xs:complexType name="SubPremiseType">
+		<xs:sequence>
+			<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+			<xs:element name="SubPremiseName" minOccurs="0" maxOccurs="unbounded">
+				<xs:annotation>
+					<xs:documentation> Name of the SubPremise</xs:documentation>
+				</xs:annotation>
+				<xs:complexType mixed="true">
+					<xs:attribute name="Type"/>
+					<xs:attribute name="TypeOccurrence">
+						<xs:annotation>
+							<xs:documentation>EGIS Building where EGIS occurs before Building</xs:documentation>
+						</xs:annotation>
+						<xs:simpleType>
+							<xs:restriction base="xs:NMTOKEN">
+								<xs:enumeration value="Before"/>
+								<xs:enumeration value="After"/>
+							</xs:restriction>
+						</xs:simpleType>
+					</xs:attribute>
+					<xs:attributeGroup ref="grPostal"/>
+					<xs:anyAttribute namespace="##other"/>
+				</xs:complexType>
+			</xs:element>
+			<xs:choice minOccurs="0">
+				<xs:element name="SubPremiseLocation">
+					<xs:annotation>
+						<xs:documentation> Name of the SubPremise Location. eg. LOBBY, BASEMENT, GROUND FLOOR, etc...</xs:documentation>
+					</xs:annotation>
+					<xs:complexType mixed="true">
+						<xs:attributeGroup ref="grPostal"/>
+					</xs:complexType>
+				</xs:element>
+				<xs:element name="SubPremiseNumber" minOccurs="0" maxOccurs="unbounded">
+					<xs:annotation>
+						<xs:documentation> 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).</xs:documentation>
+					</xs:annotation>
+					<xs:complexType mixed="true">
+						<xs:attribute name="Indicator">
+							<xs:annotation>
+								<xs:documentation>"TH" in 12TH which is a floor number, "NO." in NO.1, "#" in APT #12, etc.</xs:documentation>
+							</xs:annotation>
+						</xs:attribute>
+						<xs:attribute name="IndicatorOccurrence">
+							<xs:annotation>
+								<xs:documentation>"No." occurs before 1 in No.1, or TH occurs after 12 in 12TH</xs:documentation>
+							</xs:annotation>
+							<xs:simpleType>
+								<xs:restriction base="xs:NMTOKEN">
+									<xs:enumeration value="Before"/>
+									<xs:enumeration value="After"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:attribute>
+						<xs:attribute name="NumberTypeOccurrence">
+							<xs:annotation>
+								<xs:documentation>12TH occurs "before" FLOOR (a type of subpremise) in 12TH FLOOR</xs:documentation>
+							</xs:annotation>
+							<xs:simpleType>
+								<xs:restriction base="xs:NMTOKEN">
+									<xs:enumeration value="Before"/>
+									<xs:enumeration value="After"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:attribute>
+						<xs:attribute name="PremiseNumberSeparator">
+							<xs:annotation>
+								<xs:documentation>"/" in 12/14 Archer Street where 12 is sub-premise number and 14 is premise number</xs:documentation>
+							</xs:annotation>
+						</xs:attribute>
+						<xs:attribute name="Type"/>
+						<xs:attributeGroup ref="grPostal"/>
+						<xs:anyAttribute namespace="##other"/>
+					</xs:complexType>
+				</xs:element>
+			</xs:choice>
+			<xs:element name="SubPremiseNumberPrefix" minOccurs="0" maxOccurs="unbounded">
+				<xs:annotation>
+					<xs:documentation> Prefix of the sub premise number. eg. A in A-12</xs:documentation>
+				</xs:annotation>
+				<xs:complexType mixed="true">
+					<xs:attribute name="NumberPrefixSeparator">
+						<xs:annotation>
+							<xs:documentation>A-12 where 12 is number and A is prefix and "-" is the separator</xs:documentation>
+						</xs:annotation>
+					</xs:attribute>
+					<xs:attribute name="Type"/>
+					<xs:attributeGroup ref="grPostal"/>
+					<xs:anyAttribute namespace="##other"/>
+				</xs:complexType>
+			</xs:element>
+			<xs:element name="SubPremiseNumberSuffix" minOccurs="0" maxOccurs="unbounded">
+				<xs:annotation>
+					<xs:documentation> Suffix of the sub premise number. eg. A in 12A</xs:documentation>
+				</xs:annotation>
+				<xs:complexType mixed="true">
+					<xs:attribute name="NumberSuffixSeparator">
+						<xs:annotation>
+							<xs:documentation>12-A where 12 is number and A is suffix and "-" is the separator</xs:documentation>
+						</xs:annotation>
+					</xs:attribute>
+					<xs:attribute name="Type"/>
+					<xs:attributeGroup ref="grPostal"/>
+					<xs:anyAttribute namespace="##other"/>
+				</xs:complexType>
+			</xs:element>
+			<xs:element name="BuildingName" type="BuildingNameType" minOccurs="0" maxOccurs="unbounded">
+				<xs:annotation>
+					<xs:documentation>Name of the building</xs:documentation>
+				</xs:annotation>
+			</xs:element>
+			<xs:element name="Firm" type="FirmType" minOccurs="0">
+				<xs:annotation>
+					<xs:documentation>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.</xs:documentation>
+				</xs:annotation>
+			</xs:element>
+			<xs:element name="MailStop" type="MailStopType" minOccurs="0">
+				<xs:annotation>
+					<xs:documentation>A MailStop is where the the mail is delivered to within a premise/subpremise/firm or a facility.</xs:documentation>
+				</xs:annotation>
+			</xs:element>
+			<xs:element ref="PostalCode" minOccurs="0"/>
+			<xs:element name="SubPremise" type="SubPremiseType" minOccurs="0">
+				<xs:annotation>
+					<xs:documentation>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</xs:documentation>
+				</xs:annotation>
+			</xs:element>
+			<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+		</xs:sequence>
+		<xs:attribute name="Type"/>
+		<xs:anyAttribute namespace="##other"/>
+	</xs:complexType>
+	<xs:complexType name="ThoroughfareLeadingTypeType" mixed="true">
+		<xs:attribute name="Type"/>
+		<xs:attributeGroup ref="grPostal"/>
+		<xs:anyAttribute namespace="##other"/>
+	</xs:complexType>
+	<xs:complexType name="ThoroughfareNameType" mixed="true">
+		<xs:attribute name="Type"/>
+		<xs:attributeGroup ref="grPostal"/>
+		<xs:anyAttribute namespace="##other"/>
+	</xs:complexType>
+	<xs:complexType name="ThoroughfarePostDirectionType" mixed="true">
+		<xs:attribute name="Type"/>
+		<xs:attributeGroup ref="grPostal"/>
+		<xs:anyAttribute namespace="##other"/>
+	</xs:complexType>
+	<xs:complexType name="ThoroughfarePreDirectionType" mixed="true">
+		<xs:attribute name="Type"/>
+		<xs:attributeGroup ref="grPostal"/>
+		<xs:anyAttribute namespace="##other"/>
+	</xs:complexType>
+	<xs:complexType name="ThoroughfareTrailingTypeType" mixed="true">
+		<xs:attribute name="Type"/>
+		<xs:attributeGroup ref="grPostal"/>
+		<xs:anyAttribute namespace="##other"/>
+	</xs:complexType>
+	<xs:element name="AddressLine">
+		<xs:annotation>
+			<xs:documentation>Free format address representation. An address can have more than one line. The order of the AddressLine elements must be preserved.</xs:documentation>
+		</xs:annotation>
+		<xs:complexType mixed="true">
+			<xs:attribute name="Type">
+				<xs:annotation>
+					<xs:documentation>Defines the type of address line. eg. Street, Address Line 1, etc.</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:attributeGroup ref="grPostal"/>
+			<xs:anyAttribute namespace="##other"/>
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="Locality">
+		<xs:annotation>
+			<xs:documentation>Locality is one level lower than adminisstrative area. Eg.: cities, reservations and any other built-up areas.</xs:documentation>
+		</xs:annotation>
+		<xs:complexType>
+			<xs:sequence>
+				<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+				<xs:element name="LocalityName" minOccurs="0" maxOccurs="unbounded">
+					<xs:annotation>
+						<xs:documentation>Name of the locality</xs:documentation>
+					</xs:annotation>
+					<xs:complexType mixed="true">
+						<xs:attribute name="Type"/>
+						<xs:attributeGroup ref="grPostal"/>
+						<xs:anyAttribute namespace="##other"/>
+					</xs:complexType>
+				</xs:element>
+				<xs:choice minOccurs="0">
+					<xs:element ref="PostBox"/>
+					<xs:element name="LargeMailUser" type="LargeMailUserType">
+						<xs:annotation>
+							<xs:documentation>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</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element ref="PostOffice"/>
+					<xs:element name="PostalRoute" type="PostalRouteType">
+						<xs:annotation>
+							<xs:documentation>A Postal van is specific for a route as in Is`rael, Rural route</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+				</xs:choice>
+				<xs:element ref="Thoroughfare" minOccurs="0"/>
+				<xs:element ref="Premise" minOccurs="0"/>
+				<xs:element name="DependentLocality" type="DependentLocalityType" minOccurs="0">
+					<xs:annotation>
+						<xs:documentation>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).</xs:documentation>
+					</xs:annotation>
+				</xs:element>
+				<xs:element ref="PostalCode" minOccurs="0"/>
+				<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+			</xs:sequence>
+			<xs:attribute name="Type">
+				<xs:annotation>
+					<xs:documentation>Possible values not limited to: City, IndustrialEstate, etc</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:attribute name="UsageType">
+				<xs:annotation>
+					<xs:documentation>Postal or Political - Sometimes locations must be distinguished between postal system, and physical locations as defined by a political system</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:attribute name="Indicator">
+				<xs:annotation>
+					<xs:documentation>Erode (Dist) where (Dist) is the Indicator</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:anyAttribute namespace="##other"/>
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="Thoroughfare">
+		<xs:annotation>
+			<xs:documentation>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</xs:documentation>
+		</xs:annotation>
+		<xs:complexType>
+			<xs:sequence>
+				<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+				<xs:choice minOccurs="0" maxOccurs="unbounded">
+					<xs:element ref="ThoroughfareNumber"/>
+					<xs:element name="ThoroughfareNumberRange">
+						<xs:annotation>
+							<xs:documentation>A container to represent a range of numbers (from x thru y)for a thoroughfare. eg. 1-2 Albert Av</xs:documentation>
+						</xs:annotation>
+						<xs:complexType>
+							<xs:sequence>
+								<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+								<xs:element name="ThoroughfareNumberFrom">
+									<xs:annotation>
+										<xs:documentation>Starting number in the range</xs:documentation>
+									</xs:annotation>
+									<xs:complexType mixed="true">
+										<xs:sequence>
+											<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+											<xs:element ref="ThoroughfareNumberPrefix" minOccurs="0" maxOccurs="unbounded"/>
+											<xs:element ref="ThoroughfareNumber" maxOccurs="unbounded"/>
+											<xs:element ref="ThoroughfareNumberSuffix" minOccurs="0" maxOccurs="unbounded"/>
+										</xs:sequence>
+										<xs:attributeGroup ref="grPostal"/>
+										<xs:anyAttribute namespace="##other"/>
+									</xs:complexType>
+								</xs:element>
+								<xs:element name="ThoroughfareNumberTo">
+									<xs:annotation>
+										<xs:documentation>Ending number in the range</xs:documentation>
+									</xs:annotation>
+									<xs:complexType mixed="true">
+										<xs:sequence>
+											<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+											<xs:element ref="ThoroughfareNumberPrefix" minOccurs="0" maxOccurs="unbounded"/>
+											<xs:element ref="ThoroughfareNumber" maxOccurs="unbounded"/>
+											<xs:element ref="ThoroughfareNumberSuffix" minOccurs="0" maxOccurs="unbounded"/>
+										</xs:sequence>
+										<xs:attributeGroup ref="grPostal"/>
+										<xs:anyAttribute namespace="##other"/>
+									</xs:complexType>
+								</xs:element>
+							</xs:sequence>
+							<xs:attribute name="RangeType">
+								<xs:annotation>
+									<xs:documentation>Thoroughfare number ranges are odd or even</xs:documentation>
+								</xs:annotation>
+								<xs:simpleType>
+									<xs:restriction base="xs:NMTOKEN">
+										<xs:enumeration value="Odd"/>
+										<xs:enumeration value="Even"/>
+									</xs:restriction>
+								</xs:simpleType>
+							</xs:attribute>
+							<xs:attribute name="Indicator">
+								<xs:annotation>
+									<xs:documentation>"No." No.12-13</xs:documentation>
+								</xs:annotation>
+							</xs:attribute>
+							<xs:attribute name="Separator">
+								<xs:annotation>
+									<xs:documentation>"-" in 12-14  or "Thru" in 12 Thru 14 etc.</xs:documentation>
+								</xs:annotation>
+							</xs:attribute>
+							<xs:attribute name="IndicatorOccurrence">
+								<xs:annotation>
+									<xs:documentation>No.12-14 where "No." is before actual street number</xs:documentation>
+								</xs:annotation>
+								<xs:simpleType>
+									<xs:restriction base="xs:NMTOKEN">
+										<xs:enumeration value="Before"/>
+										<xs:enumeration value="After"/>
+									</xs:restriction>
+								</xs:simpleType>
+							</xs:attribute>
+							<xs:attribute name="NumberRangeOccurrence">
+								<xs:annotation>
+									<xs:documentation>23-25 Archer St, where number appears before name</xs:documentation>
+								</xs:annotation>
+								<xs:simpleType>
+									<xs:restriction base="xs:NMTOKEN">
+										<xs:enumeration value="BeforeName"/>
+										<xs:enumeration value="AfterName"/>
+										<xs:enumeration value="BeforeType"/>
+										<xs:enumeration value="AfterType"/>
+									</xs:restriction>
+								</xs:simpleType>
+							</xs:attribute>
+							<xs:attribute name="Type"/>
+							<xs:attributeGroup ref="grPostal"/>
+							<xs:anyAttribute namespace="##other"/>
+						</xs:complexType>
+					</xs:element>
+				</xs:choice>
+				<xs:element ref="ThoroughfareNumberPrefix" minOccurs="0" maxOccurs="unbounded"/>
+				<xs:element ref="ThoroughfareNumberSuffix" minOccurs="0" maxOccurs="unbounded"/>
+				<xs:element name="ThoroughfarePreDirection" type="ThoroughfarePreDirectionType" minOccurs="0">
+					<xs:annotation>
+						<xs:documentation>North Baker Street, where North is the pre-direction. The direction appears before the name.</xs:documentation>
+					</xs:annotation>
+				</xs:element>
+				<xs:element name="ThoroughfareLeadingType" type="ThoroughfareLeadingTypeType" minOccurs="0">
+					<xs:annotation>
+						<xs:documentation>Appears before the thoroughfare name. Ed. Spanish: Avenida Aurora, where Avenida is the leading type / French: Rue Moliere, where Rue is the leading type.</xs:documentation>
+					</xs:annotation>
+				</xs:element>
+				<xs:element name="ThoroughfareName" type="ThoroughfareNameType" minOccurs="0" maxOccurs="unbounded">
+					<xs:annotation>
+						<xs:documentation>Specification of the name of a Thoroughfare (also dependant street name): street name, canal name, etc.</xs:documentation>
+					</xs:annotation>
+				</xs:element>
+				<xs:element name="ThoroughfareTrailingType" type="ThoroughfareTrailingTypeType" minOccurs="0">
+					<xs:annotation>
+						<xs:documentation>Appears after the thoroughfare name. Ed. British: Baker Lane, where Lane is the trailing type.</xs:documentation>
+					</xs:annotation>
+				</xs:element>
+				<xs:element name="ThoroughfarePostDirection" type="ThoroughfarePostDirectionType" minOccurs="0">
+					<xs:annotation>
+						<xs:documentation>221-bis Baker Street North, where North is the post-direction. The post-direction appears after the name.</xs:documentation>
+					</xs:annotation>
+				</xs:element>
+				<xs:element name="DependentThoroughfare" minOccurs="0">
+					<xs:annotation>
+						<xs:documentation>DependentThroughfare is related to a street; occurs in GB, IE, ES, PT</xs:documentation>
+					</xs:annotation>
+					<xs:complexType>
+						<xs:sequence>
+							<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+							<xs:element name="ThoroughfarePreDirection" type="ThoroughfarePreDirectionType" minOccurs="0">
+								<xs:annotation>
+									<xs:documentation>North Baker Street, where North is the pre-direction. The direction appears before the name.</xs:documentation>
+								</xs:annotation>
+							</xs:element>
+							<xs:element name="ThoroughfareLeadingType" type="ThoroughfareLeadingTypeType" minOccurs="0">
+								<xs:annotation>
+									<xs:documentation>Appears before the thoroughfare name. Ed. Spanish: Avenida Aurora, where Avenida is the leading type / French: Rue Moliere, where Rue is the leading type.</xs:documentation>
+								</xs:annotation>
+							</xs:element>
+							<xs:element name="ThoroughfareName" type="ThoroughfareNameType" minOccurs="0" maxOccurs="unbounded">
+								<xs:annotation>
+									<xs:documentation>Specification of the name of a Thoroughfare (also dependant street name): street name, canal name, etc.</xs:documentation>
+								</xs:annotation>
+							</xs:element>
+							<xs:element name="ThoroughfareTrailingType" type="ThoroughfareTrailingTypeType" minOccurs="0">
+								<xs:annotation>
+									<xs:documentation>Appears after the thoroughfare name. Ed. British: Baker Lane, where Lane is the trailing type.</xs:documentation>
+								</xs:annotation>
+							</xs:element>
+							<xs:element name="ThoroughfarePostDirection" type="ThoroughfarePostDirectionType" minOccurs="0">
+								<xs:annotation>
+									<xs:documentation>221-bis Baker Street North, where North is the post-direction. The post-direction appears after the name.</xs:documentation>
+								</xs:annotation>
+							</xs:element>
+							<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+						</xs:sequence>
+						<xs:attribute name="Type"/>
+						<xs:anyAttribute namespace="##other"/>
+					</xs:complexType>
+				</xs:element>
+				<xs:choice minOccurs="0">
+					<xs:element name="DependentLocality" type="DependentLocalityType">
+						<xs:annotation>
+							<xs:documentation>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).</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element ref="Premise"/>
+					<xs:element name="Firm" type="FirmType">
+						<xs:annotation>
+							<xs:documentation>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.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element ref="PostalCode"/>
+				</xs:choice>
+				<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+			</xs:sequence>
+			<xs:attribute name="Type"/>
+			<xs:attribute name="DependentThoroughfares">
+				<xs:annotation>
+					<xs:documentation>Does this thoroughfare have a a dependent thoroughfare? Corner of street X, etc</xs:documentation>
+				</xs:annotation>
+				<xs:simpleType>
+					<xs:restriction base="xs:NMTOKEN">
+						<xs:enumeration value="Yes"/>
+						<xs:enumeration value="No"/>
+					</xs:restriction>
+				</xs:simpleType>
+			</xs:attribute>
+			<xs:attribute name="DependentThoroughfaresIndicator">
+				<xs:annotation>
+					<xs:documentation>Corner of, Intersection of</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:attribute name="DependentThoroughfaresConnector">
+				<xs:annotation>
+					<xs:documentation>Corner of Street1 AND Street 2 where AND is the Connector</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:attribute name="DependentThoroughfaresType">
+				<xs:annotation>
+					<xs:documentation>STS in GEORGE and ADELAIDE STS, RDS IN A and B RDS, etc. Use only when both the street types are the same</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:anyAttribute namespace="##other"/>
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="AdministrativeArea">
+		<xs:annotation>
+			<xs:documentation>Examples of administrative areas are provinces counties, special regions (such as "Rijnmond"), etc.</xs:documentation>
+		</xs:annotation>
+		<xs:complexType>
+			<xs:sequence>
+				<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+				<xs:element name="AdministrativeAreaName" minOccurs="0" maxOccurs="unbounded">
+					<xs:annotation>
+						<xs:documentation> Name of the administrative area. eg. MI in USA, NSW in Australia</xs:documentation>
+					</xs:annotation>
+					<xs:complexType mixed="true">
+						<xs:attribute name="Type"/>
+						<xs:attributeGroup ref="grPostal"/>
+						<xs:anyAttribute namespace="##other"/>
+					</xs:complexType>
+				</xs:element>
+				<xs:element name="SubAdministrativeArea" minOccurs="0">
+					<xs:annotation>
+						<xs:documentation> 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.</xs:documentation>
+					</xs:annotation>
+					<xs:complexType>
+						<xs:sequence>
+							<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+							<xs:element name="SubAdministrativeAreaName" minOccurs="0" maxOccurs="unbounded">
+								<xs:annotation>
+									<xs:documentation> Name of the sub-administrative area</xs:documentation>
+								</xs:annotation>
+								<xs:complexType mixed="true">
+									<xs:attribute name="Type"/>
+									<xs:attributeGroup ref="grPostal"/>
+									<xs:anyAttribute namespace="##other"/>
+								</xs:complexType>
+							</xs:element>
+							<xs:choice minOccurs="0">
+								<xs:element ref="Locality"/>
+								<xs:element ref="PostOffice"/>
+								<xs:element ref="PostalCode"/>
+							</xs:choice>
+							<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+						</xs:sequence>
+						<xs:attribute name="Type">
+							<xs:annotation>
+								<xs:documentation>Province or State or County or Kanton, etc</xs:documentation>
+							</xs:annotation>
+						</xs:attribute>
+						<xs:attribute name="UsageType">
+							<xs:annotation>
+								<xs:documentation>Postal or Political - Sometimes locations must be distinguished between postal system, and physical locations as defined by a political system</xs:documentation>
+							</xs:annotation>
+						</xs:attribute>
+						<xs:attribute name="Indicator">
+							<xs:annotation>
+								<xs:documentation>Erode (Dist) where (Dist) is the Indicator</xs:documentation>
+							</xs:annotation>
+						</xs:attribute>
+						<xs:anyAttribute namespace="##other"/>
+					</xs:complexType>
+				</xs:element>
+				<xs:choice minOccurs="0">
+					<xs:element ref="Locality"/>
+					<xs:element ref="PostOffice"/>
+					<xs:element ref="PostalCode"/>
+				</xs:choice>
+				<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+			</xs:sequence>
+			<xs:attribute name="Type">
+				<xs:annotation>
+					<xs:documentation>Province or State or County or Kanton, etc</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:attribute name="UsageType">
+				<xs:annotation>
+					<xs:documentation>Postal or Political - Sometimes locations must be distinguished between postal system, and physical locations as defined by a political system</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:attribute name="Indicator">
+				<xs:annotation>
+					<xs:documentation>Erode (Dist) where (Dist) is the Indicator</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:anyAttribute namespace="##other"/>
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="PostOffice">
+		<xs:annotation>
+			<xs:documentation>Specification of a post office. Examples are a rural post office where post is delivered and a post office containing post office boxes.</xs:documentation>
+		</xs:annotation>
+		<xs:complexType>
+			<xs:sequence>
+				<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+				<xs:choice>
+					<xs:element name="PostOfficeName" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation>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.</xs:documentation>
+						</xs:annotation>
+						<xs:complexType mixed="true">
+							<xs:attribute name="Type"/>
+							<xs:attributeGroup ref="grPostal"/>
+							<xs:anyAttribute namespace="##other"/>
+						</xs:complexType>
+					</xs:element>
+					<xs:element name="PostOfficeNumber" minOccurs="0">
+						<xs:annotation>
+							<xs:documentation>Specification of the number of the postoffice. Common in rural postoffices</xs:documentation>
+						</xs:annotation>
+						<xs:complexType mixed="true">
+							<xs:attribute name="Indicator">
+								<xs:annotation>
+									<xs:documentation>MS in MS 62, # in MS # 12, etc.</xs:documentation>
+								</xs:annotation>
+							</xs:attribute>
+							<xs:attribute name="IndicatorOccurrence">
+								<xs:annotation>
+									<xs:documentation>MS occurs before 62 in MS 62</xs:documentation>
+								</xs:annotation>
+								<xs:simpleType>
+									<xs:restriction base="xs:NMTOKEN">
+										<xs:enumeration value="Before"/>
+										<xs:enumeration value="After"/>
+									</xs:restriction>
+								</xs:simpleType>
+							</xs:attribute>
+							<xs:attributeGroup ref="grPostal"/>
+							<xs:anyAttribute namespace="##other"/>
+						</xs:complexType>
+					</xs:element>
+				</xs:choice>
+				<xs:element name="PostalRoute" type="PostalRouteType" minOccurs="0">
+					<xs:annotation>
+						<xs:documentation>A Postal van is specific for a route as in Is`rael, Rural route</xs:documentation>
+					</xs:annotation>
+				</xs:element>
+				<xs:element ref="PostBox" minOccurs="0"/>
+				<xs:element ref="PostalCode" minOccurs="0"/>
+				<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+			</xs:sequence>
+			<xs:attribute name="Type">
+				<xs:annotation>
+					<xs:documentation>Could be a Mobile Postoffice Van as in Isreal</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:attribute name="Indicator">
+				<xs:annotation>
+					<xs:documentation>eg. Kottivakkam (P.O) here (P.O) is the Indicator</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:anyAttribute namespace="##other"/>
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="PostalCode">
+		<xs:annotation>
+			<xs:documentation>PostalCode is the container element for either simple or complex (extended) postal codes. Type: Area Code, Postcode, etc.</xs:documentation>
+		</xs:annotation>
+		<xs:complexType>
+			<xs:sequence>
+				<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+				<xs:element name="PostalCodeNumber" minOccurs="0" maxOccurs="unbounded">
+					<xs:annotation>
+						<xs:documentation>Specification of a postcode. The postcode is formatted according to country-specific rules. Example: SW3 0A8-1A, 600074, 2067</xs:documentation>
+					</xs:annotation>
+					<xs:complexType mixed="true">
+						<xs:attribute name="Type">
+							<xs:annotation>
+								<xs:documentation>Old Postal Code, new code, etc</xs:documentation>
+							</xs:annotation>
+						</xs:attribute>
+						<xs:attributeGroup ref="grPostal"/>
+						<xs:anyAttribute namespace="##other"/>
+					</xs:complexType>
+				</xs:element>
+				<xs:element name="PostalCodeNumberExtension" minOccurs="0" maxOccurs="unbounded">
+					<xs:annotation>
+						<xs:documentation>Examples are: 1234 (USA), 1G (UK), etc.</xs:documentation>
+					</xs:annotation>
+					<xs:complexType mixed="true">
+						<xs:attribute name="Type">
+							<xs:annotation>
+								<xs:documentation>Delivery Point Suffix, New Postal Code, etc..</xs:documentation>
+							</xs:annotation>
+						</xs:attribute>
+						<xs:attribute name="NumberExtensionSeparator">
+							<xs:annotation>
+								<xs:documentation>The separator between postal code number and the extension. Eg. "-"</xs:documentation>
+							</xs:annotation>
+						</xs:attribute>
+						<xs:attributeGroup ref="grPostal"/>
+						<xs:anyAttribute namespace="##other"/>
+					</xs:complexType>
+				</xs:element>
+				<xs:element name="PostTown" minOccurs="0">
+					<xs:annotation>
+						<xs:documentation>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".</xs:documentation>
+					</xs:annotation>
+					<xs:complexType>
+						<xs:sequence>
+							<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+							<xs:element name="PostTownName" minOccurs="0" maxOccurs="unbounded">
+								<xs:annotation>
+									<xs:documentation>Name of the post town</xs:documentation>
+								</xs:annotation>
+								<xs:complexType mixed="true">
+									<xs:attribute name="Type"/>
+									<xs:attributeGroup ref="grPostal"/>
+									<xs:anyAttribute namespace="##other"/>
+								</xs:complexType>
+							</xs:element>
+							<xs:element name="PostTownSuffix" minOccurs="0">
+								<xs:annotation>
+									<xs:documentation>GENERAL PO in MIAMI GENERAL PO</xs:documentation>
+								</xs:annotation>
+								<xs:complexType mixed="true">
+									<xs:attributeGroup ref="grPostal"/>
+									<xs:anyAttribute namespace="##other"/>
+								</xs:complexType>
+							</xs:element>
+						</xs:sequence>
+						<xs:attribute name="Type">
+							<xs:annotation>
+								<xs:documentation>eg. village, town, suburb, etc</xs:documentation>
+							</xs:annotation>
+						</xs:attribute>
+						<xs:anyAttribute namespace="##other"/>
+					</xs:complexType>
+				</xs:element>
+				<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+			</xs:sequence>
+			<xs:attribute name="Type">
+				<xs:annotation>
+					<xs:documentation>Area Code, Postcode, Delivery code as in NZ, etc</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:anyAttribute namespace="##other"/>
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="PostBox">
+		<xs:annotation>
+			<xs:documentation>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.</xs:documentation>
+		</xs:annotation>
+		<xs:complexType>
+			<xs:sequence>
+				<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+				<xs:element name="PostBoxNumber">
+					<xs:annotation>
+						<xs:documentation>Specification of the number of a postbox</xs:documentation>
+					</xs:annotation>
+					<xs:complexType mixed="true">
+						<xs:attributeGroup ref="grPostal"/>
+						<xs:anyAttribute namespace="##other"/>
+					</xs:complexType>
+				</xs:element>
+				<xs:element name="PostBoxNumberPrefix" minOccurs="0">
+					<xs:annotation>
+						<xs:documentation>Specification of the prefix of the post box number. eg. A in POBox:A-123</xs:documentation>
+					</xs:annotation>
+					<xs:complexType mixed="true">
+						<xs:attribute name="NumberPrefixSeparator">
+							<xs:annotation>
+								<xs:documentation>A-12 where 12 is number and A is prefix and "-" is the separator</xs:documentation>
+							</xs:annotation>
+						</xs:attribute>
+						<xs:attributeGroup ref="grPostal"/>
+						<xs:anyAttribute namespace="##other"/>
+					</xs:complexType>
+				</xs:element>
+				<xs:element name="PostBoxNumberSuffix" minOccurs="0">
+					<xs:annotation>
+						<xs:documentation>Specification of the suffix of the post box number. eg. A in POBox:123A</xs:documentation>
+					</xs:annotation>
+					<xs:complexType mixed="true">
+						<xs:attribute name="NumberSuffixSeparator">
+							<xs:annotation>
+								<xs:documentation>12-A where 12 is number and A is suffix and "-" is the separator</xs:documentation>
+							</xs:annotation>
+						</xs:attribute>
+						<xs:attributeGroup ref="grPostal"/>
+						<xs:anyAttribute namespace="##other"/>
+					</xs:complexType>
+				</xs:element>
+				<xs:element name="PostBoxNumberExtension" minOccurs="0">
+					<xs:annotation>
+						<xs:documentation>Some countries like USA have POBox as 12345-123</xs:documentation>
+					</xs:annotation>
+					<xs:complexType mixed="true">
+						<xs:attribute name="NumberExtensionSeparator">
+							<xs:annotation>
+								<xs:documentation>"-" is the NumberExtensionSeparator in POBOX:12345-123</xs:documentation>
+							</xs:annotation>
+						</xs:attribute>
+						<xs:anyAttribute namespace="##other"/>
+					</xs:complexType>
+				</xs:element>
+				<xs:element name="Firm" type="FirmType" minOccurs="0">
+					<xs:annotation>
+						<xs:documentation>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.</xs:documentation>
+					</xs:annotation>
+				</xs:element>
+				<xs:element ref="PostalCode" minOccurs="0"/>
+				<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+			</xs:sequence>
+			<xs:attribute name="Type">
+				<xs:annotation>
+					<xs:documentation>Possible values are, not limited to: POBox and Freepost.</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:attribute name="Indicator">
+				<xs:annotation>
+					<xs:documentation>LOCKED BAG NO:1234 where the Indicator is NO: and Type is LOCKED BAG</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:anyAttribute namespace="##other"/>
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="Department">
+		<xs:annotation>
+			<xs:documentation>Subdivision in the firm: School of Physics at Victoria University (School of Physics is the department)</xs:documentation>
+		</xs:annotation>
+		<xs:complexType>
+			<xs:sequence>
+				<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+				<xs:element name="DepartmentName" minOccurs="0" maxOccurs="unbounded">
+					<xs:annotation>
+						<xs:documentation>Specification of the name of a department.</xs:documentation>
+					</xs:annotation>
+					<xs:complexType mixed="true">
+						<xs:attribute name="Type"/>
+						<xs:attributeGroup ref="grPostal"/>
+						<xs:anyAttribute namespace="##other"/>
+					</xs:complexType>
+				</xs:element>
+				<xs:element name="MailStop" type="MailStopType" minOccurs="0">
+					<xs:annotation>
+						<xs:documentation>A MailStop is where the the mail is delivered to within a premise/subpremise/firm or a facility.</xs:documentation>
+					</xs:annotation>
+				</xs:element>
+				<xs:element ref="PostalCode" minOccurs="0"/>
+				<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+			</xs:sequence>
+			<xs:attribute name="Type">
+				<xs:annotation>
+					<xs:documentation>School in Physics School, Division in Radiology division of school of physics</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:anyAttribute namespace="##other"/>
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="Premise">
+		<xs:annotation>
+			<xs:documentation>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</xs:documentation>
+		</xs:annotation>
+		<xs:complexType>
+			<xs:sequence>
+				<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+				<xs:element name="PremiseName" minOccurs="0" maxOccurs="unbounded">
+					<xs:annotation>
+						<xs:documentation>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.</xs:documentation>
+					</xs:annotation>
+					<xs:complexType mixed="true">
+						<xs:attribute name="Type"/>
+						<xs:attribute name="TypeOccurrence">
+							<xs:annotation>
+								<xs:documentation>EGIS Building where EGIS occurs before Building, DES JARDINS occurs after COMPLEXE DES JARDINS</xs:documentation>
+							</xs:annotation>
+							<xs:simpleType>
+								<xs:restriction base="xs:NMTOKEN">
+									<xs:enumeration value="Before"/>
+									<xs:enumeration value="After"/>
+								</xs:restriction>
+							</xs:simpleType>
+						</xs:attribute>
+						<xs:attributeGroup ref="grPostal"/>
+						<xs:anyAttribute namespace="##other"/>
+					</xs:complexType>
+				</xs:element>
+				<xs:choice minOccurs="0">
+					<xs:element name="PremiseLocation">
+						<xs:annotation>
+							<xs:documentation>LOBBY, BASEMENT, GROUND FLOOR, etc...</xs:documentation>
+						</xs:annotation>
+						<xs:complexType mixed="true">
+							<xs:attributeGroup ref="grPostal"/>
+							<xs:anyAttribute namespace="##other"/>
+						</xs:complexType>
+					</xs:element>
+					<xs:choice>
+						<xs:element ref="PremiseNumber" maxOccurs="unbounded"/>
+						<xs:element name="PremiseNumberRange">
+							<xs:annotation>
+								<xs:documentation>Specification for defining the premise number range. Some premises have number as Building C1-C7</xs:documentation>
+							</xs:annotation>
+							<xs:complexType>
+								<xs:sequence>
+									<xs:element name="PremiseNumberRangeFrom">
+										<xs:annotation>
+											<xs:documentation>Start number details of the premise number range</xs:documentation>
+										</xs:annotation>
+										<xs:complexType>
+											<xs:sequence>
+												<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+												<xs:element ref="PremiseNumberPrefix" minOccurs="0" maxOccurs="unbounded"/>
+												<xs:element ref="PremiseNumber" maxOccurs="unbounded"/>
+												<xs:element ref="PremiseNumberSuffix" minOccurs="0" maxOccurs="unbounded"/>
+											</xs:sequence>
+										</xs:complexType>
+									</xs:element>
+									<xs:element name="PremiseNumberRangeTo">
+										<xs:annotation>
+											<xs:documentation>End number details of the premise number range</xs:documentation>
+										</xs:annotation>
+										<xs:complexType>
+											<xs:sequence>
+												<xs:element ref="AddressLine" minOccurs="0" maxOccurs="unbounded"/>
+												<xs:element ref="PremiseNumberPrefix" minOccurs="0" maxOccurs="unbounded"/>
+												<xs:element ref="PremiseNumber" maxOccurs="unbounded"/>
+												<xs:element ref="PremiseNumberSuffix" minOccurs="0" maxOccurs="unbounded"/>
+											</xs:sequence>
+										</xs:complexType>
+									</xs:element>
+								</xs:sequence>
+								<xs:attribute name="RangeType">
+									<xs:annotation>
+										<xs:documentation>Eg. Odd or even number range</xs:documentation>
+									</xs:annotation>
+								</xs:attribute>
+								<xs:attribute name="Indicator">
+									<xs:annotation>
+										<xs:documentation>Eg. No. in Building No:C1-C5</xs:documentation>
+									</xs:annotation>
+								</xs:attribute>
+								<xs:attribute name="Separator">
+									<xs:annotation>
+										<xs:documentation>"-" in 12-14  or "Thru" in 12 Thru 14 etc.</xs:documentation>
+									</xs:annotation>
+								</xs:attribute>
+								<xs:attribute name="Type"/>
+								<xs:attribute name="IndicatorOccurence">
+									<xs:annotation>
+										<xs:documentation>No.12-14 where "No." is before actual street number</xs:documentation>
+									</xs:annotation>
+									<xs:simpleType>
+										<xs:restriction base="xs:NMTOKEN">
+											<xs:enumeration value="Before"/>
+											<xs:enumeration value="After"/>
+										</xs:restriction>
+									</xs:simpleType>
+								</xs:attribute>
+								<xs:attribute name="NumberRangeOccurence">
+									<xs:annotation>
+										<xs:documentation>Building 23-25 where the number occurs after building name</xs:documentation>
+									</xs:annotation>
+									<xs:simpleType>
+										<xs:restriction base="xs:NMTOKEN">
+											<xs:enumeration value="BeforeName"/>
+											<xs:enumeration value="AfterName"/>
+											<xs:enumeration value="BeforeType"/>
+											<xs:enumeration value="AfterType"/>
+										</xs:restriction>
+									</xs:simpleType>
+								</xs:attribute>
+							</xs:complexType>
+						</xs:element>
+					</xs:choice>
+				</xs:choice>
+				<xs:element ref="PremiseNumberPrefix" minOccurs="0" maxOccurs="unbounded"/>
+				<xs:element ref="PremiseNumberSuffix" minOccurs="0" maxOccurs="unbounded"/>
+				<xs:element name="BuildingName" type="BuildingNameType" minOccurs="0" maxOccurs="unbounded">
+					<xs:annotation>
+						<xs:documentation>Specification of the name of a building.</xs:documentation>
+					</xs:annotation>
+				</xs:element>
+				<xs:choice>
+					<xs:element name="SubPremise" type="SubPremiseType" minOccurs="0" maxOccurs="unbounded">
+						<xs:annotation>
+							<xs:documentation>Specification of a single sub-premise. Examples of sub-premises are apartments and suites. Each sub-premise should be uniquely identifiable.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+					<xs:element name="Firm" type="FirmType" minOccurs="0">
+						<xs:annotation>
+							<xs:documentation>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.</xs:documentation>
+						</xs:annotation>
+					</xs:element>
+				</xs:choice>
+				<xs:element name="MailStop" type="MailStopType" minOccurs="0">
+					<xs:annotation>
+						<xs:documentation>A MailStop is where the the mail is delivered to within a premise/subpremise/firm or a facility.</xs:documentation>
+					</xs:annotation>
+				</xs:element>
+				<xs:element ref="PostalCode" minOccurs="0"/>
+				<xs:element ref="Premise" minOccurs="0"/>
+				<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+			</xs:sequence>
+			<xs:attribute name="Type">
+				<xs:annotation>
+					<xs:documentation>COMPLEXE in COMPLEX DES JARDINS, A building, station, etc</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:attribute name="PremiseDependency">
+				<xs:annotation>
+					<xs:documentation>STREET, PREMISE, SUBPREMISE, PARK, FARM, etc</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:attribute name="PremiseDependencyType">
+				<xs:annotation>
+					<xs:documentation>NEAR, ADJACENT TO, etc</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:attribute name="PremiseThoroughfareConnector">
+				<xs:annotation>
+					<xs:documentation>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</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:anyAttribute namespace="##other"/>
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="ThoroughfareNumberPrefix">
+		<xs:annotation>
+			<xs:documentation>Prefix before the number. A in A12 Archer Street</xs:documentation>
+		</xs:annotation>
+		<xs:complexType mixed="true">
+			<xs:annotation>
+				<xs:documentation>A-12 where 12 is number and A is prefix and "-" is the separator</xs:documentation>
+			</xs:annotation>
+			<xs:attribute name="NumberPrefixSeparator"/>
+			<xs:attribute name="Type"/>
+			<xs:attributeGroup ref="grPostal"/>
+			<xs:anyAttribute namespace="##other"/>
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="ThoroughfareNumberSuffix">
+		<xs:annotation>
+			<xs:documentation>Suffix after the number. A in 12A Archer Street</xs:documentation>
+		</xs:annotation>
+		<xs:complexType mixed="true">
+			<xs:attribute name="NumberSuffixSeparator">
+				<xs:annotation>
+					<xs:documentation>NEAR, ADJACENT TO, etc</xs:documentation>
+					<xs:documentation>12-A where 12 is number and A is suffix and "-" is the separator</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:attribute name="Type"/>
+			<xs:attributeGroup ref="grPostal"/>
+			<xs:anyAttribute namespace="##other"/>
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="ThoroughfareNumber">
+		<xs:annotation>
+			<xs:documentation>Eg.: 23 Archer street or 25/15 Zero Avenue, etc</xs:documentation>
+		</xs:annotation>
+		<xs:complexType mixed="true">
+			<xs:attribute name="NumberType">
+				<xs:annotation>
+					<xs:documentation>12 Archer Street is "Single" and 12-14 Archer Street is "Range"</xs:documentation>
+				</xs:annotation>
+				<xs:simpleType>
+					<xs:restriction base="xs:NMTOKEN">
+						<xs:enumeration value="Single"/>
+						<xs:enumeration value="Range"/>
+					</xs:restriction>
+				</xs:simpleType>
+			</xs:attribute>
+			<xs:attribute name="Type"/>
+			<xs:attribute name="Indicator">
+				<xs:annotation>
+					<xs:documentation>No. in Street No.12 or "#" in Street # 12, etc.</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:attribute name="IndicatorOccurrence">
+				<xs:annotation>
+					<xs:documentation>No.12 where "No." is before actual street number</xs:documentation>
+				</xs:annotation>
+				<xs:simpleType>
+					<xs:restriction base="xs:NMTOKEN">
+						<xs:enumeration value="Before"/>
+						<xs:enumeration value="After"/>
+					</xs:restriction>
+				</xs:simpleType>
+			</xs:attribute>
+			<xs:attribute name="NumberOccurrence">
+				<xs:annotation>
+					<xs:documentation>23 Archer St, Archer Street 23, St Archer 23</xs:documentation>
+				</xs:annotation>
+				<xs:simpleType>
+					<xs:restriction base="xs:NMTOKEN">
+						<xs:enumeration value="BeforeName"/>
+						<xs:enumeration value="AfterName"/>
+						<xs:enumeration value="BeforeType"/>
+						<xs:enumeration value="AfterType"/>
+					</xs:restriction>
+				</xs:simpleType>
+			</xs:attribute>
+			<xs:attributeGroup ref="grPostal"/>
+			<xs:anyAttribute namespace="##other"/>
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="PremiseNumber">
+		<xs:annotation>
+			<xs:documentation>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.</xs:documentation>
+		</xs:annotation>
+		<xs:complexType mixed="true">
+			<xs:attribute name="NumberType">
+				<xs:annotation>
+					<xs:documentation>Building 12-14 is "Range" and Building 12 is "Single"</xs:documentation>
+				</xs:annotation>
+				<xs:simpleType>
+					<xs:restriction base="xs:NMTOKEN">
+						<xs:enumeration value="Single"/>
+						<xs:enumeration value="Range"/>
+					</xs:restriction>
+				</xs:simpleType>
+			</xs:attribute>
+			<xs:attribute name="Type"/>
+			<xs:attribute name="Indicator">
+				<xs:annotation>
+					<xs:documentation>No. in House No.12, # in #12, etc.</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:attribute name="IndicatorOccurrence">
+				<xs:annotation>
+					<xs:documentation>No. occurs before 12 No.12</xs:documentation>
+				</xs:annotation>
+				<xs:simpleType>
+					<xs:restriction base="xs:NMTOKEN">
+						<xs:enumeration value="Before"/>
+						<xs:enumeration value="After"/>
+					</xs:restriction>
+				</xs:simpleType>
+			</xs:attribute>
+			<xs:attribute name="NumberTypeOccurrence">
+				<xs:annotation>
+					<xs:documentation>12 in BUILDING 12 occurs "after" premise type BUILDING</xs:documentation>
+				</xs:annotation>
+				<xs:simpleType>
+					<xs:restriction base="xs:NMTOKEN">
+						<xs:enumeration value="Before"/>
+						<xs:enumeration value="After"/>
+					</xs:restriction>
+				</xs:simpleType>
+			</xs:attribute>
+			<xs:attributeGroup ref="grPostal"/>
+			<xs:anyAttribute namespace="##other"/>
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="PremiseNumberPrefix">
+		<xs:annotation>
+			<xs:documentation>A in A12</xs:documentation>
+		</xs:annotation>
+		<xs:complexType>
+			<xs:simpleContent>
+				<xs:extension base="xs:string">
+					<xs:attribute name="NumberPrefixSeparator">
+						<xs:annotation>
+							<xs:documentation>A-12 where 12 is number and A is prefix and "-" is the separator</xs:documentation>
+						</xs:annotation>
+					</xs:attribute>
+					<xs:attribute name="Type"/>
+					<xs:attributeGroup ref="grPostal"/>
+					<xs:anyAttribute namespace="##other"/>
+				</xs:extension>
+			</xs:simpleContent>
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="PremiseNumberSuffix">
+		<xs:annotation>
+			<xs:documentation>A in 12A</xs:documentation>
+		</xs:annotation>
+		<xs:complexType mixed="true">
+			<xs:attribute name="NumberSuffixSeparator">
+				<xs:annotation>
+					<xs:documentation>12-A where 12 is number and A is suffix and "-" is the separator</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:attribute name="Type"/>
+			<xs:attributeGroup ref="grPostal"/>
+			<xs:anyAttribute namespace="##other"/>
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="CountryName">
+		<xs:annotation>
+			<xs:documentation>Specification of the name of a country.</xs:documentation>
+		</xs:annotation>
+		<xs:complexType mixed="true">
+			<xs:attribute name="Type">
+				<xs:annotation>
+					<xs:documentation>Old name, new name, etc</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:attributeGroup ref="grPostal"/>
+			<xs:anyAttribute namespace="##other"/>
+		</xs:complexType>
+	</xs:element>
+</xs:schema>
diff --git a/mapproxy/test/schemas/ows/1.1.0/ReadMe.txt b/mapproxy/test/schemas/ows/1.1.0/ReadMe.txt
new file mode 100644
index 0000000..45a2d86
--- /dev/null
+++ b/mapproxy/test/schemas/ows/1.1.0/ReadMe.txt
@@ -0,0 +1,87 @@
+OpenGIS(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
+	  <import> 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.
+
+-----------------------------------------------------------------------
+
diff --git a/mapproxy/test/schemas/ows/1.1.0/ows19115subset.xsd b/mapproxy/test/schemas/ows/1.1.0/ows19115subset.xsd
new file mode 100644
index 0000000..a2a3175
--- /dev/null
+++ b/mapproxy/test/schemas/ows/1.1.0/ows19115subset.xsd
@@ -0,0 +1,235 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/ows/1.1" 
+xmlns:ows="http://www.opengis.net/ows/1.1" 
+xmlns:xlink="http://www.w3.org/1999/xlink" 
+xmlns="http://www.w3.org/2001/XMLSchema" 
+elementFormDefault="qualified" version="1.1.0 2010-01-30" xml:lang="en">
+	<annotation>
+		<appinfo>ows19115subset.xsd 2010-01-30</appinfo>
+		<documentation>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  [...]
+		
+		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/ .
+		</documentation>
+	</annotation>
+	<!-- ==============================================================
+		includes and imports
+	============================================================== -->
+	<import namespace="http://www.w3.org/1999/xlink" schemaLocation="../../xlink/1.0.0/xlinks.xsd"/>
+	<import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="../../xml.xsd"/>
+	<!-- ==============================================================
+		elements and types
+	============================================================== -->
+	<complexType name="LanguageStringType">
+		<annotation>
+			<documentation>Text string with the language of the string identified as recommended in the XML 1.0 W3C Recommendation, section 2.12. </documentation>
+		</annotation>
+		<simpleContent>
+			<extension base="string">
+				<attribute ref="xml:lang" use="optional"/>
+			</extension>
+		</simpleContent>
+	</complexType>
+	<!-- =========================================================== -->
+	<element name="Title" type="ows:LanguageStringType">
+		<annotation>
+			<documentation>Title of this resource, normally used for display to a human. </documentation>
+		</annotation>
+	</element>
+	<!-- =========================================================== -->
+	<element name="Abstract" type="ows:LanguageStringType">
+		<annotation>
+			<documentation>Brief narrative description of this resource, normally used for display to a human. </documentation>
+		</annotation>
+	</element>
+	<!-- =========================================================== -->
+	<element name="Keywords" type="ows:KeywordsType"/>
+	<!-- =========================================================== -->
+	<complexType name="KeywordsType">
+		<annotation>
+			<documentation>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. </documentation>
+			<documentation>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. </documentation>
+		</annotation>
+		<sequence>
+			<element name="Keyword" type="ows:LanguageStringType" maxOccurs="unbounded"/>
+			<element name="Type" type="ows:CodeType" minOccurs="0"/>
+		</sequence>
+	</complexType>
+	<!-- =========================================================== -->
+	<complexType name="CodeType">
+		<annotation>
+			<documentation>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. </documentation>
+			<documentation>Type copied from basicTypes.xsd of GML 3 with documentation edited, for possible use outside the ServiceIdentification section of a service metadata document. </documentation>
+		</annotation>
+		<simpleContent>
+			<extension base="string">
+				<attribute name="codeSpace" type="anyURI" use="optional"/>
+			</extension>
+		</simpleContent>
+	</complexType>
+	<!-- =========================================================== -->
+	<element name="PointOfContact" type="ows:ResponsiblePartyType">
+		<annotation>
+			<documentation>Identification of, and means of communication with, person(s) responsible for the resource(s). </documentation>
+			<documentation>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 [...]
+		</annotation>
+	</element>
+	<!-- =========================================================== -->
+	<complexType name="ResponsiblePartyType">
+		<annotation>
+			<documentation>Identification of, and means of communication with, person responsible for the server. At least one of IndividualName, OrganisationName, or PositionName shall be included. </documentation>
+		</annotation>
+		<sequence>
+			<element ref="ows:IndividualName" minOccurs="0"/>
+			<element ref="ows:OrganisationName" minOccurs="0"/>
+			<element ref="ows:PositionName" minOccurs="0"/>
+			<element ref="ows:ContactInfo" minOccurs="0"/>
+			<element ref="ows:Role"/>
+		</sequence>
+	</complexType>
+	<!-- =========================================================== -->
+	<!-- =========================================================== -->
+	<complexType name="ResponsiblePartySubsetType">
+		<annotation>
+			<documentation>Identification of, and means of communication with, person responsible for the server. </documentation>
+			<documentation>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. </documentation>
+		</annotation>
+		<sequence>
+			<element ref="ows:IndividualName" minOccurs="0"/>
+			<element ref="ows:PositionName" minOccurs="0"/>
+			<element ref="ows:ContactInfo" minOccurs="0"/>
+			<element ref="ows:Role" minOccurs="0"/>
+		</sequence>
+	</complexType>
+	<!-- =========================================================== -->
+	<element name="IndividualName" type="string">
+		<annotation>
+			<documentation>Name of the responsible person: surname, given name, title separated by a delimiter. </documentation>
+		</annotation>
+	</element>
+	<!-- =========================================================== -->
+	<element name="OrganisationName" type="string">
+		<annotation>
+			<documentation>Name of the responsible organization. </documentation>
+		</annotation>
+	</element>
+	<!-- =========================================================== -->
+	<element name="PositionName" type="string">
+		<annotation>
+			<documentation>Role or position of the responsible person. </documentation>
+		</annotation>
+	</element>
+	<!-- =========================================================== -->
+	<element name="Role" type="ows:CodeType">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+	</element>
+	<!-- =========================================================== -->
+	<element name="ContactInfo" type="ows:ContactType">
+		<annotation>
+			<documentation>Address of the responsible party. </documentation>
+		</annotation>
+	</element>
+	<!-- =========================================================== -->
+	<complexType name="ContactType">
+		<annotation>
+			<documentation>Information required to enable contact with the responsible person and/or organization. </documentation>
+			<documentation>For OWS use in the service metadata document, the optional hoursOfService and contactInstructions elements were retained, as possibly being useful in the ServiceProvider section. </documentation>
+		</annotation>
+		<sequence>
+			<element name="Phone" type="ows:TelephoneType" minOccurs="0">
+				<annotation>
+					<documentation>Telephone numbers at which the organization or individual may be contacted. </documentation>
+				</annotation>
+			</element>
+			<element name="Address" type="ows:AddressType" minOccurs="0">
+				<annotation>
+					<documentation>Physical and email address at which the organization or individual may be contacted. </documentation>
+				</annotation>
+			</element>
+			<element name="OnlineResource" type="ows:OnlineResourceType" minOccurs="0">
+				<annotation>
+					<documentation>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 [...]
+				</annotation>
+			</element>
+			<element name="HoursOfService" type="string" minOccurs="0">
+				<annotation>
+					<documentation>Time period (including time zone) when individuals can contact the organization or individual. </documentation>
+				</annotation>
+			</element>
+			<element name="ContactInstructions" type="string" minOccurs="0">
+				<annotation>
+					<documentation>Supplemental instructions on how or when to contact the individual or organization. </documentation>
+				</annotation>
+			</element>
+		</sequence>
+	</complexType>
+	<!-- =========================================================== -->
+	<complexType name="OnlineResourceType">
+		<annotation>
+			<documentation>Reference to on-line resource from which data can be obtained. </documentation>
+			<documentation>For OWS use in the service metadata document, the CI_OnlineResource class was XML encoded as the attributeGroup "xlink:simpleLink", as used in GML. </documentation>
+		</annotation>
+		<attributeGroup ref="xlink:simpleLink"/>
+	</complexType>
+	<!-- ========================================================== -->
+	<complexType name="TelephoneType">
+		<annotation>
+			<documentation>Telephone numbers for contacting the responsible individual or organization. </documentation>
+		</annotation>
+		<sequence>
+			<element name="Voice" type="string" minOccurs="0" maxOccurs="unbounded">
+				<annotation>
+					<documentation>Telephone number by which individuals can speak to the responsible organization or individual. </documentation>
+				</annotation>
+			</element>
+			<element name="Facsimile" type="string" minOccurs="0" maxOccurs="unbounded">
+				<annotation>
+					<documentation>Telephone number of a facsimile machine for the responsible
+organization or individual. </documentation>
+				</annotation>
+			</element>
+		</sequence>
+	</complexType>
+	<!-- =========================================================== -->
+	<complexType name="AddressType">
+		<annotation>
+			<documentation>Location of the responsible individual or organization. </documentation>
+		</annotation>
+		<sequence>
+			<element name="DeliveryPoint" type="string" minOccurs="0" maxOccurs="unbounded">
+				<annotation>
+					<documentation>Address line for the location. </documentation>
+				</annotation>
+			</element>
+			<element name="City" type="string" minOccurs="0">
+				<annotation>
+					<documentation>City of the location. </documentation>
+				</annotation>
+			</element>
+			<element name="AdministrativeArea" type="string" minOccurs="0">
+				<annotation>
+					<documentation>State or province of the location. </documentation>
+				</annotation>
+			</element>
+			<element name="PostalCode" type="string" minOccurs="0">
+				<annotation>
+					<documentation>ZIP or other postal code. </documentation>
+				</annotation>
+			</element>
+			<element name="Country" type="string" minOccurs="0">
+				<annotation>
+					<documentation>Country of the physical address. </documentation>
+				</annotation>
+			</element>
+			<element name="ElectronicMailAddress" type="string" minOccurs="0" maxOccurs="unbounded">
+				<annotation>
+					<documentation>Address of the electronic mailbox of the responsible organization or individual. </documentation>
+				</annotation>
+			</element>
+		</sequence>
+	</complexType>
+</schema>
diff --git a/mapproxy/test/schemas/ows/1.1.0/owsAll.xsd b/mapproxy/test/schemas/ows/1.1.0/owsAll.xsd
new file mode 100644
index 0000000..4cb1719
--- /dev/null
+++ b/mapproxy/test/schemas/ows/1.1.0/owsAll.xsd
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/ows/1.1" 
+xmlns:ows="http://www.opengis.net/ows/1.1" 
+xmlns="http://www.w3.org/2001/XMLSchema" 
+elementFormDefault="qualified" version="1.1.0 2010-01-30" xml:lang="en">
+	<annotation>
+		<appinfo>owsAll.xsd 2010-01-30</appinfo>
+		<documentation>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/ .
+		</documentation>
+	</annotation>
+	<!-- ==============================================================
+		includes and imports
+	============================================================== -->
+	<include schemaLocation="owsGetResourceByID.xsd"/>
+	<include schemaLocation="owsExceptionReport.xsd"/>
+	<include schemaLocation="owsDomainType.xsd"/>
+	<include schemaLocation="owsContents.xsd"/>
+	<include schemaLocation="owsInputOutputData.xsd"/>
+</schema>
diff --git a/mapproxy/test/schemas/ows/1.1.0/owsCommon.xsd b/mapproxy/test/schemas/ows/1.1.0/owsCommon.xsd
new file mode 100644
index 0000000..8db5b72
--- /dev/null
+++ b/mapproxy/test/schemas/ows/1.1.0/owsCommon.xsd
@@ -0,0 +1,157 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/ows/1.1" 
+xmlns:ows="http://www.opengis.net/ows/1.1" 
+xmlns:xlink="http://www.w3.org/1999/xlink" 
+xmlns="http://www.w3.org/2001/XMLSchema" 
+elementFormDefault="qualified" version="1.1.0 2010-01-30" xml:lang="en">
+	<annotation>
+		<appinfo>owsCommon.xsd 2010-01-30</appinfo>
+		<documentation>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/ .
+		</documentation>
+	</annotation>
+	<!-- ==============================================================
+		includes and imports
+	============================================================== -->
+	<import namespace="http://www.w3.org/1999/xlink" schemaLocation="../../xlink/1.0.0/xlinks.xsd"/>
+	<!-- ==============================================================
+		elements and types
+	============================================================== -->
+	<simpleType name="MimeType">
+		<annotation>
+			<documentation>XML encoded identifier of a standard MIME type, possibly a parameterized MIME type. </documentation>
+		</annotation>
+		<restriction base="string">
+			<pattern value="(application|audio|image|text|video|message|multipart|model)/.+(;\s*.+=.+)*"/>
+		</restriction>
+	</simpleType>
+	<!-- ========================================================= -->
+	<simpleType name="VersionType">
+		<annotation>
+			<documentation>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  [...]
+		</annotation>
+		<restriction base="string">
+			<pattern value="\d+\.\d?\d\.\d?\d"/>
+		</restriction>
+	</simpleType>
+	<!-- ========================================================== -->
+	<element name="Metadata" type="ows:MetadataType"/>
+	<!-- ========================================================== -->
+	<complexType name="MetadataType">
+		<annotation>
+			<documentation>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 m [...]
+		</annotation>
+		<sequence>
+			<element ref="ows:AbstractMetaData" minOccurs="0"/>
+		</sequence>
+		<attributeGroup ref="xlink:simpleLink">
+			<annotation>
+				<documentation>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. </documentation>
+			</annotation>
+		</attributeGroup>
+		<attribute name="about" type="anyURI" use="optional">
+			<annotation>
+				<documentation>Optional reference to the aspect of the element which includes this "metadata" element that this metadata provides more information about. </documentation>
+			</annotation>
+		</attribute>
+	</complexType>
+	<!-- ========================================================== -->
+	<element name="AbstractMetaData" abstract="true">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+	</element>
+	<!-- ========================================================== -->
+	<!-- ========================================================== -->
+	<element name="BoundingBox" type="ows:BoundingBoxType"/>
+	<!-- =========================================================== -->
+	<complexType name="BoundingBoxType">
+		<annotation>
+			<documentation>XML encoded minimum rectangular bounding box (or region) parameter, surrounding all the associated data. </documentation>
+			<documentation>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. </documentation>
+		</annotation>
+		<sequence>
+			<element name="LowerCorner" type="ows:PositionType">
+				<annotation>
+					<documentation>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. </documentation>
+				</annotation>
+			</element>
+			<element name="UpperCorner" type="ows:PositionType">
+				<annotation>
+					<documentation>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. </documentation>
+				</annotation>
+			</element>
+		</sequence>
+		<attribute name="crs" type="anyURI" use="optional">
+			<annotation>
+				<documentation>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. </documentation>
+			</annotation>
+		</attribute>
+		<attribute name="dimensions" type="positiveInteger" use="optional">
+			<annotation>
+				<documentation>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. </documentation>
+			</annotation>
+		</attribute>
+	</complexType>
+	<!-- =========================================================== -->
+	<simpleType name="PositionType">
+		<annotation>
+			<documentation>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. </documentation>
+			<documentation>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. </documentation>
+		</annotation>
+		<list itemType="double"/>
+	</simpleType>
+	<!-- =========================================================== -->
+	<element name="WGS84BoundingBox" type="ows:WGS84BoundingBoxType" substitutionGroup="ows:BoundingBox"/>
+	<!-- =========================================================== -->
+	<complexType name="WGS84BoundingBoxType">
+		<annotation>
+			<documentation>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. </documentation>
+			<documentation>This type is adapted from the general BoundingBoxType, with modified contents and documentation for use with the 2D WGS 84 coordinate reference system. </documentation>
+		</annotation>
+		<complexContent>
+			<restriction base="ows:BoundingBoxType">
+				<sequence>
+					<element name="LowerCorner" type="ows:PositionType2D">
+						<annotation>
+							<documentation>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. </documentation>
+						</annotation>
+					</element>
+					<element name="UpperCorner" type="ows:PositionType2D">
+						<annotation>
+							<documentation>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. </documentation>
+						</annotation>
+					</element>
+				</sequence>
+				<attribute name="crs" type="anyURI" use="optional" fixed="urn:ogc:def:crs:OGC:2:84">
+					<annotation>
+						<documentation>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. </documentation>
+					</annotation>
+				</attribute>
+				<attribute name="dimensions" type="positiveInteger" use="optional" fixed="2">
+					<annotation>
+						<documentation>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. </documentation>
+					</annotation>
+				</attribute>
+			</restriction>
+		</complexContent>
+	</complexType>
+	<!-- =========================================================== -->
+	<simpleType name="PositionType2D">
+		<annotation>
+			<documentation>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 m [...]
+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. </documentation>
+		</annotation>
+		<restriction base="ows:PositionType">
+			<length value="2"/>
+		</restriction>
+	</simpleType>
+</schema>
diff --git a/mapproxy/test/schemas/ows/1.1.0/owsContents.xsd b/mapproxy/test/schemas/ows/1.1.0/owsContents.xsd
new file mode 100644
index 0000000..77b6c84
--- /dev/null
+++ b/mapproxy/test/schemas/ows/1.1.0/owsContents.xsd
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/ows/1.1"
+xmlns:ows="http://www.opengis.net/ows/1.1"
+xmlns:xlink="http://www.w3.org/1999/xlink"
+xmlns="http://www.w3.org/2001/XMLSchema"
+elementFormDefault="qualified" version="1.1.0 2010-01-30" xml:lang="en">
+	<annotation>
+		<appinfo>owsContents.xsd 2010-01-30</appinfo>
+		<documentation>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/ .
+		</documentation>
+	</annotation>
+	<!-- ==============================================================
+		includes and imports
+	============================================================== -->
+	<include schemaLocation="owsDataIdentification.xsd"/>
+	<!-- ==============================================================
+		elements and types
+	============================================================== -->
+	<complexType name="ContentsBaseType">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+		<sequence>
+			<element ref="ows:DatasetDescriptionSummary" minOccurs="0" maxOccurs="unbounded">
+				<annotation>
+					<documentation>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. </documentation>
+				</annotation>
+			</element>
+			<element ref="ows:OtherSource" minOccurs="0" maxOccurs="unbounded">
+				<annotation>
+					<documentation>Unordered set of references to other sources of metadata describing the coverage offerings available from this server. </documentation>
+				</annotation>
+			</element>
+		</sequence>
+	</complexType>
+	<!-- ===========================================================-->
+	<element name="OtherSource" type="ows:MetadataType">
+		<annotation>
+			<documentation>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 s [...]
+		</annotation>
+	</element>
+	<!-- ===========================================================-->
+	<element name="DatasetDescriptionSummary" type="ows:DatasetDescriptionSummaryBaseType"/>
+	<!-- ===========================================================-->
+	<complexType name="DatasetDescriptionSummaryBaseType">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+		<complexContent>
+			<extension base="ows:DescriptionType">
+				<sequence>
+					<element ref="ows:WGS84BoundingBox" minOccurs="0" maxOccurs="unbounded">
+						<annotation>
+							<documentation>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 igno [...]
+						</annotation>
+					</element>
+					<element name="Identifier" type="ows:CodeType">
+						<annotation>
+							<documentation>Unambiguous identifier or name of this coverage, unique for this server. </documentation>
+						</annotation>
+					</element>
+					<element ref="ows:BoundingBox" minOccurs="0" maxOccurs="unbounded">
+						<annotation>
+							<documentation>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, an [...]
+						</annotation>
+					</element>
+					<element ref="ows:Metadata" minOccurs="0" maxOccurs="unbounded">
+						<annotation>
+							<documentation>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. </documentation>
+						</annotation>
+					</element>
+					<element ref="ows:DatasetDescriptionSummary" minOccurs="0" maxOccurs="unbounded">
+						<annotation>
+							<documentation>Metadata describing zero or more unordered subsidiary datasets available from this server. </documentation>
+						</annotation>
+					</element>
+				</sequence>
+			</extension>
+		</complexContent>
+	</complexType>
+	<!-- ===========================================================-->
+</schema>
diff --git a/mapproxy/test/schemas/ows/1.1.0/owsDataIdentification.xsd b/mapproxy/test/schemas/ows/1.1.0/owsDataIdentification.xsd
new file mode 100644
index 0000000..dde83e1
--- /dev/null
+++ b/mapproxy/test/schemas/ows/1.1.0/owsDataIdentification.xsd
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/ows/1.1" 
+xmlns:ows="http://www.opengis.net/ows/1.1" 
+xmlns:xlink="http://www.w3.org/1999/xlink" 
+xmlns="http://www.w3.org/2001/XMLSchema" 
+elementFormDefault="qualified" version="1.1.0 2010-01-30" xml:lang="en">
+	<annotation>
+		<appinfo>owsDataIdentification.xsd 2010-01-30</appinfo>
+		<documentation>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/ .
+		</documentation>
+	</annotation>
+	<!-- ==============================================================
+		includes and imports
+	============================================================== -->
+	<include schemaLocation="owsCommon.xsd"/>
+	<include schemaLocation="ows19115subset.xsd"/>
+	<!-- ==============================================================
+		elements and types
+	============================================================== -->
+	<complexType name="DescriptionType">
+		<annotation>
+			<documentation>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. </doc [...]
+		</annotation>
+		<sequence>
+			<element ref="ows:Title" minOccurs="0" maxOccurs="unbounded"/>
+			<element ref="ows:Abstract" minOccurs="0" maxOccurs="unbounded"/>
+			<element ref="ows:Keywords" minOccurs="0" maxOccurs="unbounded"/>
+		</sequence>
+	</complexType>
+	<!-- ========================================================= -->
+	<complexType name="BasicIdentificationType">
+		<annotation>
+			<documentation>Basic metadata identifying and describing a set of data. </documentation>
+		</annotation>
+		<complexContent>
+			<extension base="ows:DescriptionType">
+				<sequence>
+					<element ref="ows:Identifier" minOccurs="0">
+						<annotation>
+							<documentation>Optional unique identifier or name of this dataset. </documentation>
+						</annotation>
+					</element>
+					<element ref="ows:Metadata" minOccurs="0" maxOccurs="unbounded">
+						<annotation>
+							<documentation>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. </documentation>
+						</annotation>
+					</element>
+				</sequence>
+			</extension>
+		</complexContent>
+	</complexType>
+	<!-- ========================================================= -->
+	<complexType name="IdentificationType">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+		<complexContent>
+			<extension base="ows:BasicIdentificationType">
+				<sequence>
+					<element ref="ows:BoundingBox" minOccurs="0" maxOccurs="unbounded">
+						<annotation>
+							<documentation>Unordered list of zero or more bounding boxes whose union describes the extent of this dataset. </documentation>
+						</annotation>
+					</element>
+					<element ref="ows:OutputFormat" minOccurs="0" maxOccurs="unbounded">
+						<annotation>
+							<documentation>Unordered list of zero or more references to data formats supported for server outputs. </documentation>
+						</annotation>
+					</element>
+					<element ref="ows:AvailableCRS" minOccurs="0" maxOccurs="unbounded">
+						<annotation>
+							<documentation>Unordered list of zero or more available coordinate reference systems. </documentation>
+						</annotation>
+					</element>
+				</sequence>
+			</extension>
+		</complexContent>
+	</complexType>
+	<!-- ===========================================================-->
+	<element name="Identifier" type="ows:CodeType">
+		<annotation>
+			<documentation>Unique identifier or name of this dataset. </documentation>
+		</annotation>
+	</element>
+	<!-- ===========================================================-->
+	<element name="OutputFormat" type="ows:MimeType">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+	</element>
+	<!-- ===========================================================-->
+	<element name="AvailableCRS" type="anyURI"/>
+	<element name="SupportedCRS" type="anyURI" substitutionGroup="ows:AvailableCRS">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+	</element>
+	<!-- ==========================================================
+	The following elements could be added to the IdentificationType when useful for a 
+	specific OWS. In addition the PointOfContact element in ows19115subset.xsd could 
+	be added.
+	============================================================= -->
+	<element name="AccessConstraints" type="string">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+	</element>
+	<!-- ========================================================== -->
+	<element name="Fees" type="string">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+	</element>
+	<!-- ========================================================== -->
+	<element name="Language" type="language">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+	</element>
+	<!-- ========================================================== -->
+</schema>
diff --git a/mapproxy/test/schemas/ows/1.1.0/owsDomainType.xsd b/mapproxy/test/schemas/ows/1.1.0/owsDomainType.xsd
new file mode 100644
index 0000000..4d0a54c
--- /dev/null
+++ b/mapproxy/test/schemas/ows/1.1.0/owsDomainType.xsd
@@ -0,0 +1,279 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/ows/1.1" 
+xmlns:ows="http://www.opengis.net/ows/1.1" 
+xmlns:xlink="http://www.w3.org/1999/xlink" 
+xmlns="http://www.w3.org/2001/XMLSchema" 
+elementFormDefault="qualified" version="1.1.0 2010-01-30" xml:lang="en">
+	<annotation>
+		<appinfo>owsDomainType.xsd 2010-01-30</appinfo>
+		<documentation>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/ .
+		</documentation>
+	</annotation>
+	<!-- ==============================================================
+		includes and imports
+	============================================================== -->
+	<include schemaLocation="owsCommon.xsd"></include>
+	<import namespace="http://www.w3.org/1999/xlink" schemaLocation="../../xlink/1.0.0/xlinks.xsd"></import>
+	<!-- ==============================================================
+		elements and types
+	============================================================== -->
+	<complexType name="DomainType">
+		<annotation>
+			<documentation>Valid domain (or allowed set of values) of one quantity, with its name or identifier. </documentation>
+		</annotation>
+		<complexContent>
+			<extension base="ows:UnNamedDomainType">
+				<attribute name="name" type="string" use="required">
+					<annotation>
+						<documentation>Name or identifier of this quantity. </documentation>
+					</annotation>
+				</attribute>
+			</extension>
+		</complexContent>
+	</complexType>
+	<!-- ========================================================== -->
+	<complexType name="UnNamedDomainType">
+		<annotation>
+			<documentation>Valid domain (or allowed set of values) of one quantity, with needed metadata but without a quantity name or identifier. </documentation>
+		</annotation>
+		<sequence>
+			<group ref="ows:PossibleValues"/>
+			<element ref="ows:DefaultValue" minOccurs="0">
+				<annotation>
+					<documentation>Optional default value for this quantity, which should be included when this quantity has a default value. </documentation>
+				</annotation>
+			</element>
+			<element ref="ows:Meaning" minOccurs="0">
+				<annotation>
+					<documentation>Meaning metadata should be referenced or included for each quantity. </documentation>
+				</annotation>
+			</element>
+			<element ref="ows:DataType" minOccurs="0">
+				<annotation>
+					<documentation>This data type metadata should be referenced or included for each quantity. </documentation>
+				</annotation>
+			</element>
+			<group ref="ows:ValuesUnit" minOccurs="0">
+				<annotation>
+					<documentation>Unit of measure, which should be included when this set of PossibleValues has units or a more complete reference system. </documentation>
+				</annotation>
+			</group>
+			<element ref="ows:Metadata" minOccurs="0" maxOccurs="unbounded">
+				<annotation>
+					<documentation>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. </documentation>
+				</annotation>
+			</element>
+		</sequence>
+	</complexType>
+	<!-- ========================================================== -->
+	<group name="PossibleValues">
+		<annotation>
+			<documentation>Specifies the possible values of this quantity. </documentation>
+		</annotation>
+			<choice>
+				<element ref="ows:AllowedValues"/>
+				<element ref="ows:AnyValue"/>
+				<element ref="ows:NoValues"/>
+				<element ref="ows:ValuesReference"/>
+			</choice>
+	</group>
+	<!-- ========================================================== -->
+	<element name="AnyValue">
+		<annotation>
+			<documentation>Specifies that any value is allowed for this parameter.</documentation>
+		</annotation>
+		<complexType></complexType>
+	</element>
+	<!-- ========================================================== -->
+	<element name="NoValues">
+		<annotation>
+			<documentation>Specifies that no values are allowed for this parameter or quantity.</documentation>
+		</annotation>
+		<complexType></complexType>
+	</element>
+	<!-- ========================================================== -->
+	<element name="ValuesReference">
+		<annotation>
+			<documentation>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.) </documentation>
+		</annotation>
+		<complexType>
+			<simpleContent>
+				<extension base="string">
+					<annotation>
+						<documentation>Human-readable name of the list of values provided by the referenced document. Can be empty string when this list has no name. </documentation>
+					</annotation>
+					<attribute ref="ows:reference" use="required">
+					</attribute>
+				</extension>
+			</simpleContent>
+		</complexType>
+	</element>
+	<!-- ========================================================== -->
+	<group name="ValuesUnit">
+		<annotation>
+			<documentation>Indicates that this quantity has units or a reference system, and identifies the unit or reference system used by the AllowedValues or ValuesReference. </documentation>
+		</annotation>
+			<choice>
+				<element ref="ows:UOM">
+					<annotation>
+						<documentation>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). </documentation>
+					</annotation>
+				</element>
+				<element ref="ows:ReferenceSystem">
+					<annotation>
+						<documentation>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). </documentation>
+					</annotation>
+				</element>
+			</choice>
+	</group>
+	<!-- ========================================================== -->
+	<!-- ========================================================== -->
+	<element name="AllowedValues">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+		<complexType>
+			<choice maxOccurs="unbounded">
+				<element ref="ows:Value"/>
+				<element ref="ows:Range"/>
+			</choice>
+		</complexType>
+	</element>
+	<!-- ========================================================== -->
+	<element name="Value" type="ows:ValueType"></element>
+	<!-- ========================================================== -->
+	<complexType name="ValueType">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+		<simpleContent>
+			<extension base="string"></extension>
+		</simpleContent>
+	</complexType>
+	<!-- ========================================================== -->
+	<element name="DefaultValue" type="ows:ValueType">
+		<annotation>
+			<documentation>The default value for a quantity for which multiple values are allowed. </documentation>
+		</annotation>
+	</element>
+	<!-- ========================================================== -->
+	<element name="Range" type="ows:RangeType"></element>
+	<!-- ========================================================== -->
+	<complexType name="RangeType">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+		<sequence>
+			<element ref="ows:MinimumValue" minOccurs="0"></element>
+			<element ref="ows:MaximumValue" minOccurs="0"></element>
+			<element ref="ows:Spacing" minOccurs="0">
+				<annotation>
+					<documentation>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. </documentation>
+				</annotation>
+			</element>
+		</sequence>
+		<attribute ref="ows:rangeClosure" use="optional">
+			<annotation>
+				<documentation>Shall be included unless the default value applies. </documentation>
+			</annotation>
+		</attribute>
+	</complexType>
+	<!-- ========================================================== -->
+	<element name="MinimumValue" type="ows:ValueType">
+		<annotation>
+			<documentation>Minimum value of this numeric parameter. </documentation>
+		</annotation>
+	</element>
+	<!-- ========================================================== -->
+	<element name="MaximumValue" type="ows:ValueType">
+		<annotation>
+			<documentation>Maximum value of this numeric parameter. </documentation>
+		</annotation>
+	</element>
+	<!-- ========================================================== -->
+	<element name="Spacing" type="ows:ValueType">
+		<annotation>
+			<documentation>The regular distance or spacing between the allowed values in a range. </documentation>
+		</annotation>
+	</element>
+	<!-- ========================================================== -->
+	<attribute name="rangeClosure" default="closed">
+		<annotation>
+			<documentation>Specifies which of the minimum and maximum values are included in the range. Note that plus and minus infinity are considered closed bounds. </documentation>
+		</annotation>
+		<simpleType>
+			<restriction base="NMTOKENS">
+				<enumeration value="closed">
+					<annotation>
+						<documentation>The specified minimum and maximum values are included in this range. </documentation>
+					</annotation>
+				</enumeration>
+				<enumeration value="open">
+					<annotation>
+						<documentation>The specified minimum and maximum values are NOT included in this range. </documentation>
+					</annotation>
+				</enumeration>
+				<enumeration value="open-closed">
+					<annotation>
+						<documentation>The specified minimum value is NOT included in this range, and the specified maximum value IS included in this range. </documentation>
+					</annotation>
+				</enumeration>
+				<enumeration value="closed-open">
+					<annotation>
+						<documentation>The specified minimum value IS included in this range, and the specified maximum value is NOT included in this range. </documentation>
+					</annotation>
+				</enumeration>
+			</restriction>
+		</simpleType>
+	</attribute>
+	<!-- ========================================================== -->
+	<!-- ========================================================== -->
+	<complexType name="DomainMetadataType">
+		<annotation>
+			<documentation>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.) </documentation>
+		</annotation>
+		<simpleContent>
+			<extension base="string">
+				<annotation>
+					<documentation>Human-readable name of the metadata described by associated referenced document. </documentation>
+				</annotation>
+				<attribute ref="ows:reference" use="optional"/>
+			</extension>
+		</simpleContent>
+	</complexType>
+	<!-- ========================================================== -->
+	<attribute name="reference" type="anyURI">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+	</attribute>
+	<!-- ========================================================== -->
+	<element name="Meaning" type="ows:DomainMetadataType">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+	</element>
+	<!-- ========================================================== -->
+	<element name="DataType" type="ows:DomainMetadataType">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+	</element>
+	<!-- ========================================================== -->
+	<element name="ReferenceSystem" type="ows:DomainMetadataType">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+	</element>
+	<!-- ========================================================== -->
+	<element name="UOM" type="ows:DomainMetadataType">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+	</element>
+	<!-- ========================================================== -->
+</schema>
diff --git a/mapproxy/test/schemas/ows/1.1.0/owsExceptionReport.xsd b/mapproxy/test/schemas/ows/1.1.0/owsExceptionReport.xsd
new file mode 100644
index 0000000..cd5d8ca
--- /dev/null
+++ b/mapproxy/test/schemas/ows/1.1.0/owsExceptionReport.xsd
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/ows/1.1" 
+xmlns:ows="http://www.opengis.net/ows/1.1" 
+xmlns="http://www.w3.org/2001/XMLSchema" 
+elementFormDefault="qualified" version="1.1.0 2011-02-07" xml:lang="en">
+	<annotation>
+		<appinfo>owsExceptionReport.xsd 2011-02-07</appinfo>
+		<documentation>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/ .
+		</documentation>
+	</annotation>
+	<!-- ==============================================================
+		includes and imports
+	============================================================== -->
+	<import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="../../xml.xsd"/>
+	<!-- ==============================================================
+		elements and types
+	============================================================== -->
+	<element name="ExceptionReport">
+		<annotation>
+			<documentation>Report message returned to the client that requested any OWS operation when the server detects an error while processing that operation request. </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="ows:Exception" maxOccurs="unbounded">
+					<annotation>
+						<documentation>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). </documentation>
+					</annotation>
+				</element>
+			</sequence>
+			<attribute name="version" use="required">
+				<annotation>
+					<documentation>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 XM [...]
+				</annotation>
+				<simpleType>
+					<restriction base="string">
+						<pattern value="\d+\.\d?\d\.\d?\d"/>
+					</restriction>
+				</simpleType>
+			</attribute>
+			<attribute ref="xml:lang" use="optional">
+				<annotation>
+					<documentation>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. </documentation>
+				</annotation>
+			</attribute>
+		</complexType>
+	</element>
+	<!-- ======================================================= -->
+	<element name="Exception" type="ows:ExceptionType"/>
+	<!-- ======================================================= -->
+	<complexType name="ExceptionType">
+		<annotation>
+			<documentation>An Exception element describes one detected error that a server chooses to convey to the client. </documentation>
+		</annotation>
+		<sequence>
+			<element name="ExceptionText" type="string" minOccurs="0" maxOccurs="unbounded">
+				<annotation>
+					<documentation>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  [...]
+				</annotation>
+			</element>
+		</sequence>
+		<attribute name="exceptionCode" type="string" use="required">
+			<annotation>
+				<documentation>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. </documentation>
+			</annotation>
+		</attribute>
+		<attribute name="locator" type="string" use="optional">
+			<annotation>
+				<documentation>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. </documentation>
+			</annotation>
+		</attribute>
+	</complexType>
+</schema>
diff --git a/mapproxy/test/schemas/ows/1.1.0/owsGetCapabilities.xsd b/mapproxy/test/schemas/ows/1.1.0/owsGetCapabilities.xsd
new file mode 100644
index 0000000..dbb71d3
--- /dev/null
+++ b/mapproxy/test/schemas/ows/1.1.0/owsGetCapabilities.xsd
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/ows/1.1" 
+xmlns:ows="http://www.opengis.net/ows/1.1" 
+xmlns="http://www.w3.org/2001/XMLSchema" 
+elementFormDefault="qualified" version="1.1.0 2010-01-30" xml:lang="en">
+	<annotation>
+		<appinfo>owsGetCapabilities.xsd 2010-01-30</appinfo>
+		<documentation>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/ .
+		</documentation>
+	</annotation>
+	<!-- ==============================================================
+		includes and imports
+	============================================================== -->
+	<include schemaLocation="owsServiceIdentification.xsd"/>
+	<include schemaLocation="owsServiceProvider.xsd"/>
+	<include schemaLocation="owsOperationsMetadata.xsd"/>
+	<!-- ==============================================================
+		elements and types
+	============================================================== -->
+	<complexType name="CapabilitiesBaseType">
+		<annotation>
+			<documentation>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 includ [...]
+		</annotation>
+		<sequence>
+			<element ref="ows:ServiceIdentification" minOccurs="0"/>
+			<element ref="ows:ServiceProvider" minOccurs="0"/>
+			<element ref="ows:OperationsMetadata" minOccurs="0"/>
+		</sequence>
+		<attribute name="version" type="ows:VersionType" use="required"/>
+		<attribute name="updateSequence" type="ows:UpdateSequenceType" use="optional">
+			<annotation>
+				<documentation>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. </documentation>
+			</annotation>
+		</attribute>
+	</complexType>
+	<!-- =========================================================== -->
+	<element name="GetCapabilities" type="ows:GetCapabilitiesType"/>
+	<!-- =========================================================== -->
+	<complexType name="GetCapabilitiesType">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+		<sequence>
+			<element name="AcceptVersions" type="ows:AcceptVersionsType" minOccurs="0">
+				<annotation>
+					<documentation>When omitted, server shall return latest supported version. </documentation>
+				</annotation>
+			</element>
+			<element name="Sections" type="ows:SectionsType" minOccurs="0">
+				<annotation>
+					<documentation>When omitted or not supported by server, server shall return complete service metadata (Capabilities) document. </documentation>
+				</annotation>
+			</element>
+			<element name="AcceptFormats" type="ows:AcceptFormatsType" minOccurs="0">
+				<annotation>
+					<documentation>When omitted or not supported by server, server shall return service metadata document using the MIME type "text/xml". </documentation>
+				</annotation>
+			</element>
+		</sequence>
+		<attribute name="updateSequence" type="ows:UpdateSequenceType" use="optional">
+			<annotation>
+				<documentation>When omitted or not supported by server, server shall return latest complete service metadata document. </documentation>
+			</annotation>
+		</attribute>
+	</complexType>
+	<!-- =========================================================== -->
+	<!-- =========================================================== -->
+	<simpleType name="ServiceType">
+		<annotation>
+			<documentation>Service type identifier, where the string value is the OWS type abbreviation, such as "WMS" or "WFS". </documentation>
+		</annotation>
+		<restriction base="string"/>
+	</simpleType>
+	<!-- ========================================================= -->
+	<complexType name="AcceptVersionsType">
+		<annotation>
+			<documentation>Prioritized sequence of one or more specification versions accepted by client, with preferred versions listed first. See Version negotiation subclause for more information. </documentation>
+		</annotation>
+		<sequence>
+			<element name="Version" type="ows:VersionType" maxOccurs="unbounded"/>
+		</sequence>
+	</complexType>
+	<!-- =========================================================== -->
+	<complexType name="SectionsType">
+		<annotation>
+			<documentation>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.  </documentation>
+		</annotation>
+		<sequence>
+			<element name="Section" type="string" minOccurs="0" maxOccurs="unbounded"/>
+		</sequence>
+	</complexType>
+	<!-- =========================================================== -->
+	<simpleType name="UpdateSequenceType">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+		<restriction base="string"/>
+	</simpleType>
+	<!-- =========================================================== -->
+	<complexType name="AcceptFormatsType">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+		<sequence>
+			<element name="OutputFormat" type="ows:MimeType" minOccurs="0" maxOccurs="unbounded"/>
+		</sequence>
+	</complexType>
+</schema>
diff --git a/mapproxy/test/schemas/ows/1.1.0/owsGetResourceByID.xsd b/mapproxy/test/schemas/ows/1.1.0/owsGetResourceByID.xsd
new file mode 100644
index 0000000..f15b22f
--- /dev/null
+++ b/mapproxy/test/schemas/ows/1.1.0/owsGetResourceByID.xsd
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/ows/1.1" 
+xmlns:ows="http://www.opengis.net/ows/1.1" 
+xmlns="http://www.w3.org/2001/XMLSchema" 
+elementFormDefault="qualified" version="1.1.0 2010-01-30" xml:lang="en">
+	<annotation>
+		<appinfo>owsGetResourceByID.xsd 2010-01-30</appinfo>
+		<documentation>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/ .
+		</documentation>
+	</annotation>
+	<!-- ==============================================================
+		includes and imports
+	============================================================== -->
+	<include schemaLocation="owsDataIdentification.xsd"></include>
+	<include schemaLocation="owsGetCapabilities.xsd"></include>
+	<!-- ==============================================================
+		elements and types
+	============================================================== -->
+	<element name="Resource">
+		<annotation>
+			<documentation>XML encoded GetResourceByID operation response. The complexType used by this element shall be specified by each specific OWS.  </documentation>
+		</annotation>
+	</element>
+	<!-- =========================================================== -->
+	<element name="GetResourceByID" type="ows:GetResourceByIdType"></element>
+	<!-- =========================================================== -->
+	<complexType name="GetResourceByIdType">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+		<sequence>
+			<element name="ResourceID" type="anyURI" minOccurs="0" maxOccurs="unbounded">
+				<annotation>
+					<documentation>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. </documentation>
+				</annotation>
+			</element>
+			<element ref="ows:OutputFormat" minOccurs="0">
+				<annotation>
+					<documentation>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. </documentation>
+				</annotation>
+			</element>
+		</sequence>
+		<attribute name="service" type="ows:ServiceType" use="required"></attribute>
+		<attribute name="version" type="ows:VersionType" use="required"></attribute>
+	</complexType>
+	<!-- =========================================================== -->
+</schema>
diff --git a/mapproxy/test/schemas/ows/1.1.0/owsInputOutputData.xsd b/mapproxy/test/schemas/ows/1.1.0/owsInputOutputData.xsd
new file mode 100644
index 0000000..e8f28b6
--- /dev/null
+++ b/mapproxy/test/schemas/ows/1.1.0/owsInputOutputData.xsd
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/ows/1.1" 
+xmlns:ows="http://www.opengis.net/ows/1.1" 
+xmlns="http://www.w3.org/2001/XMLSchema" 
+elementFormDefault="qualified" version="1.1.0 2010-01-30" xml:lang="en">
+	<annotation>
+		<appinfo>owsInputOutputData.xsd 2010-01-30</appinfo>
+		<documentation>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/ .
+		</documentation>
+	</annotation>
+	<!-- ==============================================================
+		includes and imports
+	============================================================== -->
+	<include schemaLocation="owsManifest.xsd"/>
+	<!-- ==========================================================
+		Types and elements
+	    ========================================================== -->
+	<element name="OperationResponse" type="ows:ManifestType">
+		<annotation>
+			<documentation>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. </documentation>
+			<documentation>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. </documentation>
+		</annotation>
+	</element>
+	<!-- ========================================================== -->
+	<element name="InputData" type="ows:ManifestType">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+	</element>
+	<!-- ========================================================== -->
+	<element name="ServiceReference" type="ows:ServiceReferenceType" substitutionGroup="ows:Reference"/>
+	<!-- ========================================================== -->
+	<complexType name="ServiceReferenceType">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+		<complexContent>
+			<extension base="ows:ReferenceType">
+				<choice>
+					<element name="RequestMessage" type="anyType">
+						<annotation>
+							<documentation>The XML-encoded operation request message to be sent to request this input data from another web server using HTTP Post. </documentation>
+						</annotation>
+					</element>
+					<element name="RequestMessageReference" type="anyURI">
+						<annotation>
+							<documentation>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. </documentation>
+						</annotation>
+					</element>
+				</choice>
+			</extension>
+		</complexContent>
+	</complexType>
+	<!-- ========================================================== -->
+</schema>
diff --git a/mapproxy/test/schemas/ows/1.1.0/owsManifest.xsd b/mapproxy/test/schemas/ows/1.1.0/owsManifest.xsd
new file mode 100644
index 0000000..d5c0cee
--- /dev/null
+++ b/mapproxy/test/schemas/ows/1.1.0/owsManifest.xsd
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/ows/1.1" 
+xmlns:ows="http://www.opengis.net/ows/1.1" 
+xmlns:xlink="http://www.w3.org/1999/xlink" 
+xmlns="http://www.w3.org/2001/XMLSchema" 
+elementFormDefault="qualified" version="1.1.0 2010-01-30" xml:lang="en">
+	<annotation>
+		<appinfo>owsManifest.xsd 2010-01-30</appinfo>
+		<documentation>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/ .
+		</documentation>
+	</annotation>
+	<!-- ==============================================================
+		includes and imports
+	============================================================== -->
+	<include schemaLocation="owsDataIdentification.xsd"/>
+	<import namespace="http://www.w3.org/1999/xlink" schemaLocation="../../xlink/1.0.0/xlinks.xsd"/>
+	<!-- ==========================================================
+		Types and elements
+	    ========================================================== -->
+	<element name="AbstractReferenceBase" type="ows:AbstractReferenceBaseType" abstract="true"/>
+	<!-- ========================================================== -->
+	<complexType name="AbstractReferenceBaseType">
+		<annotation>
+			<documentation> Base for a reference to a remote or local resource. </documentation>
+			<documentation>This type contains only a restricted and annotated set of the attributes from the xlink:simpleLink attributeGroup. </documentation>
+		</annotation>
+		<attribute name="type" type="string" fixed="simple" form="qualified"/>
+		<attribute ref="xlink:href" use="required">
+				<annotation>
+					<documentation>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:. </documentation>
+			</annotation>
+		</attribute>
+		<attribute ref="xlink:role" use="optional">
+			<annotation>
+				<documentation>Reference to a resource that describes the role of this reference. When no value is supplied, no particular role value is to be inferred. </documentation>
+			</annotation>
+		</attribute>
+		<attribute ref="xlink:arcrole" use="optional">
+			<annotation>
+				<documentation>Although allowed, this attribute is not expected to be useful in this application of xlink:simpleLink. </documentation>
+			</annotation>
+		</attribute>
+		<attribute ref="xlink:title" use="optional">
+			<annotation>
+				<documentation>Describes the meaning of the referenced resource in a human-readable fashion. </documentation>
+			</annotation>
+		</attribute>
+		<attribute ref="xlink:show" use="optional">
+			<annotation>
+				<documentation>Although allowed, this attribute is not expected to be useful in this application of xlink:simpleLink. </documentation>
+			</annotation>
+		</attribute>
+		<attribute ref="xlink:actuate" use="optional">
+			<annotation>
+				<documentation>Although allowed, this attribute is not expected to be useful in this application of xlink:simpleLink. </documentation>
+			</annotation>
+		</attribute>
+	</complexType>
+	<!-- ========================================================== -->
+	<element name="Reference" type="ows:ReferenceType" substitutionGroup="ows:AbstractReferenceBase"/>
+	<!-- ========================================================== -->
+	<complexType name="ReferenceType">
+		<annotation>
+			<documentation>Complete reference to a remote or local resource, allowing including metadata about that resource. </documentation>
+		</annotation>
+		<complexContent>
+			<extension base="ows:AbstractReferenceBaseType">
+				<sequence>
+					<element ref="ows:Identifier" minOccurs="0">
+						<annotation>
+							<documentation>Optional unique identifier of the referenced resource. </documentation>
+						</annotation>
+					</element>
+					<element ref="ows:Abstract" minOccurs="0" maxOccurs="unbounded"/>
+					<element name="Format" type="ows:MimeType" minOccurs="0">
+						<annotation>
+							<documentation>The format of the referenced resource. This element is omitted when the mime type is indicated in the http header of the reference. </documentation>
+						</annotation>
+					</element>
+					<element ref="ows:Metadata" minOccurs="0" maxOccurs="unbounded">
+						<annotation>
+							<documentation>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. </documentation>
+						</annotation>
+					</element>
+				</sequence>
+			</extension>
+		</complexContent>
+	</complexType>
+	<!-- =========================================================== -->
+	<!-- =========================================================== -->
+	<element name="ReferenceGroup" type="ows:ReferenceGroupType"/>
+	<!-- =========================================================== -->
+	<complexType name="ReferenceGroupType">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+		<complexContent>
+			<extension base="ows:BasicIdentificationType">
+				<sequence>
+					<element ref="ows:AbstractReferenceBase" maxOccurs="unbounded"/>
+				</sequence>
+			</extension>
+		</complexContent>
+	</complexType>
+	<!-- =========================================================== -->
+	<element name="Manifest" type="ows:ManifestType"/>
+	<!-- =========================================================== -->
+	<complexType name="ManifestType">
+		<annotation>
+			<documentation>Unordered list of one or more groups of references to remote and/or local resources. </documentation>
+		</annotation>
+		<complexContent>
+			<extension base="ows:BasicIdentificationType">
+				<sequence>
+					<element ref="ows:ReferenceGroup" maxOccurs="unbounded"/>
+				</sequence>
+			</extension>
+		</complexContent>
+	</complexType>
+	<!-- ========================================================== -->
+</schema>
diff --git a/mapproxy/test/schemas/ows/1.1.0/owsOperationsMetadata.xsd b/mapproxy/test/schemas/ows/1.1.0/owsOperationsMetadata.xsd
new file mode 100644
index 0000000..7e0ef74
--- /dev/null
+++ b/mapproxy/test/schemas/ows/1.1.0/owsOperationsMetadata.xsd
@@ -0,0 +1,140 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/ows/1.1" 
+xmlns:ows="http://www.opengis.net/ows/1.1" 
+xmlns="http://www.w3.org/2001/XMLSchema" 
+elementFormDefault="qualified" version="1.1.0 2010-01-30" xml:lang="en">
+	<annotation>
+		<appinfo>owsOperationsMetadata.xsd 2010-01-30</appinfo>
+		<documentation>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/ .
+		</documentation>
+	</annotation>
+	<!-- ==============================================================
+		includes and imports
+	============================================================== -->
+	<include schemaLocation="owsCommon.xsd"/>
+	<include schemaLocation="ows19115subset.xsd"/>
+	<include schemaLocation="owsDomainType.xsd"/>
+	<!-- ==============================================================
+		elements and types
+	============================================================== -->
+	<element name="OperationsMetadata">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="ows:Operation" minOccurs="2" maxOccurs="unbounded">
+					<annotation>
+						<documentation>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. </documentation>
+					</annotation>
+				</element>
+				<element name="Parameter" type="ows:DomainType" minOccurs="0" maxOccurs="unbounded">
+					<annotation>
+						<documentation>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. </documentation>
+					</annotation>
+				</element>
+				<element name="Constraint" type="ows:DomainType" minOccurs="0" maxOccurs="unbounded">
+					<annotation>
+						<documentation>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. </documentation>
+					</annotation>
+				</element>
+				<element ref="ows:ExtendedCapabilities" minOccurs="0"/>
+			</sequence>
+		</complexType>
+	</element>
+	<!-- ========================================================== -->
+	<element name="ExtendedCapabilities" type="anyType">
+		<annotation>
+			<documentation>Individual software vendors and servers can use this element to provide metadata about any additional server abilities. </documentation>
+		</annotation>
+	</element>
+	<!-- ========================================================== -->
+	<element name="Operation">
+		<annotation>
+			<documentation>Metadata for one operation that this server implements. </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="ows:DCP" maxOccurs="unbounded">
+					<annotation>
+						<documentation>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. </documentation>
+					</annotation>
+				</element>
+				<element name="Parameter" type="ows:DomainType" minOccurs="0" maxOccurs="unbounded">
+					<annotation>
+						<documentation>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. </documentation>
+					</annotation>
+				</element>
+				<element name="Constraint" type="ows:DomainType" minOccurs="0" maxOccurs="unbounded">
+					<annotation>
+						<documentation>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. </documentation>
+					</annotation>
+				</element>
+				<element ref="ows:Metadata" minOccurs="0" maxOccurs="unbounded">
+					<annotation>
+						<documentation>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.) </documentation>
+					</annotation>
+				</element>
+			</sequence>
+			<attribute name="name" type="string" use="required">
+				<annotation>
+					<documentation>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. </documentation>
+				</annotation>
+			</attribute>
+		</complexType>
+	</element>
+	<!-- ========================================================== -->
+	<element name="DCP">
+		<annotation>
+			<documentation>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.
+</documentation>
+		</annotation>
+		<complexType>
+			<choice>
+				<element ref="ows:HTTP"/>
+			</choice>
+		</complexType>
+	</element>
+	<!-- ========================================================== -->
+	<element name="HTTP">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+		<complexType>
+			<choice maxOccurs="unbounded">
+				<element name="Get" type="ows:RequestMethodType">
+					<annotation>
+						<documentation>Connect point URL prefix and any constraints for the HTTP "Get" request method for this operation request. </documentation>
+					</annotation>
+				</element>
+				<element name="Post" type="ows:RequestMethodType">
+					<annotation>
+						<documentation>Connect point URL and any constraints for the HTTP "Post" request method for this operation request. </documentation>
+					</annotation>
+				</element>
+			</choice>
+		</complexType>
+	</element>
+	<!-- ========================================================== -->
+	<complexType name="RequestMethodType">
+		<annotation>
+			<documentation>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. </documentation>
+		</annotation>
+		<complexContent>
+			<extension base="ows:OnlineResourceType">
+				<sequence>
+					<element name="Constraint" type="ows:DomainType" minOccurs="0" maxOccurs="unbounded">
+						<annotation>
+							<documentation>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 Imple [...]
+						</annotation>
+					</element>
+				</sequence>
+			</extension>
+		</complexContent>
+	</complexType>
+	<!-- ========================================================== -->
+</schema>
diff --git a/mapproxy/test/schemas/ows/1.1.0/owsServiceIdentification.xsd b/mapproxy/test/schemas/ows/1.1.0/owsServiceIdentification.xsd
new file mode 100644
index 0000000..520a2b8
--- /dev/null
+++ b/mapproxy/test/schemas/ows/1.1.0/owsServiceIdentification.xsd
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/ows/1.1" 
+xmlns:ows="http://www.opengis.net/ows/1.1" 
+xmlns="http://www.w3.org/2001/XMLSchema" 
+elementFormDefault="qualified" version="1.1.0 2010-01-30" xml:lang="en">
+	<annotation>
+		<appinfo>owsServiceIdentification.xsd 2010-01-30</appinfo>
+		<documentation>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/ .
+		</documentation>
+	</annotation>
+	<!-- ==============================================================
+		includes and imports
+	============================================================== -->
+	<include schemaLocation="owsDataIdentification.xsd"/>
+	<!-- ==============================================================
+		elements and types
+	============================================================== -->
+	<element name="ServiceIdentification">
+		<annotation>
+			<documentation>General metadata for this specific server. This XML Schema of this section shall be the same for all OWS. </documentation>
+		</annotation>
+		<complexType>
+			<complexContent>
+				<extension base="ows:DescriptionType">
+					<sequence>
+						<element name="ServiceType" type="ows:CodeType">
+							<annotation>
+								<documentation>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. </documentation>
+							</annotation>
+						</element>
+						<element name="ServiceTypeVersion" type="ows:VersionType" maxOccurs="unbounded">
+							<annotation>
+								<documentation>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. </documentation>
+							</annotation>
+						</element>
+						<element name="Profile" type="anyURI" minOccurs="0" maxOccurs="unbounded">
+							<annotation>
+								<documentation>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. </documentation>
+							</annotation>
+						</element>
+						<element ref="ows:Fees" minOccurs="0">
+							<annotation>
+								<documentation>If this element is omitted, no meaning is implied. </documentation>
+							</annotation>
+						</element>
+						<element ref="ows:AccessConstraints" minOccurs="0" maxOccurs="unbounded">
+							<annotation>
+								<documentation>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. </documentation>
+							</annotation>
+						</element>
+					</sequence>
+				</extension>
+			</complexContent>
+		</complexType>
+	</element>
+</schema>
diff --git a/mapproxy/test/schemas/ows/1.1.0/owsServiceProvider.xsd b/mapproxy/test/schemas/ows/1.1.0/owsServiceProvider.xsd
new file mode 100644
index 0000000..b15f621
--- /dev/null
+++ b/mapproxy/test/schemas/ows/1.1.0/owsServiceProvider.xsd
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/ows/1.1" 
+xmlns:ows="http://www.opengis.net/ows/1.1" 
+xmlns:xlink="http://www.w3.org/1999/xlink" 
+xmlns="http://www.w3.org/2001/XMLSchema" 
+elementFormDefault="qualified" version="1.1.0 2010-01-30" xml:lang="en">
+	<annotation>
+		<appinfo>owsServiceProvider.xsd 2010-01-30</appinfo>
+		<documentation>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/ .
+		</documentation>
+	</annotation>
+	<!-- ==============================================================
+		includes and imports
+	============================================================== -->
+	<include schemaLocation="ows19115subset.xsd"/>
+	<!-- ==============================================================
+		elements and types
+	============================================================== -->
+	<element name="ServiceProvider">
+		<annotation>
+			<documentation>Metadata about the organization that provides this specific service instance or server. </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element name="ProviderName" type="string">
+					<annotation>
+						<documentation>A unique identifier for the service provider organization. </documentation>
+					</annotation>
+				</element>
+				<element name="ProviderSite" type="ows:OnlineResourceType" minOccurs="0">
+					<annotation>
+						<documentation>Reference to the most relevant web site of the service provider. </documentation>
+					</annotation>
+				</element>
+				<element name="ServiceContact" type="ows:ResponsiblePartySubsetType">
+					<annotation>
+						<documentation>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. </documentation>
+					</annotation>
+				</element>
+			</sequence>
+		</complexType>
+	</element>
+</schema>
diff --git a/mapproxy/test/schemas/sld/1.1.0/sld_capabilities.xsd b/mapproxy/test/schemas/sld/1.1.0/sld_capabilities.xsd
new file mode 100644
index 0000000..bf00b5b
--- /dev/null
+++ b/mapproxy/test/schemas/sld/1.1.0/sld_capabilities.xsd
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xsd:schema targetNamespace="http://www.opengis.net/sld" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:wms="http://www.opengis.net/wms" xmlns:sld="http://www.opengis.net/sld" xmlns="http://www.w3.org/2001/XMLSchema" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" version="1.1.0 2010-02-01">
+	
+	<xsd:import namespace="http://www.opengis.net/wms" schemaLocation="../../wms/1.3.0/capabilities_1_3_0.xsd"/>
+	<xsd:annotation>
+		<xsd:documentation>
+			<description>Styled Layer Descriptor version 1.1.0 (2010-02-01)</description>
+			<copyright>
+				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/ .
+			</copyright>
+		</xsd:documentation>
+	</xsd:annotation>
+	<xsd:element name="UserDefinedSymbolization" substitutionGroup="wms:_ExtendedCapabilities">
+		<xsd:complexType>
+			<xsd:attribute name="SupportSLD" type="boolean" default="0"/>
+			<xsd:attribute name="UserLayer" type="boolean" default="0"/>
+			<xsd:attribute name="UserStyle" type="boolean" default="0"/>
+			<xsd:attribute name="RemoteWFS" type="boolean" default="0"/>
+			<xsd:attribute name="InlineFeature" type="boolean" default="0"/>
+			<xsd:attribute name="RemoteWCS" type="boolean" default="0"/>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="DescribeLayer" type="wms:OperationType" substitutionGroup="wms:_ExtendedOperation"/>
+	<xsd:element name="GetLegendGraphic" type="wms:OperationType" substitutionGroup="wms:_ExtendedOperation"/>
+</xsd:schema>
diff --git a/mapproxy/test/schemas/wms/1.0.0/capabilities_1_0_0.dtd b/mapproxy/test/schemas/wms/1.0.0/capabilities_1_0_0.dtd
new file mode 100644
index 0000000..4f569c7
--- /dev/null
+++ b/mapproxy/test/schemas/wms/1.0.0/capabilities_1_0_0.dtd
@@ -0,0 +1,353 @@
+<!-- NOTE: comments in this Document Type Definition impose additional
+constraints beyond those codified in the DTD syntax.  A conformant Web Map
+Server must provide Capabilities XML that (1) validates against the DTD and
+(2) does not violate the constraints stated in comments herein. -->
+
+<!-- The parent element of the Capabilities document includes as children a
+Service element with general information about the server and a Capability
+element with specific information about the kinds of functionality offered by
+the server. -->
+<!ELEMENT WMT_MS_Capabilities (Service, Capability) >
+
+<!-- The version attribute specifies the specification revision to which this
+DTD applies.  Its format is one, two or three integers separated by periods:
+"x", or "x.y", or "x.y.z", with the most significant number appearing first.
+Future revisions are guaranteed to be numbered in monotonically increasing
+fashion, though gaps may appear in the sequence.  All known versions may
+be found at http://www.digitalearth.gov/wmt/xml/ -->
+<!-- The updateSequence attribute is a sequence number for managing
+propagation of the contents of this document.  For example, if a Map Server
+adds some data layers it can increment the update sequence to inform catalog
+servers that their previously cached versions are now stale.  The format is a
+positive integer. -->
+<!ATTLIST WMT_MS_Capabilities
+	  version CDATA #FIXED "1.0.0"
+	  updateSequence CDATA "0">
+
+<!-- This WMT-wide list of possible output formats can be redefined by
+individual servers; see the sample XML. -->
+<!ENTITY % KnownFormats " GIF | JPEG | PNG | WebCGM | SVG
+              | GML.1 | GML.2 | GML.3 | WBMP
+              | WMS_XML | MIME | INIMAGE | TIFF | GeoTIFF | PPM | BLANK " >
+
+<!-- The Service element provides metadata for the service as a whole. -->
+<!ELEMENT Service (Name, Title, Abstract?, Keywords?, OnlineResource, Fees?,
+ AccessConstraints?) >
+
+<!-- A service name defined within the Web Mapping Specification namespace.
+Currently only the name "GetMap" is defined. -->
+<!ELEMENT Name (#PCDATA) >
+
+<!-- A human-readable title to briefly identify this server in menus. -->
+<!ELEMENT Title (#PCDATA) >
+
+<!-- A descriptive narrative for more information about this server. -->
+<!ELEMENT Abstract (#PCDATA) > 
+
+<!-- Short words to help catalog searching.  Currently, no controlled
+vocabulary has been defined. -->
+<!ELEMENT Keywords (#PCDATA) >
+
+<!-- The top-level HTTP URL of this service.  Typically the URL of a "home
+page" for the service.  See also the onlineResource attributes of <DCPType>
+children, below.  Currently, no non-HTTP platforms have been specified. -->
+<!ELEMENT OnlineResource (#PCDATA)>
+
+<!-- Elements indicating what fees or access constraints are imposed.
+The reserved keyword "none" indicates no constraint exists. -->
+<!ELEMENT Fees (#PCDATA)>
+<!ELEMENT AccessConstraints (#PCDATA)>
+
+<!-- A Capability lists available request types, how exceptions
+may be reported, and whether any vendor-specific capabilities are defined.  It
+also includes an optional list of map layers available from this server. -->
+
+<!ELEMENT Capability 
+          (Request, Exception?, VendorSpecificCapabilities?, Layer?) >
+
+<!-- Available WMT-defined request types are listed here.
+At least one of the values is required, but more than one
+may be given. -->
+<!ELEMENT Request (Map | Capabilities | FeatureInfo)+ >
+
+<!-- For each request method offered by the server, list the
+available output formats and the supported distributed
+computing platforms (DCPs).  Example:
+    <Map>
+      <Format><PNG /><JPEG /><GML.1 /></Format>
+      <DCPType><HTTP><Get onlineResource="URL" /></HTTP></DCPType>
+    </Map>
+ -->
+
+<!-- GetMap interface: Presence of the Map element means this server can
+generate a map of a specified area, either as a picture or a feature
+collection -->
+<!ELEMENT Map (Format, DCPType+)>
+
+<!-- GetCapabilities interface: Presence of the Capabilities element means
+this server can generate a description of its abilities and holdings formatted
+in XML that complies with this DTD. -->
+<!ELEMENT Capabilities (Format, DCPType+)>
+
+<!-- GetFeatureInfo interface: Presence of the FeatureInfo element means this
+server can return information about a specific feature given a valid map
+request and a location on that map. -->
+<!ELEMENT FeatureInfo (Format, DCPType+)>
+
+<!-- Available Distributed Computing Platforms (DCPs) are
+listed here.  At present, only HTTP is defined. -->
+<!ELEMENT DCPType (HTTP) >
+
+<!-- Available HTTP request methods. -->
+<!ELEMENT HTTP (Get | Post)+ >
+
+<!-- HTTP request methods.  The onlineResource attribute indicates the URL
+prefix for HTTP GET requests (everything before the question mark and query
+string: http://hostname[:port]/path/scriptname); for HTTP POST requests,
+onlineResource is the complete URL.  The HTTP GET syntax for Map, Capabilities
+and FeatureInfo requests has been well defined and is described in the OGC
+Web Map Server Interface Specification.  The POST formalism, wherein GetMap
+arguments are encoded in XML and POSTed to the server, has not yet been fully
+developed. -->
+<!ELEMENT Get EMPTY>
+<!ATTLIST Get onlineResource CDATA #REQUIRED>
+<!ELEMENT Post EMPTY>
+<!ATTLIST Post onlineResource CDATA #REQUIRED>
+
+<!-- Available formats.  Not all formats are relevant to all requests.
+Individual servers MAY add new formats as shown in the sample XML accompanying
+this DTD. -->
+<!ELEMENT Format ( %KnownFormats; )+ >
+<!ELEMENT GIF EMPTY> <!-- Graphics Interchange Format -->
+<!ELEMENT JPEG EMPTY> <!-- Joint Photographics Expert Group -->
+<!ELEMENT PNG EMPTY> <!-- Portable Network Graphics -->
+<!ELEMENT PPM EMPTY> <!-- Portable PixMap -->
+<!ELEMENT TIFF EMPTY> <!-- Tagged Image File Format -->
+<!ELEMENT GeoTIFF EMPTY> <!-- Geographic TIFF -->
+<!ELEMENT WebCGM EMPTY> <!-- Web Computer Graphics Metafile -->
+<!ELEMENT SVG EMPTY>  <!-- Scalable Vector Graphics -->
+<!ELEMENT WMS_XML EMPTY> <!-- eXtensible Markup Language -->
+<!ELEMENT GML.1 EMPTY> <!-- Geography Markup Language, profile 1 -->
+<!ELEMENT GML.2 EMPTY> <!-- Geography Markup Language, profile 2 -->
+<!ELEMENT GML.3 EMPTY> <!-- Geography Markup Language, profile 3 -->
+<!ELEMENT WBMP EMPTY> <!-- Wireless Access Protocol (WAP) Bitmap -->
+<!ELEMENT MIME EMPTY> <!-- Multipurpose Internet Mail Extensions -->
+<!ELEMENT INIMAGE EMPTY> <!-- display text in the returned image -->
+<!ELEMENT BLANK EMPTY> <!-- return an image with all pixels transparent if
+                            supported by the image format, otherwise all
+                            pixels set to the BGCOLOR if present, otherwise
+                            all pixels set to the same (arbitrary) value -->
+
+<!-- An Exception element indicates which output formats are supported
+for reporting problems encountered when executing a request.  Available
+Exception formats MUST include one or more of WMS_XML, INIMAGE, or BLANK.
+Example: <Exception><Format><INIMAGE /><WMS_XML /></Format></Exception>. -->
+<!ELEMENT Exception (Format)>
+
+<!-- The optional VendorSpecificCapabilities element lists any capabilities
+unique to a particular Map Server.  Because the information is not known a
+priori, it cannot be constrained by this particular DTD.  A vendor-specific
+DTD fragment must be supplied at the start of the XML Capabilities document,
+after the reference to the general WMT_MS_Capabilities DTD.  See the sample
+XML for further information. -->
+
+<!--
+ DEFINE THIS ELEMENT AS NEEDED IN YOUR XML
+ <!ELEMENT VendorSpecificCapabilities (your stuff here) >
+-->
+
+<!-- Nested list of zero or more map Layers offered by this server.  The Layer
+element can be omitted if the server has no layers (for example, if it offers
+only geoprocessing services but no actual data). -->
+
+<!-- A Layer element has two functions: it either refers to a map layer which
+can be requested by Name in the LAYERS parameter of a GetMap request, or it is
+a category Title for all the layers nested within.  In the latter case, the
+category itself MAY include a Name by which all of the nested layers can be
+requested at once.  For example, a parent layer "Roads" may have children
+"Interstates" and "County Roads" and allow the user to request either child
+individually or both together. -->
+
+<!-- A Map Server which advertises a Layer containing a Name element MUST be
+able to accept that Name as the value of LAYERS argument in a GetMap request
+and return the corresponding map.  A Viewer Client MUST NOT attempt to request
+a Layer that has a Title but no Name. -->
+
+<!-- A Map Server MUST include at least one <Layer> element for each map layer
+offered.  If desired, data layers MAY be repeated in different categories when
+relevant.  A Layer element MAY state the Name by which a map of the layer is
+requested, MUST give a Title to be used in human-readable menus, and MAY
+include: a human-readable Abstract containing further description, available
+Spatial Reference Systems (SRS), bounding boxes in Lat/Lon and SRS-specific
+coordinates indicating the available geographic coverage, styles in which the
+layer is available, a URL for more information about the data, and a hint
+concerning appropriate map scales for displaying this layer.  Use of the
+nesting hierarchy is optional.  -->
+
+<!-- The following table specifies the number and source of the various
+elements (and one attribute) describing a Layer that has a Name.  Without a
+Name, the Layer is merely a category title and all other elements are
+optional; if present, some of those elements may be inherited by children as
+described in the table.
+
+                       inherit
+                        from
+element         number parent?  comments
+=======         ====== =======  ========
+Name              1      -      this table only applies to Named Layers
+                                (those which can be requested in a GetMap call)
+                                no default
+
+Title             1      -      required in each Layer; no default
+
+Abstract          0/1    -      optional; no default
+
+Keywords          0/1    -      optional; no default
+
+SRS               1     add     one list is required; default from parent;
+                                list may include multiple whitespace-separated
+                                values
+
+LatLonBoundingBox 1    replace  exactly one is required; default from parent
+
+BoundingBox       0+   replace  one is required per SRS other than EPSG:4326;
+                                default from parent
+
+DataURL           0/1    -      optional; no default
+
+Style             0+    add     optional; default from parent
+
+ScaleHint         0/1  replace  optional; default from parent
+
+Layer             0+     -      optional; no default;
+                                if present, each is a "child" of the enclosing
+                                Layer and inherits some default values as
+                                described in this table
+
+queryable         1    replace  optional; default is "0" (not queryable)
+(attribute)                     or parent value if present
+
+
+-->
+
+<!ELEMENT Layer ( Name?, Title, Abstract?, Keywords?, SRS?,
+                  LatLonBoundingBox?, BoundingBox*, DataURL?,
+                  Style*, ScaleHint?, Layer* ) >
+
+<!-- A data layer may be queryable, as specified by this optional binary
+attribute.  1 = queryable, 0 = not queryable. A server that declares a Layer
+to by queryable MUST implement the GetFeatureInfo interface. -->
+
+<!ATTLIST Layer queryable (0 | 1) "0" >
+          
+<!-- Listing of available Spatial Reference Systems (SRS).
+* The root Layer element must include a list of all SRSes which
+  are common to *all* subsidiary layers.  This allows clients
+  to quickly determine that a server cannot possibly satisfy a
+  request for a particular SRS.  Use an empty element if there
+  is no common SRS.
+* Layer-specific SRS: Optionally, layers may add to the global SRS list, or to
+  the list inherited from a parent layer as described above.
+  Any duplication should be ignored by clients.  That is, a particular
+  layer may list all of its SRSes, even if it repeats
+  information in the top-level SRS.
+
+The content of the SRS element is a free-text list of SRS names separated by
+whitespace.  A name includes a namespace prefix, a colon, and one or more
+parameter values.  Currently defined namespaces are:
+
+prefix  parameters    comment
+======  ==========    =======
+EPSG    EPSG code     European Petroleum Survey Group geodesy parameters
+                      http://www.petroconsultants.com/products/geodetic.html
+                      Examples: 'EPSG:4326' is WGS84 lat/lon,
+                      'EPSG:26986' is NAD83 / Massachusetts Mainland.
+
+AUTO	WMT code,     WMT list of "automatic" projections.  WMT code:
+        EPSG units,   an integer identifier assigned by WMT (see
+        Longitude,    http://www.digitalearth.gov/wmt/auto.html). EPSG units:
+        Latitude      one of the EPSG codes for identifying units,
+                      indicating what units are to be used in Bounding Boxes.
+                      Longitude: central meridian of the projection (degrees).
+                      Latitude: central latitude of the projection (degrees).
+                      Example: 'AUTO:42003,9001,-100,45' is auto orthographic
+                      projection, bbox units in meters, center at 100W 45N.
+                      The bounding box is measured in a plane perpendicular to
+                      the line of sight, *not* directly on the Earth.
+-->
+
+<!ELEMENT SRS (#PCDATA) >
+
+<!-- The LatLonBoundingBox attributes indicate the edges of the enclosing
+rectangle in latitude/longitude decimal degrees (as in SRS EPSG:4326 [WGS1984
+lat/lon]).  LatLonBoundingBox MUST be supplied regardless of what SRS the map
+server may support, but it MAY be approximate if EPSG:4326 is not supported.
+Its purpose is to facilitate geographic searches without requiring coordinate
+transformations by the search engine. -->
+<!ELEMENT LatLonBoundingBox EMPTY>
+<!ATTLIST LatLonBoundingBox 
+          minx CDATA #REQUIRED
+          miny CDATA #REQUIRED
+          maxx CDATA #REQUIRED
+          maxy CDATA #REQUIRED>
+
+<!-- The BoundingBox attributes indicate the edges of the bounding box
+in units of the specified spatial reference system. -->
+<!ELEMENT BoundingBox EMPTY>
+<!ATTLIST BoundingBox 
+          SRS CDATA #REQUIRED
+          minx CDATA #REQUIRED
+          miny CDATA #REQUIRED
+          maxx CDATA #REQUIRED
+          maxy CDATA #REQUIRED>
+
+<!-- A Map Server MAY use DataURL to offer more information about the data
+underneath a particular layer. While the semantics are not well-defined, as
+long as the results of an HTTP GET request against the DataURL are properly
+MIME-typed, Viewer Clients and Cascading Map Servers can make use of this. -->
+<!ELEMENT DataURL (#PCDATA) >
+
+<!-- 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.  If a Layer is
+offered in only a single Style, the Map Server MAY choose not to give it a
+name.  Nevertheless, when handling a GetMap request a server MUST accept ''
+(null) and 'default' in the STYLES parameter as synonyms for the default (or
+only) style. -->
+<!ELEMENT Style ( Name, Title, Abstract?, StyleURL? ) >
+
+<!-- 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. -->
+<!ELEMENT StyleURL (#PCDATA) >
+ 
+<!-- Minimum and maximum scale hints for which it is appropriate to
+display this layer. It is STRONGLY RECOMMENDED that for Picture Case
+return formats the min and max values be expressed as ground distance
+in meters of a southwest to northeast diagonal of the pixel whose X
+coordinate is floor(width/2) and whose Y coordinate is floor(height/2).
+
+In the figure below, the X is in the pixel whose diagonal measurement
+is used.
+
+   +===+===+===+      +===+===+===+===+
+   |   |   |   |      |   |   |   |   |
+   +===+===+===+      +===+===+===+===+
+   |   | X |   |      |   | X |   |   |
+   +===+===+===+      +===+===+===+===+
+   |   |   |   |      |   |   |   |   |
+   +===+===+===+      +===+===+===+===+
+                      |   |   |   |   |
+                      +===+===+===+===+
+
+It is understood that this definition is not geodetically precise, but
+at the same time the hope is that by including it, conventions will
+develop around its use which can be later specified more clearly.  -->
+<!ELEMENT ScaleHint EMPTY>
+<!ATTLIST ScaleHint min CDATA #REQUIRED max CDATA #REQUIRED>
+
+
+
diff --git a/mapproxy/test/schemas/wms/1.0.0/capabilities_1_0_0.xml b/mapproxy/test/schemas/wms/1.0.0/capabilities_1_0_0.xml
new file mode 100644
index 0000000..bcfba4e
--- /dev/null
+++ b/mapproxy/test/schemas/wms/1.0.0/capabilities_1_0_0.xml
@@ -0,0 +1,188 @@
+<?xml version='1.0' encoding="UTF-8" standalone="no" ?>
+<!-- The DTD (Document Type Definition) given here must correspond to the version number declared in the WMT_MS_Capabilities element below. -->
+<!DOCTYPE WMT_MS_Capabilities SYSTEM
+ "http://www.digitalearth.gov/wmt/xml/capabilities_1_0_0.dtd"
+ [
+
+ <!-- Output formats known to this server are defined here
+if necessary (only if a format not already listed in the WMT
+Capabilities DTD is needed).  To define a new format, place
+an entity definition for KnownFormats like the one below in
+the DOCTYPE declaration of your Capabilities XML, listing at
+minimum all of the formats you support and separating each
+by logical-OR (|) characters.  Then, define a new element
+for any formats not predefined by WMT.  For example, in the
+following list "SGI" is a server-specific format, while all
+the others are known WMT-wide.  Thus, SGI is included in the
+KnownFormats list and a new empty element <SGI/> is
+defined. -->
+<!ENTITY % KnownFormats " SGI | GIF | JPEG | PNG | WebCGM | SVG | GML.1
+ | WMS_XML | MIME | INIMAGE | PPM | BLANK " >
+<!ELEMENT SGI EMPTY> <!-- Silicon Graphics RGB Format -->
+
+ <!-- other vendor-specific elements defined here -->
+ <!ELEMENT VendorSpecificCapabilities (YMD)>
+ <!ELEMENT YMD (Title, Abstract)>
+ <!ATTLIST YMD required (0 | 1) "0">
+
+ ]>  <!-- end of DOCTYPE declaration -->
+
+<!-- The version number listed in the WMT_MS_Capabilities element here must correspond to the DTD declared above.  See the WMT specification document for how to respond when a client requests a version number not implemented by the server. -->
+<WMT_MS_Capabilities version="1.0.0" updateSequence="0">
+<!-- Service Metadata -->
+<Service>
+  <!-- The WMT-defined name for this type of service -->
+  <Name>GetMap</Name>
+  <!-- Human-readable title for pick lists -->
+  <Title>Acme Corp. Map Server</Title>
+  <!-- Narrative description providing additional information -->
+  <Abstract>WMT Map Server maintained by Acme Corporation.  Contact: webmaster at wmt.acme.com.  High-quality maps showing roadrunner nests and possible ambush locations.</Abstract>
+  <Keywords>bird roadrunner ambush</Keywords>
+  <!-- Top-level address of service or service provider.  See also onlineResource attributes of <DCPType> children. -->
+  <OnlineResource>http://hostname:port/path/</OnlineResource>
+  <!-- Fees or access constraints imposed. -->
+  <Fees>none</Fees>
+  <AccessConstraints>none</AccessConstraints>
+</Service>
+<Capability>
+  <Request>
+    <Map>
+      <Format>
+	<SGI />
+        <GIF />
+        <JPEG />
+        <PNG />
+        <WebCGM />
+        <SVG />
+      </Format>
+      <DCPType>
+        <HTTP>
+	  <!-- The URL here for HTTP GET requests includes only the prefix before the '?' and the query string.  Clients are expected to append '?WMTVER=nnn&etc' as described in the Web Map interface specification. -->
+          <Get onlineResource="http://hostname:port/path/mapserver.cgi" />
+          <Post onlineResource="http://hostname:port/path/mapserver.cgi" />
+        </HTTP>
+      </DCPType>
+    </Map>
+    <Capabilities>
+      <Format>
+        <WMS_XML />
+      </Format>
+      <DCPType>
+        <HTTP>
+	  <!-- The URL here for HTTP GET requests includes only the prefix before the '?' and the query string.  Clients are expected to append '?WMTVER=nnn&etc' as described in the Web Map interface specification. -->
+          <Get onlineResource="http://hostname:port/path/mapserver.cgi" />
+        </HTTP>
+      </DCPType>
+    </Capabilities>
+    <FeatureInfo>
+      <Format>
+        <MIME />
+        <GML.1 />
+      </Format>
+      <DCPType>
+        <HTTP>
+	  <!-- The URL here for HTTP GET requests includes only the prefix before the '?' and the query string.  Clients are expected to append '?WMTVER=nnn&etc' as described in the Web Map interface specification. -->
+          <Get onlineResource="http://hostname:port/path/mapserver.cgi" />
+        </HTTP>
+      </DCPType>
+    </FeatureInfo>
+  </Request>
+  <Exception>
+    <Format>
+      <BLANK />
+      <WMS_XML />
+    </Format>
+  </Exception>
+  <!-- Any text or markup is allowed here, as required to describe
+       server-specific options.  Please define elements and attributes
+       in the DOCTYPE declaration at the start of the document. -->
+  <VendorSpecificCapabilities>
+    <YMD required="0">
+      <Title>Date in YYYYMMDD format</Title>
+      <Abstract>8-digit date in YYYYMMDD format.  If absent,
+         the latest available date (usually today, but not for
+         non-daily measurements) is sent.</Abstract>
+    </YMD>
+  </VendorSpecificCapabilities>
+  <Layer>
+    <Title>Acme Corp. Map Server</Title>
+    <SRS>EPSG:4326</SRS> <!-- all layers are available in at least this SRS -->
+    <Layer queryable="0">
+      <Name>wmt_graticule</Name> 
+      <Title>Alignment test grid</Title>
+      <Abstract>The WMT Graticule is a 10-degree grid suitable for testing alignment among Map Servers.</Abstract>
+      <Keywords>graticule test</Keywords>
+      <LatLonBoundingBox minx="-180" miny="-90" maxx="180" maxy="90" />
+      <Style>
+	<Name>on</Name>
+	<Title>Show test grid</Title>
+	<Abstract>The "on" style for the WMT Graticule causes that layer to be displayed.</Abstract>
+      </Style>
+      <Style>
+	<Name>off</Name>
+	<Title>Hide test grid</Title>
+	<Abstract>The "off" style for the WMT Graticule causes that layer to be hidden even though it was requested from the Map Server.  Style=off is the same as not requesting the graticule at all.</Abstract>
+      </Style>
+    </Layer>
+    <Layer>
+      <!-- This parent layer has a Name and can therefore be requested from a Map Server, yielding a map of all subsidiary layers. -->
+      <Name>ROADS_RIVERS</Name> 
+      <Title>Roads and Rivers</Title>
+      <!-- The following characteristics are inherited by subsidiary layers. -->
+      <SRS>EPSG:26986</SRS> <!-- An additional SRS for this layer --> 
+      <LatLonBoundingBox minx="-71.634696" miny="41.754149" maxx="-70.789798" maxy="42.908459"/>
+      <BoundingBox SRS="EPSG:26986" minx="189000" miny="834000" maxx="285000" maxy="962000"/>
+      <Style>
+        <Name>USGS Topo</Name>
+        <Title>Topo map style</Title>
+        <Abstract>Features are shown in a style like that used in USGS topographic maps.</Abstract>
+        <StyleURL></StyleURL>
+      </Style>
+      <ScaleHint min="4000" max="35000"></ScaleHint>
+      <Layer queryable="1">
+	<Name>ROADS_1M</Name> 
+	<Title>Roads at 1:1M scale</Title>
+	<Abstract>Roads at a scale of 1 to 1 million.</Abstract>
+	<Keywords>road transportation atlas</Keywords>
+	<DataURL>http://www.opengis.org?roads.xml</DataURL>
+        <!-- In addition to the Style specified in the parent Layer, this Layer is available in this style. -->
+	<Style>
+	  <Name>Rand McNally</Name>
+	  <Title>Road atlas style</Title>
+	  <Abstract>Roads are shown in a style like that used in a Rand McNally road atlas.</Abstract>
+	</Style>
+      </Layer>
+      <Layer queryable="1">
+	<Name>RIVERS_1M</Name>
+	<Title>Rivers at 1:1M scale</Title>
+	<Abstract>Rivers at a scale of 1 to 1 million.</Abstract>
+	<Keywords>river canal water</Keywords>
+	<DataURL>http://www.opengis.org?rivers.xml</DataURL>
+      </Layer>
+    </Layer>
+    <Layer queryable="1">
+      <Title>Weather Data</Title>
+      <SRS>EPSG:4326</SRS> <!-- harmless repetition of common SRS -->
+      <LatLonBoundingBox minx="-180" miny="-90" maxx="180" maxy="90" />
+      <Style>
+        <Name>default</Name>
+        <Title>Default style</Title>
+        <Abstract>Weather Data are only available in a single default style.</Abstract>
+      </Style>
+      <Layer>
+	<Name>Clouds</Name> 
+	<Title>Forecast cloud cover</Title>
+       </Layer>
+      <Layer>
+	<Name>Temperature</Name> 
+	<Title>Forecast temperature</Title>
+       </Layer>
+      <Layer>
+	<Name>Pressure</Name> 
+	<Title>Forecast barometric pressure</Title>
+       </Layer>
+    </Layer>
+  </Layer>
+</Capability>
+</WMT_MS_Capabilities>
+
diff --git a/mapproxy/test/schemas/wms/1.0.7/capabilities_1_0_7.dtd b/mapproxy/test/schemas/wms/1.0.7/capabilities_1_0_7.dtd
new file mode 100644
index 0000000..382f845
--- /dev/null
+++ b/mapproxy/test/schemas/wms/1.0.7/capabilities_1_0_7.dtd
@@ -0,0 +1,524 @@
+<!-- NOTE: comments in this Document Type Definition impose additional
+constraints beyond those codified in the DTD syntax.  A conformant Web Map
+Server must provide Capabilities XML that (1) validates against the DTD and
+(2) does not violate the constraints stated in comments herein. -->
+
+<!-- The parent element of the Capabilities document includes as children a
+Service element with general information about the server and a Capability
+element with specific information about the kinds of functionality offered by
+the server. -->
+<!ELEMENT WMT_MS_Capabilities (Service, Capability) >
+
+<!-- The version attribute specifies the specification revision to which this
+DTD applies.  Its format is one, two or three integers separated by periods:
+"x", or "x.y", or "x.y.z", with the most significant number appearing first.
+Future revisions are guaranteed to be numbered in monotonically increasing
+fashion, though gaps may appear in the sequence.  All known versions may
+be found at http://www.digitalearth.gov/wmt/xml/ -->
+<!-- The updateSequence attribute is a sequence number for managing
+propagation of the contents of this document.  For example, if a Map Server
+adds some data layers it can increment the update sequence to inform catalog
+servers that their previously cached versions are now stale.  The format is a
+positive integer. -->
+<!ATTLIST WMT_MS_Capabilities
+	  version CDATA #FIXED "1.0.7"
+	  updateSequence CDATA "0">
+
+<!-- This WMT-wide list of possible output formats can be redefined by
+individual servers; see the sample XML. -->
+<!ENTITY % KnownFormats " GIF | JPEG | PNG | WebCGM | SVG | HDF-EOS
+              | GML.1 | GML.2 | GML.3 | WBMP
+              | WMS_XML | MIME | INIMAGE | TIFF | GeoTIFF | PPM | BLANK " >
+
+<!-- The Service element provides metadata for the service as a whole. -->
+<!ELEMENT Service (Name, Title, Abstract?, KeywordList?, OnlineResource,
+                   ContactInformation, Fees?, AccessConstraints?) >
+
+<!-- A service name defined within the Web Mapping Specification namespace.
+Currently only the name "GetMap" is defined. -->
+<!ELEMENT Name (#PCDATA) >
+
+<!-- A human-readable title to briefly identify this server in menus. -->
+<!ELEMENT Title (#PCDATA) >
+
+<!-- A descriptive narrative for more information about this server. -->
+<!ELEMENT Abstract (#PCDATA) > 
+
+<!-- List of keywords or keyword phrases to help catalog searching.
+Currently, no controlled vocabulary has been defined. -->
+<!ELEMENT KeywordList (Keyword*) >
+
+<!-- A single keyword or phrase. -->
+<!ELEMENT Keyword (#PCDATA) >
+
+<!-- An HTTP URL of a service.  This may appear in several places:
+- The Service element describing the service as a whole (in which case the
+  OnlineResource may be, for example, the top-level "home page" of the service
+  provider).
+- The URLs for HTTP GET and POST requests of the specific interfaces offered
+  by the service (e.g., the URL to use for a GetMap request).
+- The Attribution information for Layer(s).
+The use of an xlink:href attribute implies the HTTP Distributed Computing Platform;
+currently, no non-HTTP platforms have been specified. -->
+<!ELEMENT OnlineResource EMPTY>
+<!ATTLIST OnlineResource xlink:href CDATA #REQUIRED
+                         xmlns:xlink CDATA #FIXED "http://www.w3.org/1999/xlink" >
+
+<!ELEMENT ContactInformation  (ContactPersonPrimary?, ContactPosition?,
+                               ContactAddress?, ContactVoiceTelephone?,
+                               ContactFacsimileTelephone?,
+                               ContactElectronicMailAddress?) >
+
+<!--The primary contact person.-->
+<!ELEMENT ContactPersonPrimary  (ContactPerson, ContactOrganization) >
+
+<!--The person to contact.-->
+<!ELEMENT ContactPerson  (#PCDATA) >
+
+<!--The organization supplying the service.-->
+<!ELEMENT ContactOrganization  (#PCDATA) >
+
+<!ELEMENT ContactPosition  (#PCDATA) >
+
+<!--The address for the contact supplying the service.-->
+<!ELEMENT ContactAddress  (AddressType,Address,City,StateOrProvince,PostCode,
+               Country) >
+
+<!--The type of address.-->
+<!ELEMENT AddressType  (#PCDATA) >
+
+<!--The street address.-->
+<!ELEMENT Address  (#PCDATA) >
+
+<!--The address city.-->
+<!ELEMENT City  (#PCDATA) >
+
+<!--The state or province.-->
+<!ELEMENT StateOrProvince  (#PCDATA) >
+
+<!--The zip or postal code.-->
+<!ELEMENT PostCode  (#PCDATA) >
+
+<!--The address country.-->
+<!ELEMENT Country  (#PCDATA) >
+
+<!--Contact telephone number.-->
+<!ELEMENT ContactVoiceTelephone  (#PCDATA) >
+
+<!--The contact fax number.-->
+<!ELEMENT ContactFacsimileTelephone  (#PCDATA) >
+
+<!--The e-mail address for the contact.-->
+<!ELEMENT ContactElectronicMailAddress  (#PCDATA) >
+
+
+
+<!-- Elements indicating what fees or access constraints are imposed.
+The reserved word "none" indicates no constraint exists. -->
+<!ELEMENT Fees (#PCDATA)>
+<!ELEMENT AccessConstraints (#PCDATA)>
+
+<!-- A Capability lists available request types, how exceptions
+may be reported, and whether any vendor-specific capabilities are defined.  It
+also includes an optional list of map layers available from this server. -->
+
+<!-- The optional VendorSpecificCapabilities element lists any capabilities
+unique to a particular Map Server.  Because the information is not known a
+priori, it cannot be constrained by this particular DTD.  A vendor-specific
+DTD fragment must be supplied at the start of the XML Capabilities document,
+after the reference to the general WMT_MS_Capabilities DTD.  See the sample
+XML for further information. -->
+
+<!ELEMENT Capability 
+          (Request, Exception?, VendorSpecificCapabilities?,
+	   UserDefinedSymbolization?, Layer?) >
+
+<!-- Available WMT-defined request types are listed here.
+At least one of the values is required, but more than one
+may be given. -->
+<!ELEMENT Request (Map | Coverage | Capabilities | FeatureInfo | DescribeLayer)+ >
+
+<!-- For each request method offered by the server, list the
+available output formats and the supported distributed
+computing platforms (DCPs).  (The xmlns:xlink attribute is a
+required XML namespace declaration; leave it as-is.)  Example:
+    <Map>
+      <Format><PNG /><JPEG /><GML.1 /></Format>
+      <DCPType><HTTP><Get>
+        <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+         xlink:href="URL" />
+      </Get></HTTP></DCPType>
+    </Map>
+ -->
+
+<!-- GetMap interface: Presence of the Map element means this server can
+generate a map of a specified area, either as a picture or a feature
+collection -->
+<!ELEMENT Map (Format, DCPType+)>
+
+<!-- GetCoverage interface: Presence of the Coverage element means this server can
+serve numeric values representing gridded coverage data over a specified area -->
+<!ELEMENT Coverage (Format, DCPType+)>
+
+<!-- GetCapabilities interface: Presence of the Capabilities element means
+this server can generate a description of its abilities and holdings formatted
+in XML that complies with this DTD. -->
+<!ELEMENT Capabilities (Format, DCPType+)>
+
+<!-- GetFeatureInfo interface: Presence of the FeatureInfo element means this
+server can return information about a specific feature given a valid map
+request and a location on that map. -->
+<!ELEMENT FeatureInfo (Format, DCPType+)>
+
+<!-- DescribeLayer interface: Presence of the DescribeLayer
+element means this server can return describe a layer
+according to the SLD spec [help me out with the right
+verbiage here - no chance to plumb SLD just now] -->
+<!ELEMENT DescribeLayer (Format, DCPType+)>
+
+<!-- Available Distributed Computing Platforms (DCPs) are
+listed here.  At present, only HTTP is defined. -->
+<!ELEMENT DCPType (HTTP) >
+
+<!-- Available HTTP request methods. -->
+<!ELEMENT HTTP (Get | Post)+ >
+
+<!-- HTTP request methods.  The onlineResource attribute indicates the URL
+prefix for HTTP GET requests (everything before the question mark and query
+string: http://hostname[:port]/path/scriptname); for HTTP POST requests,
+onlineResource is the complete URL.  The HTTP GET syntax for Map, Capabilities
+and FeatureInfo requests has been well defined and is described in the OGC
+Web Map Server Interface Specification.  The POST formalism, wherein GetMap
+arguments are encoded in XML and POSTed to the server, has not yet been fully
+developed. -->
+<!ELEMENT Get (OnlineResource) >
+<!ELEMENT Post (OnlineResource) >
+
+<!-- Available formats.  Not all formats are relevant to all requests.
+Individual servers MAY add new formats as shown in the sample XML accompanying
+this DTD. -->
+<!ELEMENT Format ( %KnownFormats; )+ >
+<!ELEMENT GIF EMPTY> <!-- Graphics Interchange Format -->
+<!ELEMENT JPEG EMPTY> <!-- Joint Photographics Expert Group -->
+<!ELEMENT PNG EMPTY> <!-- Portable Network Graphics -->
+<!ELEMENT PPM EMPTY> <!-- Portable PixMap -->
+<!ELEMENT TIFF EMPTY> <!-- Tagged Image File Format -->
+<!ELEMENT GeoTIFF EMPTY> <!-- Geographic TIFF -->
+<!ELEMENT HDF-EOS EMPTY> <!-- Hierarchical Data Format for Earth Observing System -->
+<!ELEMENT WebCGM EMPTY> <!-- Web Computer Graphics Metafile -->
+<!ELEMENT SVG EMPTY>  <!-- Scalable Vector Graphics -->
+<!ELEMENT WMS_XML EMPTY> <!-- eXtensible Markup Language -->
+<!ELEMENT GML.1 EMPTY> <!-- Geography Markup Language, profile 1 -->
+<!ELEMENT GML.2 EMPTY> <!-- Geography Markup Language, profile 2 -->
+<!ELEMENT GML.3 EMPTY> <!-- Geography Markup Language, profile 3 -->
+<!ELEMENT WBMP EMPTY> <!-- Wireless Access Protocol (WAP) Bitmap -->
+<!ELEMENT MIME EMPTY> <!-- Multipurpose Internet Mail Extensions -->
+<!ELEMENT INIMAGE EMPTY> <!-- display text in the returned image -->
+<!ELEMENT BLANK EMPTY> <!-- return an image with all pixels transparent if
+                            supported by the image format, otherwise all
+                            pixels set to the BGCOLOR if present, otherwise
+                            all pixels set to the same (arbitrary) value -->
+
+<!-- An Exception element indicates which output formats are supported
+for reporting problems encountered when executing a request.  Available
+Exception formats MUST include one or more of WMS_XML, INIMAGE, or BLANK.
+Example: <Exception><Format><INIMAGE /><WMS_XML /></Format></Exception>. -->
+<!ELEMENT Exception (Format)>
+
+<!-- Optional user-defined symbolization -->
+<!ELEMENT UserDefinedSymbolization EMPTY >
+<!ATTLIST UserDefinedSymbolization
+          SupportSLD (0 | 1) "1"
+          UserLayer (0 | 1) "1"
+          UserStyle (0 | 1) "1"
+          RemoteWFS (0 | 1) "1" >
+
+<!-- Nested list of zero or more map Layers offered by this server.  The Layer
+element can be omitted if the server has no layers (for example, if it offers
+only geoprocessing services but no actual data). -->
+
+<!-- A Layer element has two functions: it either refers to a map layer which
+can be requested by Name in the LAYERS parameter of a GetMap request, or it is
+a category Title for all the layers nested within.  In the latter case, the
+category itself MAY include a Name by which all of the nested layers can be
+requested at once.  For example, a parent layer "Roads" may have children
+"Interstates" and "County Roads" and allow the user to request either child
+individually or both together. -->
+
+<!-- A Map Server which advertises a Layer containing a Name element MUST be
+able to accept that Name as the value of LAYERS argument in a GetMap request
+and return the corresponding map.  A Viewer Client MUST NOT attempt to request
+a Layer that has a Title but no Name. -->
+
+<!-- A Map Server MUST include at least one <Layer> element for each map layer
+offered.  If desired, data layers MAY be repeated in different categories when
+relevant.  A Layer element MAY state the Name by which a map of the layer is
+requested, MUST give a Title to be used in human-readable menus, and MAY
+include: a human-readable Abstract containing further description, available
+Spatial Reference Systems (SRS), bounding boxes in Lat/Lon and SRS-specific
+coordinates indicating the available geographic coverage, styles in which the
+layer is available, a URL for more information about the data, and a hint
+concerning appropriate map scales for displaying this layer.  Use of the
+nesting hierarchy is optional.  -->
+
+<!-- The following table specifies the number and source of the various
+elements (and one attribute) describing a Layer that has a Name.  Without a
+Name, the Layer is merely a category title and all other elements are
+optional; if present, some of those elements may be inherited by children as
+described in the table.
+
+                       inherit
+                        from
+element         number parent?  comments
+=======         ====== =======  ========
+Name              1      -      this table only applies to Named Layers
+                                (those which can be requested in a GetMap call)
+                                no default
+
+Title             1      -      required in each Layer; no default
+
+Abstract          0/1    -      optional; no default
+
+KeywordList       0/1    -      optional; no default
+
+SRS               1     add     one list is required; default from parent;
+                                list may include multiple whitespace-separated
+                                values
+
+LatLonBoundingBox 1    replace  exactly one is required; default from parent
+
+BoundingBox       0+   replace  one is required per SRS other than EPSG:4326;
+                                default from parent
+
+Dimension         0+    add     optional; default from parent
+
+Extent            0+   replace  optional; default from parent; no more than one per
+                                declared Dimension
+
+Attribution       0/1  replace  optional; default from parent
+
+MetadataURL       0+     -      optional; no default
+
+DataURL           0/1    -      optional; no default
+
+FeatureListURL    0/1    -      optional; no default
+
+Style             0+    add     optional; default from parent
+
+ScaleHint         0/1  replace  optional; default from parent
+
+Layer             0+     -      optional; no default;
+                                if present, each is a "child" of the enclosing
+                                Layer and inherits some default values as
+                                described in this table
+
+queryable         1    replace  optional; default is "0" (not queryable)
+(attribute)                     or parent value if present
+
+
+-->
+
+<!ELEMENT Layer ( Name?, Title, Abstract?, KeywordList?, SRS?,
+                  LatLonBoundingBox?, BoundingBox*, Dimension*, Extent*,
+                  Attribution?, MetadataURL*, DataURL?, FeatureListURL?,
+                  Style*, ScaleHint?, Layer* ) >
+
+<!-- Layer attributes (default=0):
+* If queryable=1, then WMS implements the GetFeatureInfo
+  operation on this Layer.
+* If opaque=1, then this layer is an area-filling coverage
+  with significant areas that cannot be made transparent.
+* If noSubsets=1, then this layer cannot be displayed using a
+  bounding box values different than the ones declared in this XML.
+* If fixedWidth and fixedHeight are nonzero, then the Layer is
+  only available in a single size.
+-->
+<!ATTLIST Layer queryable (0 | 1) "0"
+                opaque (0 | 1) "0"
+                noSubsets (0 | 1) "0"
+                fixedWidth CDATA #IMPLIED
+                fixedHeight CDATA #IMPLIED >
+          
+<!-- Listing of available Spatial Reference Systems (SRS).
+* The root Layer element must include a list of all SRSes which
+  are common to *all* subsidiary layers.  This allows clients
+  to quickly determine that a server cannot possibly satisfy a
+  request for a particular SRS.  Use an empty element if there
+  is no common SRS.
+* Layer-specific SRS: Optionally, layers may add to the global SRS list, or to
+  the list inherited from a parent layer as described above.
+  Any duplication should be ignored by clients.  That is, a particular
+  layer may list all of its SRSes, even if it repeats
+  information in the top-level SRS.
+
+The content of the SRS element is a free-text list of SRS names separated by
+whitespace.  A name includes a namespace prefix, a colon, and one or more
+parameter values.  Currently defined namespaces are:
+
+prefix  parameters    comment
+======  ==========    =======
+EPSG    EPSG code     European Petroleum Survey Group geodesy parameters
+                      http://www.petroconsultants.com/products/geodetic.html
+                      Examples: 'EPSG:4326' is WGS84 lat/lon,
+                      'EPSG:26986' is NAD83 / Massachusetts Mainland.
+
+AUTO	WMT code,     WMT list of "automatic" projections.  WMT code:
+        EPSG units,   an integer identifier assigned by WMT (see
+        Longitude,    http://www.digitalearth.gov/wmt/auto.html). EPSG units:
+        Latitude      one of the EPSG codes for identifying units,
+                      indicating what units are to be used in Bounding Boxes.
+                      Longitude: central meridian of the projection (degrees).
+                      Latitude: central latitude of the projection (degrees).
+                      Example: 'AUTO:42003,9001,-100,45' is auto orthographic
+                      projection, bbox units in meters, center at 100W 45N.
+                      The bounding box is measured in a plane perpendicular to
+                      the line of sight, *not* directly on the Earth.
+-->
+
+<!ELEMENT SRS (#PCDATA) >
+
+<!-- The LatLonBoundingBox attributes indicate the edges of the enclosing
+rectangle in latitude/longitude decimal degrees (as in SRS EPSG:4326 [WGS1984
+lat/lon]).  LatLonBoundingBox MUST be supplied regardless of what SRS the map
+server may support, but it MAY be approximate if EPSG:4326 is not supported.
+Its purpose is to facilitate geographic searches without requiring coordinate
+transformations by the search engine. -->
+<!ELEMENT LatLonBoundingBox EMPTY>
+<!ATTLIST LatLonBoundingBox 
+          minx CDATA #REQUIRED
+          miny CDATA #REQUIRED
+          maxx CDATA #REQUIRED
+          maxy CDATA #REQUIRED>
+
+<!-- The BoundingBox attributes indicate the edges of the bounding box
+in units of the specified spatial reference system. -->
+<!ELEMENT BoundingBox EMPTY>
+<!ATTLIST BoundingBox 
+          SRS CDATA #REQUIRED
+          minx CDATA #REQUIRED
+          miny CDATA #REQUIRED
+          maxx CDATA #REQUIRED
+          maxy CDATA #REQUIRED>
+
+<!-- The Dimension element declares the _existence_ of a dimension in an
+abstract sense for the enclosed Layer(s).  See
+http://www.digitalearth.gov/wmt/md/ for details. -->
+<!ELEMENT Dimension EMPTY >
+<!ATTLIST Dimension
+          name CDATA #REQUIRED
+          units CDATA #REQUIRED
+          unitSymbol CDATA #IMPLIED>
+
+<!-- The Extent element indicates what _values_ along a dimension are
+appropriate for the enclosed Layer(s).  See
+http://www.digitalearth.gov/wmt/md/ for details.  The 'name' attribute must
+have been defined within a Dimension element.  The 'name' can be used
+case-insensitively in a GetMap request so that Clients can request a specific
+value.  The 'default' attribute indicates what value will be used if none is
+specified by the Client.  A 'default' is strongly recommended but optional.
+If there is no declared default the Server MUST NOT send a default map;
+instead, the Server MUST raise an Exception to indicate that a value was
+required. -->
+<!ELEMENT Extent (#PCDATA) >
+<!ATTLIST Extent
+          name CDATA #REQUIRED
+          default CDATA #IMPLIED>
+
+<!-- A Map Server MAY use Attribution to indicate and give credit to 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.  The LogoURL width and height
+assist client applications in laying out space to display the logo. -->
+<!ELEMENT Attribution ( Title?, OnlineResource?, LogoURL? )>
+<!ELEMENT LogoURL (#PCDATA) >
+<!ATTLIST LogoURL
+          width NMTOKEN #REQUIRED
+          height NMTOKEN #REQUIRED>
+
+<!-- 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
+attribute indicates how the metadata is structured.  Two types are defined at
+present: 'TC211' = ISO TC211 19115; 'FGDC' = FGDC CSDGM. -->
+<!ELEMENT MetadataURL (#PCDATA) >
+<!ATTLIST MetadataURL
+          type ( TC211 | FGDC ) #REQUIRED
+          format ( XML | SGML | TXT ) #REQUIRED>
+
+<!-- A Map Server MAY use DataURL to offer more information about the data
+underneath a particular layer. While the semantics are not well-defined, as
+long as the results of an HTTP GET request against the DataURL are properly
+MIME-typed, Viewer Clients and Cascading Map Servers can make use of this. -->
+<!ELEMENT DataURL (#PCDATA) >
+
+<!-- A Map Server MAY use FeatureListURL to point to a GML-encoded enumeration
+of the features represented in a Layer.  An attribute indicates the format
+of the feature list. -->
+<!ELEMENT FeatureListURL (#PCDATA) >
+<!ATTLIST FeatureListURL
+          format ( %KnownFormats; ) #REQUIRED>
+
+<!-- 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.  If a Layer is
+offered in only a single Style, the Map Server MAY choose not to give it a
+name.  Nevertheless, when handling a GetMap request a server MUST accept ''
+(null) and 'default' in the STYLES parameter as synonyms for the default (or
+only) style. -->
+<!ELEMENT Style ( Name, Title, Abstract?,
+                  LegendURL*, StyleSheetURL?, StyleURL? ) >
+
+<!-- A Map Server MAY use zero or more LegendURL elements to provide an
+image(s) of a legend, or a legend encoded as GML, relevant to each Style of a
+Layer.  An attribute indicates the media type of the legend; optional width
+and height are encouraged for image types to assist client applications in
+laying out space to display the legend. -->
+<!ELEMENT LegendURL (#PCDATA) >
+<!ATTLIST LegendURL
+          format ( %KnownFormats; ) #REQUIRED
+          width NMTOKEN #IMPLIED
+          height NMTOKEN #IMPLIED>
+
+<!-- A Map Server MAY use StyleSheeetURL to provide symbology information for
+each Style of a Layer encoded as XML (e.g., in GML format).  An attribute
+indicates the type of stylesheet at the specified URL; only XSL is presently
+defined. -->
+<!ELEMENT StyleSheetURL (#PCDATA) >
+<!ATTLIST StyleSheetURL
+          format ( XSL ) #REQUIRED>
+
+<!-- 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. -->
+<!ELEMENT StyleURL (#PCDATA) >
+
+<!-- Minimum and maximum scale hints for which it is appropriate to
+display this layer. It is STRONGLY RECOMMENDED that for Picture Case
+return formats the min and max values be expressed as ground distance
+in meters of a southwest to northeast diagonal of the pixel whose X
+coordinate is floor(width/2) and whose Y coordinate is floor(height/2).
+
+In the figure below, the X is in the pixel whose diagonal measurement
+is used.
+
+   +===+===+===+      +===+===+===+===+
+   |   |   |   |      |   |   |   |   |
+   +===+===+===+      +===+===+===+===+
+   |   | X |   |      |   | X |   |   |
+   +===+===+===+      +===+===+===+===+
+   |   |   |   |      |   |   |   |   |
+   +===+===+===+      +===+===+===+===+
+                      |   |   |   |   |
+                      +===+===+===+===+
+
+It is understood that this definition is not geodetically precise, but
+at the same time the hope is that by including it, conventions will
+develop around its use which can be later specified more clearly.  -->
+<!ELEMENT ScaleHint EMPTY>
+<!ATTLIST ScaleHint min CDATA #REQUIRED max CDATA #REQUIRED>
+
+
+
diff --git a/mapproxy/test/schemas/wms/1.0.7/capabilities_1_0_7.xml b/mapproxy/test/schemas/wms/1.0.7/capabilities_1_0_7.xml
new file mode 100644
index 0000000..6557d47
--- /dev/null
+++ b/mapproxy/test/schemas/wms/1.0.7/capabilities_1_0_7.xml
@@ -0,0 +1,260 @@
+<?xml version='1.0' encoding="UTF-8" standalone="no" ?>
+<!-- The DTD (Document Type Definition) given here must correspond to the version number declared in the WMT_MS_Capabilities element below. -->
+<!DOCTYPE WMT_MS_Capabilities SYSTEM
+ "http://www.digitalearth.gov/wmt/xml/capabilities_1_0_7.dtd"
+ [
+
+ <!-- Output formats known to this server are defined here
+if necessary (only if a format not already listed in the WMT
+Capabilities DTD is needed).  To define a new format, place
+an entity definition for KnownFormats like the one below in
+the DOCTYPE declaration of your Capabilities XML, listing at
+minimum all of the formats you support and separating each
+by logical-OR (|) characters.  Then, define a new element
+for any formats not predefined by WMT.  For example, in the
+following list "SGI" is a server-specific format, while all
+the others are known WMT-wide.  Thus, SGI is included in the
+KnownFormats list and a new empty element <SGI/> is
+defined. -->
+<!ENTITY % KnownFormats " SGI | GIF | JPEG | PNG | WebCGM | SVG | GML.1
+ | WMS_XML | MIME | INIMAGE | PPM | BLANK " >
+<!ELEMENT SGI EMPTY> <!-- Silicon Graphics RGB Format -->
+
+ <!-- other vendor-specific elements defined here -->
+ <!ELEMENT VendorSpecificCapabilities EMPTY>
+
+ ]>  <!-- end of DOCTYPE declaration -->
+
+<!-- The version number listed in the WMT_MS_Capabilities element here must correspond to the DTD declared above.  See the WMT specification document for how to respond when a client requests a version number not implemented by the server. -->
+<WMT_MS_Capabilities version="1.0.7" updateSequence="0">
+<!-- Service Metadata -->
+<Service>
+  <!-- The WMT-defined name for this type of service -->
+  <Name>GetMap</Name>
+  <!-- Human-readable title for pick lists -->
+  <Title>Acme Corp. Map Server</Title>
+  <!-- Narrative description providing additional information -->
+  <Abstract>WMT Map Server maintained by Acme Corporation.  Contact: webmaster at wmt.acme.com.  High-quality maps showing roadrunner nests and possible ambush locations.</Abstract>
+  <KeywordList>
+    <Keyword>bird</Keyword>
+    <Keyword>roadrunner</Keyword>
+    <Keyword>ambush</Keyword>
+  </KeywordList>
+  <!-- Top-level web address of service or service provider.  See also OnlineResource
+  elements under <DCPType>. -->
+  <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+   xlink:href="http://hostname/" />
+  <!-- Contact information -->
+  <ContactInformation>
+    <ContactPersonPrimary>
+      <ContactPerson>Jeff deLaBeaujardiere</ContactPerson>
+      <ContactOrganization>NASA</ContactOrganization>
+    </ContactPersonPrimary>
+    <ContactPosition>Computer Scientist</ContactPosition>
+    <ContactAddress>
+      <AddressType>postal</AddressType>
+      <Address>NASA Goddard Space Flight Center, Code 933</Address>
+      <City>Greenbelt</City>
+      <StateOrProvince>MD</StateOrProvince>
+      <PostCode>20771</PostCode>
+      <Country>USA</Country>
+    </ContactAddress>
+    <ContactVoiceTelephone>+1 301 286-1569</ContactVoiceTelephone>
+    <ContactFacsimileTelephone>+1 301 286-1777</ContactFacsimileTelephone>
+    <ContactElectronicMailAddress>delabeau at iniki.gsfc.nasa.gov</ContactElectronicMailAddress>
+  </ContactInformation>
+  <!-- Fees or access constraints imposed. -->
+  <Fees>none</Fees>
+  <AccessConstraints>none</AccessConstraints>
+</Service>
+<Capability>
+  <Request>
+    <Map>
+      <Format>
+	<SGI />
+        <GIF />
+        <JPEG />
+        <PNG />
+        <GML.1 />
+      </Format>
+      <DCPType>
+        <HTTP>
+          <Get>
+            <!-- The URL here for invoking GetMap using HTTP GET includes
+            only the prefix before the '?' and the query string.  Clients are
+            expected to append '?WMTVER=nnn&request=map&etc' as described in
+            the WMS specification. -->
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:href="http://hostname:port/path" />
+          </Get>
+        </HTTP>
+      </DCPType>
+    </Map>
+    <DescribeLayer>
+      <Format>
+        <!-- what is a valid format? -->
+        <GML.1 />
+      </Format>
+      <DCPType>
+        <HTTP>
+          <Get>
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:href="http://hostname:port/path" />
+          </Get>
+        </HTTP>
+      </DCPType>
+    </DescribeLayer>
+    <Capabilities>
+      <Format>
+        <WMS_XML />
+      </Format>
+      <DCPType>
+        <HTTP>
+          <Get>
+            <!-- The URL here for invoking GetCapabilities using HTTP GET
+            includes only the prefix before the '?' and the query string.
+            Clients are expected to append '?WMTVER=nnn&request=capabilities'
+            as described in the WMS specification. -->
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:href="http://hostname:port/path" />
+          </Get>
+          <Post>
+            <!-- The URL here for invoking GetCapabilities using HTTP POST
+            includes the complete address to which a query would be sent in
+            XML format.  Not all Map Servers support POST. -->
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:href="http://hostname:port/path" />
+          </Post>
+        </HTTP>
+      </DCPType>
+    </Capabilities>
+    <FeatureInfo>
+      <Format>
+        <MIME />
+        <GML.1 />
+      </Format>
+      <DCPType>
+        <HTTP>
+          <Get>
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:href="http://hostname:port/path" />
+          </Get>
+        </HTTP>
+      </DCPType>
+    </FeatureInfo>
+  </Request>
+  <Exception>
+    <Format>
+      <BLANK />
+      <WMS_XML />
+    </Format>
+  </Exception>
+  <!-- Any text or markup is allowed here, as required to describe
+       vendor-specific capabilities.  Please define elements and attributes
+       in the DOCTYPE declaration at the start of the document. -->
+  <!-- This example is empty because no VSPs were defined in preamble -->
+  <VendorSpecificCapabilities />
+  <UserDefinedSymbolization SupportSLD="1" UserLayer="1" UserStyle="1"
+	RemoteWFS="1" />
+  <Layer>
+    <Title>Acme Corp. Map Server</Title>
+    <SRS>EPSG:4326</SRS> <!-- all layers are available in at least this SRS -->
+    <Layer>
+      <!-- This parent layer has a Name and can therefore be requested from a Map Server, yielding a map of all subsidiary layers. -->
+      <Name>ROADS_RIVERS</Name> 
+      <Title>Roads and Rivers</Title>
+      <!-- The following characteristics are inherited by subsidiary layers. -->
+      <SRS>EPSG:26986</SRS> <!-- An additional SRS for this layer --> 
+      <LatLonBoundingBox minx="-71.634696" miny="41.754149" maxx="-70.789798" maxy="42.908459"/>
+      <BoundingBox SRS="EPSG:26986" minx="189000" miny="834000" maxx="285000" maxy="962000"/>
+      <Attribution>
+        <Title>State College University</Title>
+        <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+         xlink:href="http://www.university.edu/" />
+        <LogoURL width="100" height="100">http://www.university.edu/icons/logo.gif</LogoURL>
+      </Attribution>
+      <FeatureListURL format="GML.1">http://www.university.edu/data/roads_rivers.gml</FeatureListURL>
+      <Style>
+        <Name>USGS</Name>
+        <Title>USGS Topo Map Style</Title>
+        <Abstract>Features are shown in a style like that used in USGS topographic maps.</Abstract>
+        <!-- A picture of a legend for a Layer in this Style -->
+        <LegendURL format="GIF" width="72" height="72">http://www.university.edu/legends/usgs.gif</LegendURL>
+        <!-- An XML (GML) encoding of the legend for GML-enabled clients -->
+        <LegendURL format="GML.1">http://www.university.edu/legends/usgs.gml</LegendURL>
+        <!-- An XSL stylesheet for rendering this layer in this style when requested in GML format -->	
+        <StyleSheetURL format="XSL">http://www.university.edu/stylesheets/usgs.xsl</StyleSheetURL>
+      </Style>
+      <ScaleHint min="4000" max="35000"></ScaleHint>
+      <Layer queryable="1">
+	<Name>ROADS_1M</Name> 
+	<Title>Roads at 1:1M scale</Title>
+	<Abstract>Roads at a scale of 1 to 1 million.</Abstract>
+	<KeywordList>
+          <Keyword>road</Keyword>
+          <Keyword>transportation</Keyword>
+          <Keyword>atlas</Keyword>
+	</KeywordList>
+        <!-- Metadata specific to this particular layer.  The same FGDC metadata is offered in two formats. -->
+	<MetadataURL type="FGDC" format="TXT">http://www.university.edu/fgdc/clearinghouse/metadata/roads.txt</MetadataURL>
+	<MetadataURL type="FGDC" format="XML">http://www.university.edu/fgdc/clearinghouse/metadata/roads.xml</MetadataURL>
+        <!-- In addition to the Style specified in the parent Layer, this Layer is available in this style. -->
+	<Style>
+	  <Name>ATLAS</Name>
+	  <Title>Road atlas style</Title>
+	  <Abstract>Roads are shown in a style like that used in a commercial road atlas.</Abstract>
+        <LegendURL format="GIF" width="72" height="72">http://www.university.edu/legends/atlas.gif</LegendURL>
+	</Style>
+      </Layer>
+      <Layer queryable="1">
+	<Name>RIVERS_1M</Name>
+	<Title>Rivers at 1:1M scale</Title>
+	<Abstract>Rivers at a scale of 1 to 1 million.</Abstract>
+	<KeywordList>
+          <Keyword>river</Keyword>
+          <Keyword>canal</Keyword>
+          <Keyword>waterway</Keyword>
+	</KeywordList>
+      </Layer>
+    </Layer>
+    <Layer queryable="1">
+      <Title>Weather Forecast Data</Title>
+      <SRS>EPSG:4326</SRS> <!-- harmless repetition of common SRS -->
+      <LatLonBoundingBox minx="-180" miny="-90" maxx="180" maxy="90" />
+      <!-- These weather data are available daily from 1999-01-01 through
+           2000-08-22. -->
+      <Dimension name="time" units="ISO8601" />
+      <Extent name="time" default="2000-08-22">1999-01-01/2000-08-22/P1D</Extent>
+      <Layer>
+	<Name>Clouds</Name> 
+	<Title>Forecast cloud cover</Title>
+      </Layer>
+      <Layer>
+	<Name>Temperature</Name> 
+	<Title>Forecast temperature</Title>
+      </Layer>
+      <Layer>
+	<Name>Pressure</Name> 
+	<Title>Forecast barometric pressure</Title>
+        <!-- Pressure is available at several elevations.
+         EPSG:5030 is WGS 84 ellipsoid, units in metres.
+         Pressure is also available at several times.
+         NOTE: first list all Dimension elements, then all Extent elements. -->
+         <Dimension name="time" units="ISO8601" />
+         <Dimension name="elevation" units="EPSG:5030" />
+         <Extent name="time" default="2000-08-22">1999-01-01/2000-08-22/P1D</Extent>
+         <Extent name="elevation" default="0">0,1000,3000,5000,10000</Extent>
+      </Layer>
+    </Layer>
+    <!-- Example of a layer which is a static map of fixed
+         size which the server cannot subset or make transparent -->
+    <Layer opaque="1" noSubsets="1" fixedWidth="512" fixedHeight="256">
+      <Name>ozone_image</Name>
+      <Title>Global ozone distribution (1992)</Title>
+      <LatLonBoundingBox minx="-180" miny="-90" maxx="180" maxy="90" />
+      <Extent name="time" default="1992">1992</Extent>
+    </Layer>
+  </Layer>
+</Capability>
+</WMT_MS_Capabilities>
+
diff --git a/mapproxy/test/schemas/wms/1.1.0/capabilities_1_1_0.dtd b/mapproxy/test/schemas/wms/1.1.0/capabilities_1_1_0.dtd
new file mode 100644
index 0000000..3fe6413
--- /dev/null
+++ b/mapproxy/test/schemas/wms/1.1.0/capabilities_1_1_0.dtd
@@ -0,0 +1,273 @@
+<!ELEMENT WMT_MS_Capabilities (Service, Capability) >
+
+<!ATTLIST WMT_MS_Capabilities
+	  version CDATA #FIXED "1.1.0"
+	  updateSequence CDATA #IMPLIED>
+
+<!-- Elements used in multiple places. -->
+
+<!-- The Name is typically for machine-to-machine communication. -->
+<!ELEMENT Name (#PCDATA) >
+
+<!-- The Title is for informative display to a human. -->
+<!ELEMENT Title (#PCDATA) >
+
+<!-- The abstract is a longer narrative description of an object. -->
+<!ELEMENT Abstract (#PCDATA) > 
+
+<!-- An OnlineResource is typically an HTTP URL.  The URL is placed in the
+xlink:href attribute.  The xmlns:xlink attribute is a required XML namespace
+declaration. -->
+<!ELEMENT OnlineResource EMPTY>
+<!ATTLIST OnlineResource
+          xmlns:xlink CDATA #FIXED "http://www.w3.org/1999/xlink"
+          xlink:type CDATA #FIXED "simple"
+          xlink:href CDATA #REQUIRED >
+
+<!-- A container for listing an available format's MIME type. -->
+<!ELEMENT Format (#PCDATA) >
+
+
+<!-- General service metadata. -->
+
+<!ELEMENT Service (Name, Title, Abstract?, KeywordList?, OnlineResource,
+                   ContactInformation?, Fees?, AccessConstraints?) >
+
+<!-- List of keywords or keyword phrases to help catalog searching. -->
+<!ELEMENT KeywordList (Keyword*) >
+
+<!-- A single keyword or phrase. -->
+<!ELEMENT Keyword (#PCDATA) >
+
+<!-- Information about a contact person for the service. -->
+<!ELEMENT ContactInformation  (ContactPersonPrimary?, ContactPosition?,
+                               ContactAddress?, ContactVoiceTelephone?,
+                               ContactFacsimileTelephone?,
+                               ContactElectronicMailAddress?) >
+
+<!--The primary contact person.-->
+<!ELEMENT ContactPersonPrimary  (ContactPerson, ContactOrganization) >
+
+<!--The person to contact.-->
+<!ELEMENT ContactPerson  (#PCDATA) >
+
+<!--The organization supplying the service.-->
+<!ELEMENT ContactOrganization  (#PCDATA) >
+
+<!--The position title for the contact person.-->
+<!ELEMENT ContactPosition  (#PCDATA) >
+
+<!--The address for the contact supplying the service.-->
+<!ELEMENT ContactAddress  (AddressType,Address,City,StateOrProvince,PostCode,
+               Country) >
+
+<!--The type of address.-->
+<!ELEMENT AddressType  (#PCDATA) >
+
+<!--The street address.-->
+<!ELEMENT Address  (#PCDATA) >
+
+<!--The address city.-->
+<!ELEMENT City  (#PCDATA) >
+
+<!--The state or province.-->
+<!ELEMENT StateOrProvince  (#PCDATA) >
+
+<!--The zip or postal code.-->
+<!ELEMENT PostCode  (#PCDATA) >
+
+<!--The address country.-->
+<!ELEMENT Country  (#PCDATA) >
+
+<!--Contact telephone number.-->
+<!ELEMENT ContactVoiceTelephone  (#PCDATA) >
+
+<!--The contact fax number.-->
+<!ELEMENT ContactFacsimileTelephone  (#PCDATA) >
+
+<!--The e-mail address for the contact.-->
+<!ELEMENT ContactElectronicMailAddress  (#PCDATA) >
+
+
+<!-- Elements indicating what fees or access constraints are imposed. -->
+<!ELEMENT Fees (#PCDATA)>
+<!ELEMENT AccessConstraints (#PCDATA)>
+
+
+<!-- A Capability lists available request types, how exceptions
+may be reported, and whether any vendor-specific capabilities are defined.  It
+also includes an optional list of map layers available from this server. -->
+<!ELEMENT Capability 
+          (Request, Exception, VendorSpecificCapabilities?,
+	   UserDefinedSymbolization?, Layer?) >
+
+<!-- Available WMS Operations are listed in a Request element. -->
+<!ELEMENT Request (GetCapabilities, GetMap,
+                   GetFeatureInfo?, DescribeLayer?) >
+
+<!-- For each operation offered by the server, list the available output
+formats and the online resource. -->
+
+<!ELEMENT GetCapabilities (Format+, DCPType+)>
+
+<!ELEMENT GetMap (Format+, DCPType+)>
+
+<!ELEMENT GetFeatureInfo (Format+, DCPType+)>
+
+<!ELEMENT DescribeLayer (Format+, DCPType+)>
+
+<!-- Available Distributed Computing Platforms (DCPs) are
+listed here.  At present, only HTTP is defined. -->
+<!ELEMENT DCPType (HTTP) >
+
+<!-- Available HTTP request methods.  One or both may be supported. -->
+<!ELEMENT HTTP (Get | Post)+ >
+
+<!-- URL prefix for each HTTP request method. -->
+<!ELEMENT Get (OnlineResource) >
+<!ELEMENT Post (OnlineResource) >
+
+<!-- An Exception element indicates which error-reporting formats are supported. -->
+<!ELEMENT Exception (Format+)>
+
+<!-- Optional user-defined symbolization (used only by SLD-enabled WMSes). -->
+<!ELEMENT UserDefinedSymbolization EMPTY >
+<!ATTLIST UserDefinedSymbolization
+          SupportSLD (0 | 1) "0"
+          UserLayer (0 | 1) "0"
+          UserStyle (0 | 1) "0"
+          RemoteWFS (0 | 1) "0" >
+
+<!-- Nested list of zero or more map Layers offered by this server. -->
+<!ELEMENT Layer ( Name?, Title, Abstract?, KeywordList?, SRS?,
+                  LatLonBoundingBox?, BoundingBox*, Dimension*, Extent*,
+                  Attribution?, AuthorityURL*, Identifier*, MetadataURL*, DataURL*,
+                  FeatureListURL*, Style*, ScaleHint?, Layer* ) >
+
+<!-- Optional attributes-->
+<!ATTLIST Layer queryable (0 | 1) "0"
+                cascaded CDATA #IMPLIED
+                opaque (0 | 1) "0"
+                noSubsets (0 | 1) "0"
+                fixedWidth CDATA #IMPLIED
+                fixedHeight CDATA #IMPLIED >
+          
+<!-- Listing of available Spatial Reference Systems (SRS). -->
+<!ELEMENT SRS (#PCDATA) >
+
+<!-- The LatLonBoundingBox attributes indicate the edges of the enclosing
+rectangle in latitude/longitude decimal degrees (as in SRS EPSG:4326 [WGS1984
+lat/lon]). -->
+<!ELEMENT LatLonBoundingBox EMPTY>
+<!ATTLIST LatLonBoundingBox 
+          minx CDATA #REQUIRED
+          miny CDATA #REQUIRED
+          maxx CDATA #REQUIRED
+          maxy CDATA #REQUIRED>
+
+<!-- The BoundingBox attributes indicate the edges of the bounding box
+in units of the specified spatial reference system. -->
+<!ELEMENT BoundingBox EMPTY>
+<!ATTLIST BoundingBox 
+          SRS CDATA #REQUIRED
+          minx CDATA #REQUIRED
+          miny CDATA #REQUIRED
+          maxx CDATA #REQUIRED
+          maxy CDATA #REQUIRED
+          resx CDATA #IMPLIED
+          resy CDATA #IMPLIED>
+
+<!-- The Dimension element declares the _existence_ of a dimension. -->
+<!ELEMENT Dimension EMPTY >
+<!ATTLIST Dimension
+          name CDATA #REQUIRED
+          units CDATA #REQUIRED
+          unitSymbol CDATA #IMPLIED>
+
+<!-- The Extent element indicates what _values_ along a dimension are valid. -->
+<!ELEMENT Extent (#PCDATA) >
+<!ATTLIST Extent
+          name CDATA #REQUIRED
+          default CDATA #IMPLIED>
+
+<!-- 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. -->
+<!ELEMENT Attribution ( Title?, OnlineResource?, LogoURL? )>
+<!ELEMENT LogoURL (Format, OnlineResource) >
+<!ATTLIST LogoURL
+          width NMTOKEN #REQUIRED
+          height NMTOKEN #REQUIRED>
+
+<!-- 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.  Two types
+are defined at present: 'TC211' = ISO TC211 19115; 'FGDC' = FGDC CSDGM.  The
+format element indicates how the metadata is structured. -->
+<!ELEMENT MetadataURL (Format, OnlineResource) >
+<!ATTLIST MetadataURL
+          type ( TC211 | FGDC ) #REQUIRED>
+
+<!-- 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. -->
+
+<!ELEMENT AuthorityURL (OnlineResource) >
+<!ATTLIST AuthorityURL
+          name NMTOKEN #REQUIRED >
+<!ELEMENT Identifier (#PCDATA) >
+<!ATTLIST Identifier
+          authority CDATA #REQUIRED >
+
+<!-- A Map Server may use DataURL to offer more information about the data
+underneath a particular layer. While the semantics are not well-defined, as
+long as the results of an HTTP GET request against the DataURL are properly
+MIME-typed, Viewer Clients and Cascading Map Servers can make use of this. -->
+<!ELEMENT DataURL (Format, OnlineResource) >
+
+<!-- A Map Server may use FeatureListURL to point to a list of the features
+represented in a Layer. -->
+<!ELEMENT FeatureListURL (Format, OnlineResource) >
+
+<!-- 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. -->
+<!ELEMENT Style ( Name, Title, Abstract?,
+                  LegendURL*, StyleSheetURL?, StyleURL? ) >
+
+<!-- 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 are
+provided to assist client applications in laying out space to display the
+legend. -->
+<!ELEMENT LegendURL (Format, OnlineResource) >
+<!ATTLIST LegendURL
+          width NMTOKEN #REQUIRED
+          height NMTOKEN #REQUIRED>
+
+<!-- StyleSheeetURL provides symbology information foreach Style of a Layer. -->
+<!ELEMENT StyleSheetURL (Format, OnlineResource) >
+
+<!-- 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. -->
+<!ELEMENT StyleURL (Format, OnlineResource) >
+
+<!-- Minimum and maximum scale hints for which it is appropriate to
+display this layer. -->
+<!ELEMENT ScaleHint EMPTY>
+<!ATTLIST ScaleHint
+          min CDATA #REQUIRED
+          max CDATA #REQUIRED>
+
+
+
diff --git a/mapproxy/test/schemas/wms/1.1.0/capabilities_1_1_0.xml b/mapproxy/test/schemas/wms/1.1.0/capabilities_1_1_0.xml
new file mode 100644
index 0000000..29d913e
--- /dev/null
+++ b/mapproxy/test/schemas/wms/1.1.0/capabilities_1_1_0.xml
@@ -0,0 +1,303 @@
+<?xml version='1.0' encoding="UTF-8" standalone="no" ?>
+<!-- The DTD (Document Type Definition) given here must correspond to the version number declared in the WMT_MS_Capabilities element below. -->
+<!DOCTYPE WMT_MS_Capabilities SYSTEM
+ "http://www.digitalearth.gov/wmt/xml/capabilities_1_1_0.dtd"
+ [
+ <!-- Vendor-specific elements are defined here if needed. -->
+ <!-- If not needed, just leave this EMPTY declaration.  Do not
+  delete the declaration entirely. -->
+ <!ELEMENT VendorSpecificCapabilities EMPTY>
+ ]>  <!-- end of DOCTYPE declaration -->
+
+<!-- Note: this XML is just an EXAMPLE that attempts to show all
+required and optional elements for illustration.  Consult the Web Map
+Service 1.1.0 specification and the DTD for guidance on what to actually
+include and what to leave out. -->
+
+<!-- The version number listed in the WMT_MS_Capabilities element here must
+correspond to the DTD declared above.  See the WMT specification document for
+how to respond when a client requests a version number not implemented by the
+server. -->
+<WMT_MS_Capabilities version="1.1.0" updateSequence="0">
+<!-- Service Metadata -->
+<Service>
+  <!-- The WMT-defined name for this type of service -->
+  <Name>OGC:WMS</Name>
+  <!-- Human-readable title for pick lists -->
+  <Title>Acme Corp. Map Server</Title>
+  <!-- Narrative description providing additional information -->
+  <Abstract>WMT Map Server maintained by Acme Corporation.  Contact: webmaster at wmt.acme.com.  High-quality maps showing roadrunner nests and possible ambush locations.</Abstract>
+  <KeywordList>
+    <Keyword>bird</Keyword>
+    <Keyword>roadrunner</Keyword>
+    <Keyword>ambush</Keyword>
+  </KeywordList>
+  <!-- Top-level web address of service or service provider.  See also OnlineResource
+  elements under <DCPType>. -->
+  <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
+   xlink:href="http://hostname/" />
+  <!-- Contact information -->
+  <ContactInformation>
+    <ContactPersonPrimary>
+      <ContactPerson>Jeff deLaBeaujardiere</ContactPerson>
+      <ContactOrganization>NASA</ContactOrganization>
+    </ContactPersonPrimary>
+    <ContactPosition>Computer Scientist</ContactPosition>
+    <ContactAddress>
+      <AddressType>postal</AddressType>
+      <Address>NASA Goddard Space Flight Center, Code 933</Address>
+      <City>Greenbelt</City>
+      <StateOrProvince>MD</StateOrProvince>
+      <PostCode>20771</PostCode>
+      <Country>USA</Country>
+    </ContactAddress>
+    <ContactVoiceTelephone>+1 301 286-1569</ContactVoiceTelephone>
+    <ContactFacsimileTelephone>+1 301 286-1777</ContactFacsimileTelephone>
+    <ContactElectronicMailAddress>delabeau at iniki.gsfc.nasa.gov</ContactElectronicMailAddress>
+  </ContactInformation>
+  <!-- Fees or access constraints imposed. -->
+  <Fees>none</Fees>
+  <AccessConstraints>none</AccessConstraints>
+</Service>
+<Capability>
+  <Request>
+    <GetCapabilities>
+      <Format>application/vnd.ogc.wms_xml</Format>
+      <DCPType>
+        <HTTP>
+          <Get>
+            <!-- The URL here for invoking GetCapabilities using HTTP GET
+            is only a prefix to which a query string is appended. -->
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:type="simple"
+             xlink:href="http://hostname:port/path" />
+          </Get>
+          <Post>
+            <!-- The URL here for invoking GetCapabilities using HTTP POST
+            includes the complete address to which a query would be sent in
+            XML format.  This is here for future expansion; no POST encoding
+	    has yet been defined. -->
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:type="simple"
+             xlink:href="http://hostname:port/path" />
+          </Post>
+        </HTTP>
+      </DCPType>
+    </GetCapabilities>
+    <GetMap>
+      <Format>image/gif</Format>
+      <Format>image/png</Format>
+      <Format>image/jpeg</Format>
+      <DCPType>
+        <HTTP>
+          <Get>
+            <!-- The URL here for invoking GetCapabilities using HTTP GET
+            is only a prefix to which a query string is appended. -->
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:type="simple"
+             xlink:href="http://hostname:port/path" />
+          </Get>
+        </HTTP>
+      </DCPType>
+    </GetMap>
+    <GetFeatureInfo>
+      <Format>application/vnd.ogc.gml</Format>
+      <Format>text/plain</Format>
+      <Format>text/html</Format>
+      <DCPType>
+        <HTTP>
+          <Get>
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:type="simple"
+             xlink:href="http://hostname:port/path" />
+          </Get>
+        </HTTP>
+      </DCPType>
+    </GetFeatureInfo>
+    <DescribeLayer><!--optional; used only by SLD-enabled WMS-->
+      <Format>application/vnd.ogc.gml</Format>
+      <DCPType>
+        <HTTP>
+          <Get>
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:type="simple"
+             xlink:href="http://hostname:port/path" />
+          </Get>
+        </HTTP>
+      </DCPType>
+    </DescribeLayer>
+  </Request>
+  <Exception>
+    <Format>application/vnd.ogc.se_xml</Format>
+    <Format>application/vnd.ogc.se_inimage</Format>
+    <Format>application/vnd.ogc.se_blank</Format>
+  </Exception>
+  <!-- Any text or markup is allowed here, as required to describe
+       vendor-specific capabilities.  Please define elements and attributes
+       in the DOCTYPE declaration at the start of the document. -->
+  <!-- This example is empty because no VSPs were defined in preamble -->
+  <VendorSpecificCapabilities />
+  <!-- Place-holder for Styled Layer Descriptor (SLD)-enabled WMSes.
+       This element is absent for a basic WMS. -->
+  <UserDefinedSymbolization SupportSLD="1" UserLayer="1" UserStyle="1"
+	RemoteWFS="1" />
+  <Layer>
+    <Title>Acme Corp. Map Server</Title>
+    <SRS>EPSG:4326</SRS> <!-- all layers are available in at least this SRS -->
+    <AuthorityURL name="DIF_ID">
+      <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
+       xlink:href="http://gcmd.gsfc.nasa.gov/difguide/whatisadif.html" />
+    </AuthorityURL>
+    <Layer>
+      <!-- This parent layer has a Name and can therefore be requested from a Map Server, yielding a map of all subsidiary layers. -->
+      <Name>ROADS_RIVERS</Name> 
+      <Title>Roads and Rivers</Title>
+      <!-- See the spec to learn how some characteristics are inherited by
+           subsidiary layers. -->
+      <SRS>EPSG:26986</SRS> <!-- An additional SRS for this layer --> 
+      <LatLonBoundingBox minx="-71.63" miny="41.75" maxx="-70.78" maxy="42.90"/>
+      <!-- The optional resx and resy attributes below indicate the X and Y spatial
+           resolution in the units of that SRS. -->
+      <!-- The EPSG:4326 BoundingBox duplicates some of the info in LatLonBoundingBox
+           and is therefore optional, but using it here allows the additional
+           resolution attributes. -->
+      <BoundingBox SRS="EPSG:4326"
+       minx="-71.63" miny="41.75" maxx="-70.78" maxy="42.90" resx="0.01" resy="0.01"/>
+      <BoundingBox SRS="EPSG:26986"
+       minx="189000" miny="834000" maxx="285000" maxy="962000" resx="1" resy="1" />
+      <!-- Optional Title, URL and logo image of data provider. -->
+      <Attribution>
+        <Title>State College University</Title>
+        <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
+         xlink:href="http://www.university.edu/" />
+        <LogoURL width="100" height="100">
+          <Format>image/gif</Format>
+          <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+           xlink:type="simple"
+           xlink:href="http://www.university.edu/icons/logo.gif" />
+        </LogoURL>
+      </Attribution>
+      <!-- Identifier whose meaning is defined in an AuthorityURL element -->
+      <Identifier authority="DIF_ID">123456</Identifier>
+      <FeatureListURL>
+        <Format>application/vnd.ogc.se_xml"</Format>
+        <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
+         xlink:href="http://www.university.edu/data/roads_rivers.gml" />
+      </FeatureListURL>
+      <Style>
+        <Name>USGS</Name>
+        <Title>USGS Topo Map Style</Title>
+        <Abstract>Features are shown in a style like that used in USGS topographic maps.</Abstract>
+        <!-- A picture of a legend for a Layer in this Style -->
+        <LegendURL width="72" height="72">
+          <Format>image/gif</Format>
+          <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+           xlink:type="simple"
+           xlink:href="http://www.university.edu/legends/usgs.gif" />
+        </LegendURL>
+        <!-- An XSL stylesheet describing how GML feature data will rendered to create
+             a map of this layer. -->
+        <StyleSheetURL>
+          <Format>text/xsl</Format>
+          <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+           xlink:type="simple"
+           xlink:href="http://www.university.edu/stylesheets/usgs.xsl" />
+        </StyleSheetURL>
+      </Style>
+      <ScaleHint min="4000" max="35000"></ScaleHint>
+      <Layer queryable="1">
+	<Name>ROADS_1M</Name> 
+	<Title>Roads at 1:1M scale</Title>
+	<Abstract>Roads at a scale of 1 to 1 million.</Abstract>
+	<KeywordList>
+          <Keyword>road</Keyword>
+          <Keyword>transportation</Keyword>
+          <Keyword>atlas</Keyword>
+	</KeywordList>
+	<Identifier authority="DIF_ID">123456</Identifier>
+        <!-- Metadata specific to this particular layer.  The same FGDC metadata is offered in two formats. -->
+	<MetadataURL type="FGDC">
+          <Format>text/plain</Format>
+          <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+           xlink:type="simple"
+           xlink:href="http://www.university.edu/metadata/roads.txt" />
+        </MetadataURL>
+	<MetadataURL type="FGDC">
+           <Format>text/xml</Format>
+           <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+            xlink:type="simple"
+            xlink:href="http://www.university.edu/metadata/roads.xml" />
+        </MetadataURL>
+        <!-- In addition to the Style specified in the parent Layer, this Layer is available in this style. -->
+	<Style>
+	  <Name>ATLAS</Name>
+	  <Title>Road atlas style</Title>
+	  <Abstract>Roads are shown in a style like that used in a commercial road atlas.</Abstract>
+        <LegendURL width="72" height="72">
+          <Format>image/gif</Format>
+          <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+           xlink:type="simple"
+           xlink:href="http://www.university.edu/legends/atlas.gif" />
+        </LegendURL>
+	</Style>
+      </Layer>
+      <Layer queryable="1">
+	<Name>RIVERS_1M</Name>
+	<Title>Rivers at 1:1M scale</Title>
+	<Abstract>Rivers at a scale of 1 to 1 million.</Abstract>
+	<KeywordList>
+          <Keyword>river</Keyword>
+          <Keyword>canal</Keyword>
+          <Keyword>waterway</Keyword>
+	</KeywordList>
+      </Layer>
+    </Layer>
+    <Layer queryable="1">
+      <Title>Weather Forecast Data</Title>
+      <SRS>EPSG:4326</SRS> <!-- harmless repetition of common SRS -->
+      <LatLonBoundingBox minx="-180" miny="-90" maxx="180" maxy="90" />
+      <!-- These weather data are available daily from 1999-01-01 through
+           2000-08-22. -->
+      <Dimension name="time" units="ISO8601" />
+      <Extent name="time" default="2000-08-22">1999-01-01/2000-08-22/P1D</Extent>
+      <Layer>
+	<Name>Clouds</Name> 
+	<Title>Forecast cloud cover</Title>
+      </Layer>
+      <Layer>
+	<Name>Temperature</Name> 
+	<Title>Forecast temperature</Title>
+      </Layer>
+      <Layer>
+	<Name>Pressure</Name> 
+	<Title>Forecast barometric pressure</Title>
+        <!-- Pressure is available at several elevations.
+         EPSG:5030 is WGS 84 ellipsoid, units in metres.
+         Pressure is also available at several times.
+         NOTE: first list all Dimension elements, then all Extent elements. -->
+         <Dimension name="time" units="ISO8601" />
+         <Dimension name="elevation" units="EPSG:5030" />
+         <Extent name="time" default="2000-08-22">1999-01-01/2000-08-22/P1D</Extent>
+         <Extent name="elevation" default="0">0,1000,3000,5000,10000</Extent>
+      </Layer>
+    </Layer>
+    <!-- Example of a layer which is a static map of fixed
+         size which the server cannot subset or make transparent -->
+    <Layer opaque="1" noSubsets="1" fixedWidth="512" fixedHeight="256">
+      <Name>ozone_image</Name>
+      <Title>Global ozone distribution (1992)</Title>
+      <LatLonBoundingBox minx="-180" miny="-90" maxx="180" maxy="90" />
+      <Extent name="time" default="1992">1992</Extent>
+    </Layer>
+    <!-- Example of a layer which originated from another WMS and has been
+         "cascaded" by this WMS. -->
+    <Layer cascaded="1">
+      <Name>population</Name>
+      <Title>World population, annual</Title>
+      <LatLonBoundingBox minx="-180" miny="-90" maxx="180" maxy="90" />
+      <Extent name="time" default="2000">1990/2000/P1Y</Extent>
+    </Layer>
+  </Layer>
+</Capability>
+</WMT_MS_Capabilities>
+
diff --git a/mapproxy/test/schemas/wms/1.1.0/exception_1_1_0.dtd b/mapproxy/test/schemas/wms/1.1.0/exception_1_1_0.dtd
new file mode 100644
index 0000000..8db0471
--- /dev/null
+++ b/mapproxy/test/schemas/wms/1.1.0/exception_1_1_0.dtd
@@ -0,0 +1,6 @@
+<!ELEMENT ServiceExceptionReport (ServiceException*)>
+<!ATTLIST ServiceExceptionReport version CDATA #FIXED "1.1.0">
+
+<!ELEMENT ServiceException (#PCDATA)>
+<!ATTLIST ServiceException code CDATA #IMPLIED>
+
diff --git a/mapproxy/test/schemas/wms/1.1.0/exception_1_1_0.xml b/mapproxy/test/schemas/wms/1.1.0/exception_1_1_0.xml
new file mode 100644
index 0000000..3c71cc2
--- /dev/null
+++ b/mapproxy/test/schemas/wms/1.1.0/exception_1_1_0.xml
@@ -0,0 +1,33 @@
+<?xml version='1.0' encoding="UTF-8" standalone="no" ?>
+<!DOCTYPE ServiceExceptionReport SYSTEM
+ "http://www.digitalearth.gov/wmt/xml/exception_1_1_0.dtd">
+<ServiceExceptionReport version="1.1.0">
+  <ServiceException>
+    Plain text message about an error.
+  </ServiceException>
+  <ServiceException code="InvalidUpdateSequence">
+    Another message, this time with a SE code supplied.
+  </ServiceException>
+  <ServiceException>
+    <![CDATA[
+    Error in module <foo.c>, 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:
+    ]]>
+  </ServiceException>
+  <ServiceException>
+    <![CDATA[
+      <Module>foo.c</Module>
+      <Error>An error occurred</Error>
+      <Explanation>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.</Explanation>
+    ]]>
+  </ServiceException>
+</ServiceExceptionReport>
diff --git a/mapproxy/test/schemas/wms/1.1.1/OGC-exception.xsd b/mapproxy/test/schemas/wms/1.1.1/OGC-exception.xsd
new file mode 100644
index 0000000..9ed19c2
--- /dev/null
+++ b/mapproxy/test/schemas/wms/1.1.1/OGC-exception.xsd
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xsd:schema
+   targetNamespace="http://www.opengis.net/ogc"
+   xmlns:ogc="http://www.opengis.net/ogc"
+   xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+   elementFormDefault="qualified">
+
+   <xsd:element name="ServiceExceptionReport">
+      <xsd:annotation>
+         <xsd:documentation>
+            The ServiceExceptionReport element contains one
+            or more ServiceException elements that describe
+            a service exception.
+         </xsd:documentation>
+      </xsd:annotation>
+      <xsd:complexType>
+         <xsd:sequence>
+            <xsd:element name="ServiceException"
+                         type="ogc:ServiceExceptionType"
+                         minOccurs="0" maxOccurs="unbounded">
+               <xsd:annotation>
+                  <xsd:documentation>
+                     The Service exception element is used to describe 
+                     a service exception.
+                  </xsd:documentation>
+               </xsd:annotation>
+            </xsd:element>
+         </xsd:sequence>
+         <xsd:attribute name="version" type="xsd:string" fixed="1.2.0"/>
+      </xsd:complexType>
+   </xsd:element>
+
+   <xsd:complexType name="ServiceExceptionType">
+      <xsd:annotation>
+         <xsd:documentation>
+            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.
+         </xsd:documentation>
+      </xsd:annotation>
+      <xsd:simpleContent>
+         <xsd:extension base="xsd:string">
+            <xsd:attribute name="code" type="xsd:string">
+               <xsd:annotation>
+                  <xsd:documentation>
+                     A service may associate a code with an exception
+                     by using the code attribute.
+                  </xsd:documentation>
+               </xsd:annotation>
+            </xsd:attribute>
+            <xsd:attribute name="locator" type="xsd:string">
+               <xsd:annotation>
+                  <xsd:documentation>
+                     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 ...
+                  </xsd:documentation>
+               </xsd:annotation>
+            </xsd:attribute>
+         </xsd:extension>
+      </xsd:simpleContent>
+   </xsd:complexType>
+</xsd:schema>
diff --git a/mapproxy/test/schemas/wms/1.1.1/WMS_DescribeLayerResponse.dtd b/mapproxy/test/schemas/wms/1.1.1/WMS_DescribeLayerResponse.dtd
new file mode 100644
index 0000000..e601ec7
--- /dev/null
+++ b/mapproxy/test/schemas/wms/1.1.1/WMS_DescribeLayerResponse.dtd
@@ -0,0 +1,22 @@
+<!-- WMS_DescribeLayerResponse: the document is returned in response to a DescribeLayer request made on a WMS. -->
+
+<!ELEMENT WMS_DescribeLayerResponse (LayerDescription*) >
+<!ATTLIST WMS_DescribeLayerResponse
+            version CDATA #REQUIRED >
+
+<!-- LayerDescription: describes the contents of a NamedLayer, the name of which is specified in the "name" attribute.  If the NamedLayer is not feature based, then the LayerDescription has no further contents.  If the NamedLayer is feature based then the "wfs" attribute gives the URL prefix for the WFS containing the feature data.  Equivalently, the "owsType" and "owsURL" attributes can be used to indicate the OWS type & base URL of a service.  The "wfs" attribute is retained for greate [...]
+
+The LayerDescription contains one or more Query elements that specify the feature-types present in the NamedLayer. -->
+
+<!ELEMENT LayerDescription (Query*) >
+<!ATTLIST LayerDescription
+            name CDATA #REQUIRED
+            wfs CDATA #IMPLIED
+            owsType CDATA #IMPLIED
+            owsURL CDATA #IMPLIED >
+
+<!-- Query: a Query uses the "typeName" attribute to identify a feature/coverage-type.  This is a stripped down version of the Query element used in the WFS. -->
+
+<!ELEMENT Query EMPTY >
+<!ATTLIST Query
+            typeName CDATA #REQUIRED >
diff --git a/mapproxy/test/schemas/wms/1.1.1/WMS_MS_Capabilities.dtd b/mapproxy/test/schemas/wms/1.1.1/WMS_MS_Capabilities.dtd
new file mode 100644
index 0000000..51fa20f
--- /dev/null
+++ b/mapproxy/test/schemas/wms/1.1.1/WMS_MS_Capabilities.dtd
@@ -0,0 +1,274 @@
+<!ELEMENT WMT_MS_Capabilities (Service, Capability) >
+
+<!ATTLIST WMT_MS_Capabilities
+	  version CDATA #FIXED "1.1.1"
+	  updateSequence CDATA #IMPLIED>
+
+<!-- Elements used in multiple places. -->
+
+<!-- The Name is typically for machine-to-machine communication. -->
+<!ELEMENT Name (#PCDATA) >
+
+<!-- The Title is for informative display to a human. -->
+<!ELEMENT Title (#PCDATA) >
+
+<!-- The abstract is a longer narrative description of an object. -->
+<!ELEMENT Abstract (#PCDATA) > 
+
+<!-- An OnlineResource is typically an HTTP URL.  The URL is placed in the
+xlink:href attribute.  The xmlns:xlink attribute is a required XML namespace
+declaration. -->
+<!ELEMENT OnlineResource EMPTY>
+<!ATTLIST OnlineResource
+          xmlns:xlink CDATA #FIXED "http://www.w3.org/1999/xlink"
+          xlink:type CDATA #FIXED "simple"
+          xlink:href CDATA #REQUIRED >
+
+<!-- A container for listing an available format's MIME type. -->
+<!ELEMENT Format (#PCDATA) >
+
+
+<!-- General service metadata. -->
+
+<!ELEMENT Service (Name, Title, Abstract?, KeywordList?, OnlineResource,
+                   ContactInformation?, Fees?, AccessConstraints?) >
+
+<!-- List of keywords or keyword phrases to help catalog searching. -->
+<!ELEMENT KeywordList (Keyword*) >
+
+<!-- A single keyword or phrase. -->
+<!ELEMENT Keyword (#PCDATA) >
+
+<!-- Information about a contact person for the service. -->
+<!ELEMENT ContactInformation  (ContactPersonPrimary?, ContactPosition?,
+                               ContactAddress?, ContactVoiceTelephone?,
+                               ContactFacsimileTelephone?,
+                               ContactElectronicMailAddress?) >
+
+<!--The primary contact person.-->
+<!ELEMENT ContactPersonPrimary  (ContactPerson, ContactOrganization) >
+
+<!--The person to contact.-->
+<!ELEMENT ContactPerson  (#PCDATA) >
+
+<!--The organization supplying the service.-->
+<!ELEMENT ContactOrganization  (#PCDATA) >
+
+<!--The position title for the contact person.-->
+<!ELEMENT ContactPosition  (#PCDATA) >
+
+<!--The address for the contact supplying the service.-->
+<!ELEMENT ContactAddress  (AddressType,Address,City,StateOrProvince,PostCode,
+               Country) >
+
+<!--The type of address.-->
+<!ELEMENT AddressType  (#PCDATA) >
+
+<!--The street address.-->
+<!ELEMENT Address  (#PCDATA) >
+
+<!--The address city.-->
+<!ELEMENT City  (#PCDATA) >
+
+<!--The state or province.-->
+<!ELEMENT StateOrProvince  (#PCDATA) >
+
+<!--The zip or postal code.-->
+<!ELEMENT PostCode  (#PCDATA) >
+
+<!--The address country.-->
+<!ELEMENT Country  (#PCDATA) >
+
+<!--Contact telephone number.-->
+<!ELEMENT ContactVoiceTelephone  (#PCDATA) >
+
+<!--The contact fax number.-->
+<!ELEMENT ContactFacsimileTelephone  (#PCDATA) >
+
+<!--The e-mail address for the contact.-->
+<!ELEMENT ContactElectronicMailAddress  (#PCDATA) >
+
+
+<!-- Elements indicating what fees or access constraints are imposed. -->
+<!ELEMENT Fees (#PCDATA)>
+<!ELEMENT AccessConstraints (#PCDATA)>
+
+
+<!-- A Capability lists available request types, how exceptions
+may be reported, and whether any vendor-specific capabilities are defined.  It
+also includes an optional list of map layers available from this server. -->
+<!ELEMENT Capability 
+          (Request, Exception, VendorSpecificCapabilities?,
+	   UserDefinedSymbolization?, Layer?) >
+
+<!-- Available WMS Operations are listed in a Request element. -->
+<!ELEMENT Request (GetCapabilities, GetMap, GetFeatureInfo?,
+                   DescribeLayer?, GetLegendGraphic?, GetStyles?, PutStyles?) >
+
+<!-- For each operation offered by the server, list the available output
+formats and the online resource. -->
+<!ELEMENT GetCapabilities (Format+, DCPType+)>
+<!ELEMENT GetMap (Format+, DCPType+)>
+<!ELEMENT GetFeatureInfo (Format+, DCPType+)>
+<!-- The following optional operations only apply to SLD-enabled WMS -->
+<!ELEMENT DescribeLayer (Format+, DCPType+)>
+<!ELEMENT GetLegendGraphic (Format+, DCPType+)>
+<!ELEMENT GetStyles (Format+, DCPType+)>
+<!ELEMENT PutStyles (Format+, DCPType+)>
+
+<!-- Available Distributed Computing Platforms (DCPs) are
+listed here.  At present, only HTTP is defined. -->
+<!ELEMENT DCPType (HTTP) >
+
+<!-- Available HTTP request methods.  One or both may be supported. -->
+<!ELEMENT HTTP (Get | Post)+ >
+
+<!-- URL prefix for each HTTP request method. -->
+<!ELEMENT Get (OnlineResource) >
+<!ELEMENT Post (OnlineResource) >
+
+<!-- An Exception element indicates which error-reporting formats are supported. -->
+<!ELEMENT Exception (Format+)>
+
+<!-- Optional user-defined symbolization (used only by SLD-enabled WMSes). -->
+<!ELEMENT UserDefinedSymbolization EMPTY >
+<!ATTLIST UserDefinedSymbolization
+          SupportSLD (0 | 1) "0"
+          UserLayer (0 | 1) "0"
+          UserStyle (0 | 1) "0"
+          RemoteWFS (0 | 1) "0" >
+
+<!-- Nested list of zero or more map Layers offered by this server. -->
+<!ELEMENT Layer ( Name?, Title, Abstract?, KeywordList?, SRS*,
+                  LatLonBoundingBox?, BoundingBox*, Dimension*, Extent*,
+                  Attribution?, AuthorityURL*, Identifier*, MetadataURL*, DataURL*,
+                  FeatureListURL*, Style*, ScaleHint?, Layer* ) >
+
+<!-- Optional attributes-->
+<!ATTLIST Layer queryable (0 | 1) "0"
+                cascaded CDATA #IMPLIED
+                opaque (0 | 1) "0"
+                noSubsets (0 | 1) "0"
+                fixedWidth CDATA #IMPLIED
+                fixedHeight CDATA #IMPLIED >
+          
+<!-- Identifier for a single Spatial Reference Systems (SRS). -->
+<!ELEMENT SRS (#PCDATA) >
+
+<!-- The LatLonBoundingBox attributes indicate the edges of the enclosing
+rectangle in latitude/longitude decimal degrees (as in SRS EPSG:4326 [WGS1984
+lat/lon]). -->
+<!ELEMENT LatLonBoundingBox EMPTY>
+<!ATTLIST LatLonBoundingBox 
+          minx CDATA #REQUIRED
+          miny CDATA #REQUIRED
+          maxx CDATA #REQUIRED
+          maxy CDATA #REQUIRED>
+
+<!-- The BoundingBox attributes indicate the edges of the bounding box
+in units of the specified spatial reference system. -->
+<!ELEMENT BoundingBox EMPTY>
+<!ATTLIST BoundingBox 
+          SRS CDATA #REQUIRED
+          minx CDATA #REQUIRED
+          miny CDATA #REQUIRED
+          maxx CDATA #REQUIRED
+          maxy CDATA #REQUIRED
+          resx CDATA #IMPLIED
+          resy CDATA #IMPLIED>
+
+<!-- The Dimension element declares the _existence_ of a dimension. -->
+<!ELEMENT Dimension EMPTY >
+<!ATTLIST Dimension
+          name CDATA #REQUIRED
+          units CDATA #REQUIRED
+          unitSymbol CDATA #IMPLIED>
+
+<!-- The Extent element indicates what _values_ along a dimension are valid. -->
+<!ELEMENT Extent (#PCDATA) >
+<!ATTLIST Extent
+          name CDATA #REQUIRED
+          default CDATA #IMPLIED
+          nearestValue (0 | 1) "0">
+
+<!-- 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. -->
+<!ELEMENT Attribution ( Title?, OnlineResource?, LogoURL? )>
+<!ELEMENT LogoURL (Format, OnlineResource) >
+<!ATTLIST LogoURL
+          width NMTOKEN #REQUIRED
+          height NMTOKEN #REQUIRED>
+
+<!-- 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.  Two types
+are defined at present: 'TC211' = ISO TC211 19115; 'FGDC' = FGDC CSDGM.  The
+format element indicates how the metadata is structured. -->
+<!ELEMENT MetadataURL (Format, OnlineResource) >
+<!ATTLIST MetadataURL
+          type ( TC211 | FGDC ) #REQUIRED>
+
+<!-- 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. -->
+
+<!ELEMENT AuthorityURL (OnlineResource) >
+<!ATTLIST AuthorityURL
+          name NMTOKEN #REQUIRED >
+<!ELEMENT Identifier (#PCDATA) >
+<!ATTLIST Identifier
+          authority CDATA #REQUIRED >
+
+<!-- A Map Server may use DataURL to offer more information about the data
+underneath a particular layer. While the semantics are not well-defined, as
+long as the results of an HTTP GET request against the DataURL are properly
+MIME-typed, Viewer Clients and Cascading Map Servers can make use of this. -->
+<!ELEMENT DataURL (Format, OnlineResource) >
+
+<!-- A Map Server may use FeatureListURL to point to a list of the features
+represented in a Layer. -->
+<!ELEMENT FeatureListURL (Format, OnlineResource) >
+
+<!-- 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. -->
+<!ELEMENT Style ( Name, Title, Abstract?,
+                  LegendURL*, StyleSheetURL?, StyleURL? ) >
+
+<!-- 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 are
+provided to assist client applications in laying out space to display the
+legend. -->
+<!ELEMENT LegendURL (Format, OnlineResource) >
+<!ATTLIST LegendURL
+          width NMTOKEN #REQUIRED
+          height NMTOKEN #REQUIRED>
+
+<!-- StyleSheeetURL provides symbology information foreach Style of a Layer. -->
+<!ELEMENT StyleSheetURL (Format, OnlineResource) >
+
+<!-- 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. -->
+<!ELEMENT StyleURL (Format, OnlineResource) >
+
+<!-- Minimum and maximum scale hints for which it is appropriate to
+display this layer. -->
+<!ELEMENT ScaleHint EMPTY>
+<!ATTLIST ScaleHint
+          min CDATA #REQUIRED
+          max CDATA #REQUIRED>
+
+
+
diff --git a/mapproxy/test/schemas/wms/1.1.1/WMS_exception_1_1_1.dtd b/mapproxy/test/schemas/wms/1.1.1/WMS_exception_1_1_1.dtd
new file mode 100644
index 0000000..88be051
--- /dev/null
+++ b/mapproxy/test/schemas/wms/1.1.1/WMS_exception_1_1_1.dtd
@@ -0,0 +1,5 @@
+<!ELEMENT ServiceExceptionReport (ServiceException*)>
+<!ATTLIST ServiceExceptionReport version CDATA #FIXED "1.1.1">
+
+<!ELEMENT ServiceException (#PCDATA)>
+<!ATTLIST ServiceException code CDATA #IMPLIED>
diff --git a/mapproxy/test/schemas/wms/1.1.1/capabilities_1_1_1.dtd b/mapproxy/test/schemas/wms/1.1.1/capabilities_1_1_1.dtd
new file mode 100644
index 0000000..83f89eb
--- /dev/null
+++ b/mapproxy/test/schemas/wms/1.1.1/capabilities_1_1_1.dtd
@@ -0,0 +1,276 @@
+<!ELEMENT WMT_MS_Capabilities (Service, Capability) >
+
+<!ATTLIST WMT_MS_Capabilities
+	  version CDATA #FIXED "1.1.1"
+	  updateSequence CDATA #IMPLIED>
+
+<!-- Elements used in multiple places. -->
+
+<!-- The Name is typically for machine-to-machine communication. -->
+<!ELEMENT Name (#PCDATA) >
+
+<!-- The Title is for informative display to a human. -->
+<!ELEMENT Title (#PCDATA) >
+
+<!-- The abstract is a longer narrative description of an object. -->
+<!ELEMENT Abstract (#PCDATA) > 
+
+<!-- An OnlineResource is typically an HTTP URL.  The URL is placed in the
+xlink:href attribute.  The xmlns:xlink attribute is a required XML namespace
+declaration. -->
+<!ELEMENT OnlineResource EMPTY>
+<!ATTLIST OnlineResource
+          xmlns:xlink CDATA #FIXED "http://www.w3.org/1999/xlink"
+          xlink:type CDATA #FIXED "simple"
+          xlink:href CDATA #REQUIRED >
+
+<!-- A container for listing an available format's MIME type. -->
+<!ELEMENT Format (#PCDATA) >
+
+
+<!-- General service metadata. -->
+
+<!ELEMENT Service (Name, Title, Abstract?, KeywordList?, OnlineResource,
+                   ContactInformation?, Fees?, AccessConstraints?) >
+
+<!-- List of keywords or keyword phrases to help catalog searching. -->
+<!ELEMENT KeywordList (Keyword*) >
+
+<!-- A single keyword or phrase. -->
+<!ELEMENT Keyword (#PCDATA) >
+
+<!-- Information about a contact person for the service. -->
+<!ELEMENT ContactInformation  (ContactPersonPrimary?, ContactPosition?,
+                               ContactAddress?, ContactVoiceTelephone?,
+                               ContactFacsimileTelephone?,
+                               ContactElectronicMailAddress?) >
+
+<!--The primary contact person.-->
+<!ELEMENT ContactPersonPrimary  (ContactPerson, ContactOrganization) >
+
+<!--The person to contact.-->
+<!ELEMENT ContactPerson  (#PCDATA) >
+
+<!--The organization supplying the service.-->
+<!ELEMENT ContactOrganization  (#PCDATA) >
+
+<!--The position title for the contact person.-->
+<!ELEMENT ContactPosition  (#PCDATA) >
+
+<!--The address for the contact supplying the service.-->
+<!ELEMENT ContactAddress  (AddressType,Address,City,StateOrProvince,PostCode,
+               Country) >
+
+<!--The type of address.-->
+<!ELEMENT AddressType  (#PCDATA) >
+
+<!--The street address.-->
+<!ELEMENT Address  (#PCDATA) >
+
+<!--The address city.-->
+<!ELEMENT City  (#PCDATA) >
+
+<!--The state or province.-->
+<!ELEMENT StateOrProvince  (#PCDATA) >
+
+<!--The zip or postal code.-->
+<!ELEMENT PostCode  (#PCDATA) >
+
+<!--The address country.-->
+<!ELEMENT Country  (#PCDATA) >
+
+<!--Contact telephone number.-->
+<!ELEMENT ContactVoiceTelephone  (#PCDATA) >
+
+<!--The contact fax number.-->
+<!ELEMENT ContactFacsimileTelephone  (#PCDATA) >
+
+<!--The e-mail address for the contact.-->
+<!ELEMENT ContactElectronicMailAddress  (#PCDATA) >
+
+
+<!-- Elements indicating what fees or access constraints are imposed. -->
+<!ELEMENT Fees (#PCDATA)>
+<!ELEMENT AccessConstraints (#PCDATA)>
+
+
+<!-- A Capability lists available request types, how exceptions
+may be reported, and whether any vendor-specific capabilities are defined.  It
+also includes an optional list of map layers available from this server. -->
+<!ELEMENT Capability 
+          (Request, Exception, VendorSpecificCapabilities?,
+	   UserDefinedSymbolization?, Layer?) >
+
+<!-- Available WMS Operations are listed in a Request element. -->
+<!ELEMENT Request (GetCapabilities, GetMap, GetFeatureInfo?,
+                   DescribeLayer?, GetLegendGraphic?, GetStyles?, PutStyles?) >
+
+<!-- For each operation offered by the server, list the available output
+formats and the online resource. -->
+<!ELEMENT GetCapabilities (Format+, DCPType+)>
+<!ELEMENT GetMap (Format+, DCPType+)>
+<!ELEMENT GetFeatureInfo (Format+, DCPType+)>
+<!-- The following optional operations only apply to SLD-enabled WMS -->
+<!ELEMENT DescribeLayer (Format+, DCPType+)>
+<!ELEMENT GetLegendGraphic (Format+, DCPType+)>
+<!ELEMENT GetStyles (Format+, DCPType+)>
+<!ELEMENT PutStyles (Format+, DCPType+)>
+
+<!-- Available Distributed Computing Platforms (DCPs) are
+listed here.  At present, only HTTP is defined. -->
+<!ELEMENT DCPType (HTTP) >
+
+<!-- Available HTTP request methods.  One or both may be supported. -->
+<!ELEMENT HTTP (Get | Post)+ >
+
+<!-- URL prefix for each HTTP request method. -->
+<!ELEMENT Get (OnlineResource) >
+<!ELEMENT Post (OnlineResource) >
+
+<!-- An Exception element indicates which error-reporting formats are supported. -->
+<!ELEMENT Exception (Format+)>
+
+<!-- Optional user-defined symbolization (used only by SLD-enabled WMSes). -->
+<!ELEMENT UserDefinedSymbolization EMPTY >
+<!ATTLIST UserDefinedSymbolization
+          SupportSLD (0 | 1) "0"
+          UserLayer (0 | 1) "0"
+          UserStyle (0 | 1) "0"
+          RemoteWFS (0 | 1) "0" >
+
+<!-- Nested list of zero or more map Layers offered by this server. -->
+<!ELEMENT Layer ( Name?, Title, Abstract?, KeywordList?, SRS*,
+                  LatLonBoundingBox?, BoundingBox*, Dimension*, Extent*,
+                  Attribution?, AuthorityURL*, Identifier*, MetadataURL*, DataURL*,
+                  FeatureListURL*, Style*, ScaleHint?, Layer* ) >
+
+<!-- Optional attributes-->
+<!ATTLIST Layer queryable (0 | 1) "0"
+                cascaded CDATA #IMPLIED
+                opaque (0 | 1) "0"
+                noSubsets (0 | 1) "0"
+                fixedWidth CDATA #IMPLIED
+                fixedHeight CDATA #IMPLIED >
+          
+<!-- Identifier for a single Spatial Reference Systems (SRS). -->
+<!ELEMENT SRS (#PCDATA) >
+
+<!-- The LatLonBoundingBox attributes indicate the edges of the enclosing
+rectangle in latitude/longitude decimal degrees (as in SRS EPSG:4326 [WGS1984
+lat/lon]). -->
+<!ELEMENT LatLonBoundingBox EMPTY>
+<!ATTLIST LatLonBoundingBox 
+          minx CDATA #REQUIRED
+          miny CDATA #REQUIRED
+          maxx CDATA #REQUIRED
+          maxy CDATA #REQUIRED>
+
+<!-- The BoundingBox attributes indicate the edges of the bounding box
+in units of the specified spatial reference system. -->
+<!ELEMENT BoundingBox EMPTY>
+<!ATTLIST BoundingBox 
+          SRS CDATA #REQUIRED
+          minx CDATA #REQUIRED
+          miny CDATA #REQUIRED
+          maxx CDATA #REQUIRED
+          maxy CDATA #REQUIRED
+          resx CDATA #IMPLIED
+          resy CDATA #IMPLIED>
+
+<!-- The Dimension element declares the _existence_ of a dimension. -->
+<!ELEMENT Dimension EMPTY >
+<!ATTLIST Dimension
+          name CDATA #REQUIRED
+          units CDATA #REQUIRED
+          unitSymbol CDATA #IMPLIED>
+
+<!-- The Extent element indicates what _values_ along a dimension are valid. -->
+<!ELEMENT Extent (#PCDATA) >
+<!ATTLIST Extent
+          name CDATA #REQUIRED
+          default CDATA #IMPLIED
+          nearestValue (0 | 1) "0"
+          multipleValues (0 | 1) "0"
+          current (0 | 1) "0">
+
+<!-- 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. -->
+<!ELEMENT Attribution ( Title?, OnlineResource?, LogoURL? )>
+<!ELEMENT LogoURL (Format, OnlineResource) >
+<!ATTLIST LogoURL
+          width NMTOKEN #REQUIRED
+          height NMTOKEN #REQUIRED>
+
+<!-- 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.  Two types
+are defined at present: 'TC211' = ISO TC211 19115; 'FGDC' = FGDC CSDGM.  The
+format element indicates how the metadata is structured. -->
+<!ELEMENT MetadataURL (Format, OnlineResource) >
+<!ATTLIST MetadataURL
+          type ( TC211 | FGDC ) #REQUIRED>
+
+<!-- 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. -->
+
+<!ELEMENT AuthorityURL (OnlineResource) >
+<!ATTLIST AuthorityURL
+          name NMTOKEN #REQUIRED >
+<!ELEMENT Identifier (#PCDATA) >
+<!ATTLIST Identifier
+          authority CDATA #REQUIRED >
+
+<!-- A Map Server may use DataURL to offer more information about the data
+underneath a particular layer. While the semantics are not well-defined, as
+long as the results of an HTTP GET request against the DataURL are properly
+MIME-typed, Viewer Clients and Cascading Map Servers can make use of this. -->
+<!ELEMENT DataURL (Format, OnlineResource) >
+
+<!-- A Map Server may use FeatureListURL to point to a list of the features
+represented in a Layer. -->
+<!ELEMENT FeatureListURL (Format, OnlineResource) >
+
+<!-- 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. -->
+<!ELEMENT Style ( Name, Title, Abstract?,
+                  LegendURL*, StyleSheetURL?, StyleURL? ) >
+
+<!-- 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 are
+provided to assist client applications in laying out space to display the
+legend. -->
+<!ELEMENT LegendURL (Format, OnlineResource) >
+<!ATTLIST LegendURL
+          width NMTOKEN #REQUIRED
+          height NMTOKEN #REQUIRED>
+
+<!-- StyleSheeetURL provides symbology information foreach Style of a Layer. -->
+<!ELEMENT StyleSheetURL (Format, OnlineResource) >
+
+<!-- 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. -->
+<!ELEMENT StyleURL (Format, OnlineResource) >
+
+<!-- Minimum and maximum scale hints for which it is appropriate to
+display this layer. -->
+<!ELEMENT ScaleHint EMPTY>
+<!ATTLIST ScaleHint
+          min CDATA #REQUIRED
+          max CDATA #REQUIRED>
+
+
+
diff --git a/mapproxy/test/schemas/wms/1.1.1/capabilities_1_1_1.xml b/mapproxy/test/schemas/wms/1.1.1/capabilities_1_1_1.xml
new file mode 100644
index 0000000..cf701c9
--- /dev/null
+++ b/mapproxy/test/schemas/wms/1.1.1/capabilities_1_1_1.xml
@@ -0,0 +1,303 @@
+<?xml version='1.0' encoding="UTF-8" standalone="no" ?>
+<!-- The DTD (Document Type Definition) given here must correspond to the version number declared in the WMT_MS_Capabilities element below. -->
+<!DOCTYPE WMT_MS_Capabilities SYSTEM
+ "http://www.digitalearth.gov/wmt/xml/capabilities_1_1_1.dtd"
+ [
+ <!-- Vendor-specific elements are defined here if needed. -->
+ <!-- If not needed, just leave this EMPTY declaration.  Do not
+  delete the declaration entirely. -->
+ <!ELEMENT VendorSpecificCapabilities EMPTY>
+ ]>  <!-- end of DOCTYPE declaration -->
+
+<!-- Note: this XML is just an EXAMPLE that attempts to show all
+required and optional elements for illustration.  Consult the Web Map
+Service 1.1.0 specification and the DTD for guidance on what to actually
+include and what to leave out. -->
+
+<!-- The version number listed in the WMT_MS_Capabilities element here must
+correspond to the DTD declared above.  See the WMT specification document for
+how to respond when a client requests a version number not implemented by the
+server. -->
+<WMT_MS_Capabilities version="1.1.1" updateSequence="0">
+<!-- Service Metadata -->
+<Service>
+  <!-- The WMT-defined name for this type of service -->
+  <Name>OGC:WMS</Name>
+  <!-- Human-readable title for pick lists -->
+  <Title>Acme Corp. Map Server</Title>
+  <!-- Narrative description providing additional information -->
+  <Abstract>WMT Map Server maintained by Acme Corporation.  Contact: webmaster at wmt.acme.com.  High-quality maps showing roadrunner nests and possible ambush locations.</Abstract>
+  <KeywordList>
+    <Keyword>bird</Keyword>
+    <Keyword>roadrunner</Keyword>
+    <Keyword>ambush</Keyword>
+  </KeywordList>
+  <!-- Top-level web address of service or service provider.  See also OnlineResource
+  elements under <DCPType>. -->
+  <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
+   xlink:href="http://hostname/" />
+  <!-- Contact information -->
+  <ContactInformation>
+    <ContactPersonPrimary>
+      <ContactPerson>Jeff deLaBeaujardiere</ContactPerson>
+      <ContactOrganization>NASA</ContactOrganization>
+    </ContactPersonPrimary>
+    <ContactPosition>Computer Scientist</ContactPosition>
+    <ContactAddress>
+      <AddressType>postal</AddressType>
+      <Address>NASA Goddard Space Flight Center, Code 933</Address>
+      <City>Greenbelt</City>
+      <StateOrProvince>MD</StateOrProvince>
+      <PostCode>20771</PostCode>
+      <Country>USA</Country>
+    </ContactAddress>
+    <ContactVoiceTelephone>+1 301 286-1569</ContactVoiceTelephone>
+    <ContactFacsimileTelephone>+1 301 286-1777</ContactFacsimileTelephone>
+    <ContactElectronicMailAddress>delabeau at iniki.gsfc.nasa.gov</ContactElectronicMailAddress>
+  </ContactInformation>
+  <!-- Fees or access constraints imposed. -->
+  <Fees>none</Fees>
+  <AccessConstraints>none</AccessConstraints>
+</Service>
+<Capability>
+  <Request>
+    <GetCapabilities>
+      <Format>application/vnd.ogc.wms_xml</Format>
+      <DCPType>
+        <HTTP>
+          <Get>
+            <!-- The URL here for invoking GetCapabilities using HTTP GET
+            is only a prefix to which a query string is appended. -->
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:type="simple"
+             xlink:href="http://hostname:port/path" />
+          </Get>
+          <Post>
+            <!-- The URL here for invoking GetCapabilities using HTTP POST
+            includes the complete address to which a query would be sent in
+            XML format.  This is here for future expansion; no POST encoding
+	    has yet been defined. -->
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:type="simple"
+             xlink:href="http://hostname:port/path" />
+          </Post>
+        </HTTP>
+      </DCPType>
+    </GetCapabilities>
+    <GetMap>
+      <Format>image/gif</Format>
+      <Format>image/png</Format>
+      <Format>image/jpeg</Format>
+      <DCPType>
+        <HTTP>
+          <Get>
+            <!-- The URL here for invoking GetCapabilities using HTTP GET
+            is only a prefix to which a query string is appended. -->
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:type="simple"
+             xlink:href="http://hostname:port/path" />
+          </Get>
+        </HTTP>
+      </DCPType>
+    </GetMap>
+    <GetFeatureInfo>
+      <Format>application/vnd.ogc.gml</Format>
+      <Format>text/plain</Format>
+      <Format>text/html</Format>
+      <DCPType>
+        <HTTP>
+          <Get>
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:type="simple"
+             xlink:href="http://hostname:port/path" />
+          </Get>
+        </HTTP>
+      </DCPType>
+    </GetFeatureInfo>
+    <DescribeLayer><!--optional; used only by SLD-enabled WMS-->
+      <Format>application/vnd.ogc.gml</Format>
+      <DCPType>
+        <HTTP>
+          <Get>
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:type="simple"
+             xlink:href="http://hostname:port/path" />
+          </Get>
+        </HTTP>
+      </DCPType>
+    </DescribeLayer>
+  </Request>
+  <Exception>
+    <Format>application/vnd.ogc.se_xml</Format>
+    <Format>application/vnd.ogc.se_inimage</Format>
+    <Format>application/vnd.ogc.se_blank</Format>
+  </Exception>
+  <!-- Any text or markup is allowed here, as required to describe
+       vendor-specific capabilities.  Please define elements and attributes
+       in the DOCTYPE declaration at the start of the document. -->
+  <!-- This example is empty because no VSPs were defined in preamble -->
+  <VendorSpecificCapabilities />
+  <!-- Place-holder for Styled Layer Descriptor (SLD)-enabled WMSes.
+       This element is absent for a basic WMS. -->
+  <UserDefinedSymbolization SupportSLD="1" UserLayer="1" UserStyle="1"
+	RemoteWFS="1" />
+  <Layer>
+    <Title>Acme Corp. Map Server</Title>
+    <SRS>EPSG:4326</SRS> <!-- all layers are available in at least this SRS -->
+    <AuthorityURL name="DIF_ID">
+      <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
+       xlink:href="http://gcmd.gsfc.nasa.gov/difguide/whatisadif.html" />
+    </AuthorityURL>
+    <Layer>
+      <!-- This parent layer has a Name and can therefore be requested from a Map Server, yielding a map of all subsidiary layers. -->
+      <Name>ROADS_RIVERS</Name> 
+      <Title>Roads and Rivers</Title>
+      <!-- See the spec to learn how some characteristics are inherited by
+           subsidiary layers. -->
+      <SRS>EPSG:26986</SRS> <!-- An additional SRS for this layer --> 
+      <LatLonBoundingBox minx="-71.63" miny="41.75" maxx="-70.78" maxy="42.90"/>
+      <!-- The optional resx and resy attributes below indicate the X and Y spatial
+           resolution in the units of that SRS. -->
+      <!-- The EPSG:4326 BoundingBox duplicates some of the info in LatLonBoundingBox
+           and is therefore optional, but using it here allows the additional
+           resolution attributes. -->
+      <BoundingBox SRS="EPSG:4326"
+       minx="-71.63" miny="41.75" maxx="-70.78" maxy="42.90" resx="0.01" resy="0.01"/>
+      <BoundingBox SRS="EPSG:26986"
+       minx="189000" miny="834000" maxx="285000" maxy="962000" resx="1" resy="1" />
+      <!-- Optional Title, URL and logo image of data provider. -->
+      <Attribution>
+        <Title>State College University</Title>
+        <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
+         xlink:href="http://www.university.edu/" />
+        <LogoURL width="100" height="100">
+          <Format>image/gif</Format>
+          <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+           xlink:type="simple"
+           xlink:href="http://www.university.edu/icons/logo.gif" />
+        </LogoURL>
+      </Attribution>
+      <!-- Identifier whose meaning is defined in an AuthorityURL element -->
+      <Identifier authority="DIF_ID">123456</Identifier>
+      <FeatureListURL>
+        <Format>application/vnd.ogc.se_xml"</Format>
+        <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
+         xlink:href="http://www.university.edu/data/roads_rivers.gml" />
+      </FeatureListURL>
+      <Style>
+        <Name>USGS</Name>
+        <Title>USGS Topo Map Style</Title>
+        <Abstract>Features are shown in a style like that used in USGS topographic maps.</Abstract>
+        <!-- A picture of a legend for a Layer in this Style -->
+        <LegendURL width="72" height="72">
+          <Format>image/gif</Format>
+          <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+           xlink:type="simple"
+           xlink:href="http://www.university.edu/legends/usgs.gif" />
+        </LegendURL>
+        <!-- An XSL stylesheet describing how GML feature data will rendered to create
+             a map of this layer. -->
+        <StyleSheetURL>
+          <Format>text/xsl</Format>
+          <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+           xlink:type="simple"
+           xlink:href="http://www.university.edu/stylesheets/usgs.xsl" />
+        </StyleSheetURL>
+      </Style>
+      <ScaleHint min="4000" max="35000"></ScaleHint>
+      <Layer queryable="1">
+	<Name>ROADS_1M</Name> 
+	<Title>Roads at 1:1M scale</Title>
+	<Abstract>Roads at a scale of 1 to 1 million.</Abstract>
+	<KeywordList>
+          <Keyword>road</Keyword>
+          <Keyword>transportation</Keyword>
+          <Keyword>atlas</Keyword>
+	</KeywordList>
+	<Identifier authority="DIF_ID">123456</Identifier>
+        <!-- Metadata specific to this particular layer.  The same FGDC metadata is offered in two formats. -->
+	<MetadataURL type="FGDC">
+          <Format>text/plain</Format>
+          <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+           xlink:type="simple"
+           xlink:href="http://www.university.edu/metadata/roads.txt" />
+        </MetadataURL>
+	<MetadataURL type="FGDC">
+           <Format>text/xml</Format>
+           <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+            xlink:type="simple"
+            xlink:href="http://www.university.edu/metadata/roads.xml" />
+        </MetadataURL>
+        <!-- In addition to the Style specified in the parent Layer, this Layer is available in this style. -->
+	<Style>
+	  <Name>ATLAS</Name>
+	  <Title>Road atlas style</Title>
+	  <Abstract>Roads are shown in a style like that used in a commercial road atlas.</Abstract>
+        <LegendURL width="72" height="72">
+          <Format>image/gif</Format>
+          <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+           xlink:type="simple"
+           xlink:href="http://www.university.edu/legends/atlas.gif" />
+        </LegendURL>
+	</Style>
+      </Layer>
+      <Layer queryable="1">
+	<Name>RIVERS_1M</Name>
+	<Title>Rivers at 1:1M scale</Title>
+	<Abstract>Rivers at a scale of 1 to 1 million.</Abstract>
+	<KeywordList>
+          <Keyword>river</Keyword>
+          <Keyword>canal</Keyword>
+          <Keyword>waterway</Keyword>
+	</KeywordList>
+      </Layer>
+    </Layer>
+    <Layer queryable="1">
+      <Title>Weather Forecast Data</Title>
+      <SRS>EPSG:4326</SRS> <!-- harmless repetition of common SRS -->
+      <LatLonBoundingBox minx="-180" miny="-90" maxx="180" maxy="90" />
+      <!-- These weather data are available daily from 1999-01-01 through
+           2000-08-22. -->
+      <Dimension name="time" units="ISO8601" />
+      <Extent name="time" default="2000-08-22">1999-01-01/2000-08-22/P1D</Extent>
+      <Layer>
+	<Name>Clouds</Name> 
+	<Title>Forecast cloud cover</Title>
+      </Layer>
+      <Layer>
+	<Name>Temperature</Name> 
+	<Title>Forecast temperature</Title>
+      </Layer>
+      <Layer>
+	<Name>Pressure</Name> 
+	<Title>Forecast barometric pressure</Title>
+        <!-- Pressure is available at several elevations.
+         EPSG:5030 is WGS 84 ellipsoid, units in metres.
+         Pressure is also available at several times.
+         NOTE: first list all Dimension elements, then all Extent elements. -->
+         <Dimension name="time" units="ISO8601" />
+         <Dimension name="elevation" units="EPSG:5030" />
+         <Extent name="time" default="2000-08-22">1999-01-01/2000-08-22/P1D</Extent>
+         <Extent name="elevation" default="0" nearestValue="1">0,1000,3000,5000,10000</Extent>
+      </Layer>
+    </Layer>
+    <!-- Example of a layer which is a static map of fixed
+         size which the server cannot subset or make transparent -->
+    <Layer opaque="1" noSubsets="1" fixedWidth="512" fixedHeight="256">
+      <Name>ozone_image</Name>
+      <Title>Global ozone distribution (1992)</Title>
+      <LatLonBoundingBox minx="-180" miny="-90" maxx="180" maxy="90" />
+      <Extent name="time" default="1992">1992</Extent>
+    </Layer>
+    <!-- Example of a layer which originated from another WMS and has been
+         "cascaded" by this WMS. -->
+    <Layer cascaded="1">
+      <Name>population</Name>
+      <Title>World population, annual</Title>
+      <LatLonBoundingBox minx="-180" miny="-90" maxx="180" maxy="90" />
+      <Extent name="time" default="2000">1990/2000/P1Y</Extent>
+    </Layer>
+  </Layer>
+</Capability>
+</WMT_MS_Capabilities>
+
diff --git a/mapproxy/test/schemas/wms/1.1.1/exception_1_1_1.dtd b/mapproxy/test/schemas/wms/1.1.1/exception_1_1_1.dtd
new file mode 100644
index 0000000..055044c
--- /dev/null
+++ b/mapproxy/test/schemas/wms/1.1.1/exception_1_1_1.dtd
@@ -0,0 +1,6 @@
+<!ELEMENT ServiceExceptionReport (ServiceException*)>
+<!ATTLIST ServiceExceptionReport version CDATA #FIXED "1.1.1">
+
+<!ELEMENT ServiceException (#PCDATA)>
+<!ATTLIST ServiceException code CDATA #IMPLIED>
+
diff --git a/mapproxy/test/schemas/wms/1.1.1/exception_1_1_1.xml b/mapproxy/test/schemas/wms/1.1.1/exception_1_1_1.xml
new file mode 100644
index 0000000..e7a4792
--- /dev/null
+++ b/mapproxy/test/schemas/wms/1.1.1/exception_1_1_1.xml
@@ -0,0 +1,33 @@
+<?xml version='1.0' encoding="UTF-8" standalone="no" ?>
+<!DOCTYPE ServiceExceptionReport SYSTEM
+ "http://www.digitalearth.gov/wmt/xml/exception_1_1_1.dtd">
+<ServiceExceptionReport version="1.1.1">
+  <ServiceException>
+    Plain text message about an error.
+  </ServiceException>
+  <ServiceException code="InvalidUpdateSequence">
+    Another message, this one with a Service Exception code supplied.
+  </ServiceException>
+  <ServiceException>
+    <![CDATA[
+    Error in module <foo.c>, 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:
+    ]]>
+  </ServiceException>
+  <ServiceException>
+    <![CDATA[
+      <Module>foo.c</Module>
+      <Error>An error occurred</Error>
+      <Explanation>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.</Explanation>
+    ]]>
+  </ServiceException>
+</ServiceExceptionReport>
diff --git a/mapproxy/test/schemas/wms/1.3.0/ReadMe.txt b/mapproxy/test/schemas/wms/1.3.0/ReadMe.txt
new file mode 100644
index 0000000..a126db4
--- /dev/null
+++ b/mapproxy/test/schemas/wms/1.3.0/ReadMe.txt
@@ -0,0 +1,8 @@
+This 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
+
diff --git a/mapproxy/test/schemas/wms/1.3.0/capabilities_1_3_0.xml b/mapproxy/test/schemas/wms/1.3.0/capabilities_1_3_0.xml
new file mode 100644
index 0000000..6cab96d
--- /dev/null
+++ b/mapproxy/test/schemas/wms/1.3.0/capabilities_1_3_0.xml
@@ -0,0 +1,277 @@
+<?xml version='1.0' encoding="UTF-8"?>
+<WMS_Capabilities version="1.3.0" xmlns="http://www.opengis.net/wms"
+  xmlns:xlink="http://www.w3.org/1999/xlink"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://www.opengis.net/wms http://schemas.opengis.net/wms/1.3.0/capabilities_1_3_0.xsd">
+<!-- Service Metadata -->
+<Service>
+  <!-- The WMT-defined name for this type of service -->
+  <Name>WMS</Name>
+  <!-- Human-readable title for pick lists -->
+  <Title>Acme Corp. Map Server</Title>
+  <!-- Narrative description providing additional information -->
+  <Abstract>Map Server maintained by Acme Corporation.  Contact: webmaster at wmt.acme.com.  High-quality maps showing roadrunner nests and possible ambush locations.</Abstract>
+  <KeywordList>
+    <Keyword>bird</Keyword>
+    <Keyword>roadrunner</Keyword>
+    <Keyword>ambush</Keyword>
+  </KeywordList>
+  <!-- Top-level web address of service or service provider.  See also OnlineResource
+  elements under <DCPType>. -->
+  <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
+   xlink:href="http://hostname/" />
+  <!-- Contact information -->
+  <ContactInformation>
+    <ContactPersonPrimary>
+      <ContactPerson>Jeff Smith</ContactPerson>
+      <ContactOrganization>NASA</ContactOrganization>
+    </ContactPersonPrimary>
+    <ContactPosition>Computer Scientist</ContactPosition>
+    <ContactAddress>
+      <AddressType>postal</AddressType>
+      <Address>NASA Goddard Space Flight Center</Address>
+      <City>Greenbelt</City>
+      <StateOrProvince>MD</StateOrProvince>
+      <PostCode>20771</PostCode>
+      <Country>USA</Country>
+    </ContactAddress>
+    <ContactVoiceTelephone>+1 301 555-1212</ContactVoiceTelephone>
+    <ContactElectronicMailAddress>user at host.com</ContactElectronicMailAddress>
+  </ContactInformation>
+  <!-- Fees or access constraints imposed. -->
+  <Fees>none</Fees>
+  <AccessConstraints>none</AccessConstraints>
+  <LayerLimit>16</LayerLimit>
+  <MaxWidth>2048</MaxWidth>
+  <MaxHeight>2048</MaxHeight>
+</Service>
+<Capability>
+  <Request>
+    <GetCapabilities>
+      <Format>text/xml</Format>
+      <DCPType>
+        <HTTP>
+          <Get>
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:type="simple"
+             xlink:href="http://hostname/path?" />
+          </Get>
+          <Post>
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:type="simple"
+             xlink:href="http://hostname/path?" />
+          </Post>
+        </HTTP>
+      </DCPType>
+    </GetCapabilities>
+    <GetMap>
+      <Format>image/gif</Format>
+      <Format>image/png</Format>
+      <Format>image/jpeg</Format>
+      <DCPType>
+        <HTTP>
+          <Get>
+            <!-- The URL here for invoking GetCapabilities using HTTP GET
+            is only a prefix to which a query string is appended. -->
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:type="simple"
+             xlink:href="http://hostname/path?" />
+          </Get>
+        </HTTP>
+      </DCPType>
+    </GetMap>
+    <GetFeatureInfo>
+      <Format>text/xml</Format>
+      <Format>text/plain</Format>
+      <Format>text/html</Format>
+      <DCPType>
+        <HTTP>
+          <Get>
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:type="simple"
+             xlink:href="http://hostname/path?" />
+          </Get>
+        </HTTP>
+      </DCPType>
+    </GetFeatureInfo>
+  </Request>
+  <Exception>
+    <Format>XML</Format>
+    <Format>INIMAGE</Format>
+    <Format>BLANK</Format>
+  </Exception>
+  <Layer>
+    <Title>Acme Corp. Map Server</Title>
+    <CRS>CRS:84</CRS> <!-- all layers are available in at least this CRS -->
+    <AuthorityURL name="DIF_ID">
+      <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
+       xlink:href="http://gcmd.gsfc.nasa.gov/difguide/whatisadif.html" />
+    </AuthorityURL>
+    <Layer>
+      <!-- This parent layer has a Name and can therefore be requested from a Map Server, yielding a map of all subsidiary layers. -->
+      <Name>ROADS_RIVERS</Name>
+      <Title>Roads and Rivers</Title>
+      <!-- See the spec to learn how some characteristics are inherited by
+           subsidiary layers. -->
+      <CRS>EPSG:26986</CRS> <!-- An additional CRS for this layer -->
+      <EX_GeographicBoundingBox>
+        <westBoundLongitude>-71.63</westBoundLongitude>
+        <eastBoundLongitude>-70.78</eastBoundLongitude>
+        <southBoundLatitude>41.75</southBoundLatitude>
+        <northBoundLatitude>42.90</northBoundLatitude>
+      </EX_GeographicBoundingBox>
+      <!-- The optional resx and resy attributes indicate the X and Y spatial
+           resolution in the units of that CRS. -->
+      <BoundingBox CRS="CRS:84"
+       minx="-71.63" miny="41.75" maxx="-70.78" maxy="42.90" resx="0.01" resy="0.01"/>
+      <BoundingBox CRS="EPSG:26986"
+       minx="189000" miny="834000" maxx="285000" maxy="962000" resx="1" resy="1" />
+      <!-- Optional Title, URL and logo image of data provider. -->
+      <Attribution>
+        <Title>State College University</Title>
+        <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
+         xlink:href="http://www.university.edu/" />
+        <LogoURL width="100" height="100">
+          <Format>image/gif</Format>
+          <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+           xlink:type="simple"
+           xlink:href="http://www.university.edu/icons/logo.gif" />
+        </LogoURL>
+      </Attribution>
+      <!-- Identifier whose meaning is defined in an AuthorityURL element -->
+      <Identifier authority="DIF_ID">123456</Identifier>
+      <FeatureListURL>
+        <Format>XML"</Format>
+        <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
+         xlink:href="http://www.university.edu/data/roads_rivers.gml" />
+      </FeatureListURL>
+      <Style>
+        <Name>USGS</Name>
+        <Title>USGS Topo Map Style</Title>
+        <Abstract>Features are shown in a style like that used in USGS topographic maps.</Abstract>
+        <!-- A picture of a legend for a Layer in this Style -->
+        <LegendURL width="72" height="72">
+          <Format>image/gif</Format>
+          <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+           xlink:type="simple"
+           xlink:href="http://www.university.edu/legends/usgs.gif" />
+        </LegendURL>
+        <!-- An XSL stylesheet describing how feature data will rendered to create
+             a map of this layer. -->
+        <StyleSheetURL>
+          <Format>text/xsl</Format>
+          <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+           xlink:type="simple"
+           xlink:href="http://www.university.edu/stylesheets/usgs.xsl" />
+        </StyleSheetURL>
+      </Style>
+      <Layer queryable="1">
+        <Name>ROADS_1M</Name>
+        <Title>Roads at 1:1M scale</Title>
+        <Abstract>Roads at a scale of 1 to 1 million.</Abstract>
+        <KeywordList>
+          <Keyword>road</Keyword>
+          <Keyword>transportation</Keyword>
+          <Keyword>atlas</Keyword>
+        </KeywordList>
+        <Identifier authority="DIF_ID">123456</Identifier>
+        <MetadataURL type="FGDC:1998">
+                <Format>text/plain</Format>
+                <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+                 xlink:type="simple"
+                 xlink:href="http://www.university.edu/metadata/roads.txt" />
+             </MetadataURL>
+        <MetadataURL type="ISO19115:2003">
+               <Format>text/xml</Format>
+               <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+                xlink:type="simple"
+                xlink:href="http://www.university.edu/metadata/roads.xml" />
+             </MetadataURL>
+        <!-- In addition to the Style specified in the parent Layer, this Layer is
+             available in this style. -->
+        <Style>
+          <Name>ATLAS</Name>
+          <Title>Road atlas style</Title>
+          <Abstract>Roads are shown in a style like that used in a commercial road atlas.</Abstract>
+        <LegendURL width="72" height="72">
+          <Format>image/gif</Format>
+          <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+           xlink:type="simple"
+           xlink:href="http://www.university.edu/legends/atlas.gif" />
+        </LegendURL>
+        </Style>
+      </Layer>
+      <Layer queryable="1">
+        <Name>RIVERS_1M</Name>
+        <Title>Rivers at 1:1M scale</Title>
+        <Abstract>Rivers at a scale of 1 to 1 million.</Abstract>
+        <KeywordList>
+          <Keyword>river</Keyword>
+          <Keyword>canal</Keyword>
+          <Keyword>waterway</Keyword>
+        </KeywordList>
+      </Layer>
+    </Layer>
+    <Layer queryable="1">
+      <Title>Weather Forecast Data</Title>
+      <CRS>CRS:84</CRS> <!-- harmless repetition of common CRS -->
+
+      <EX_GeographicBoundingBox>
+        <westBoundLongitude>-180</westBoundLongitude>
+        <eastBoundLongitude>180</eastBoundLongitude>
+        <southBoundLatitude>-90</southBoundLatitude>
+        <northBoundLatitude>90</northBoundLatitude>
+      </EX_GeographicBoundingBox>
+      <!-- These weather data are available daily from 1999-01-01 through
+           2000-08-22. -->
+      <Dimension name="time" units="ISO8601" default="2000-08-22">
+         1999-01-01/2000-08-22/P1D
+      </Dimension>
+      <Layer>
+        <Name>Clouds</Name>
+        <Title>Forecast cloud cover</Title>
+      </Layer>
+      <Layer>
+        <Name>Temperature</Name>
+        <Title>Forecast temperature</Title>
+      </Layer>
+      <Layer>
+        <Name>Pressure</Name>
+        <Title>Forecast barometric pressure</Title>
+             <!-- This Pressure layer is available at several elevations and times. -->
+         <Dimension name="elevation" units="EPSG:5030" />
+         <Dimension name="time" units="ISO8601" default="2000-08-22">
+           1999-01-01/2000-08-22/P1D</Dimension>
+         <Dimension name="elevation" units="CRS:88" default="0" nearestValue="1">
+           0,1000,3000,5000,10000</Dimension>
+      </Layer>
+    </Layer>
+    <!-- Example of a layer which is a static map of fixed
+         size which the server cannot subset or make transparent -->
+    <Layer opaque="1" noSubsets="1" fixedWidth="512" fixedHeight="256">
+      <Name>ozone_image</Name>
+      <Title>Global ozone distribution (1992)</Title>
+      <EX_GeographicBoundingBox>
+        <westBoundLongitude>-180</westBoundLongitude>
+        <eastBoundLongitude>180</eastBoundLongitude>
+        <southBoundLatitude>-90</southBoundLatitude>
+        <northBoundLatitude>90</northBoundLatitude>
+      </EX_GeographicBoundingBox>
+      <Dimension name="time" units="ISO8601" default="1992">1992</Dimension>
+    </Layer>
+    <!-- Example of a layer which originated from another WMS and has been
+         "cascaded" by this WMS. -->
+    <Layer cascaded="1">
+      <Name>population</Name>
+      <Title>World population, annual</Title>
+      <EX_GeographicBoundingBox>
+        <westBoundLongitude>-180</westBoundLongitude>
+        <eastBoundLongitude>180</eastBoundLongitude>
+        <southBoundLatitude>-90</southBoundLatitude>
+        <northBoundLatitude>90</northBoundLatitude>
+      </EX_GeographicBoundingBox>
+      <Dimension name="time" units="ISO8601" default="2000">1990/2000/P1Y</Dimension>
+    </Layer>
+  </Layer>
+</Capability>
+</WMS_Capabilities>
diff --git a/mapproxy/test/schemas/wms/1.3.0/capabilities_1_3_0.xsd b/mapproxy/test/schemas/wms/1.3.0/capabilities_1_3_0.xsd
new file mode 100644
index 0000000..9016d71
--- /dev/null
+++ b/mapproxy/test/schemas/wms/1.3.0/capabilities_1_3_0.xsd
@@ -0,0 +1,611 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/wms" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:wms="http://www.opengis.net/wms" xmlns="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
+	<import namespace="http://www.w3.org/1999/xlink" schemaLocation="../../xlink/1.0.0/xlinks.xsd"/>
+	<!-- ********************************************************************* -->
+	<!-- **  The Top-Level Element.                                         ** -->
+	<!-- ********************************************************************* -->
+	<element name="WMS_Capabilities">
+		<annotation>
+			<documentation>
+        A WMS_Capabilities document is returned in response to a
+        GetCapabilities request made on a WMS.
+      </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="wms:Service"/>
+				<element ref="wms:Capability"/>
+			</sequence>
+			<attribute name="version" type="string" fixed="1.3.0"/>
+			<attribute name="updateSequence" type="string"/>
+		</complexType>
+	</element>
+	<!-- ********************************************************************* -->
+	<!-- **  Elements Used In Multiple Places.                              ** -->
+	<!-- ********************************************************************* -->
+	<element name="Name" type="string">
+		<annotation>
+			<documentation>
+        The Name is typically for machine-to-machine communication.
+      </documentation>
+		</annotation>
+	</element>
+	<element name="Title" type="string">
+		<annotation>
+			<documentation>
+        The Title is for informative display to a human.
+      </documentation>
+		</annotation>
+	</element>
+	<element name="Abstract" type="string">
+		<annotation>
+			<documentation>
+        The abstract is a longer narrative description of an object.
+      </documentation>
+		</annotation>
+	</element>
+	<element name="KeywordList">
+		<annotation>
+			<documentation>
+        List of keywords or keyword phrases to help catalog searching.
+      </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="wms:Keyword" minOccurs="0" maxOccurs="unbounded"/>
+			</sequence>
+		</complexType>
+	</element>
+	<element name="Keyword">
+		<annotation>
+			<documentation>
+        A single keyword or phrase.
+      </documentation>
+		</annotation>
+		<complexType>
+			<simpleContent>
+				<extension base="string">
+					<attribute name="vocabulary" type="string"/>
+				</extension>
+			</simpleContent>
+		</complexType>
+	</element>
+	<element name="OnlineResource">
+		<annotation>
+			<documentation>
+        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.
+      </documentation>
+		</annotation>
+		<complexType>
+			<attributeGroup ref="xlink:simpleLink"/>
+		</complexType>
+	</element>
+	<element name="Format" type="string">
+		<annotation>
+			<documentation>
+        A container for listing an available format's MIME type.
+      </documentation>
+		</annotation>
+	</element>
+	<!-- ********************************************************************* -->
+	<!-- **  General Service Metadata.                                      ** -->
+	<!-- ********************************************************************* -->
+	<element name="Service">
+		<annotation>
+			<documentation>
+        General service metadata.
+      </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element name="Name">
+					<simpleType>
+						<restriction base="string">
+							<enumeration value="WMS"/>
+						</restriction>
+					</simpleType>
+				</element>
+				<element ref="wms:Title"/>
+				<element ref="wms:Abstract" minOccurs="0"/>
+				<element ref="wms:KeywordList" minOccurs="0"/>
+				<element ref="wms:OnlineResource"/>
+				<element ref="wms:ContactInformation" minOccurs="0"/>
+				<element ref="wms:Fees" minOccurs="0"/>
+				<element ref="wms:AccessConstraints" minOccurs="0"/>
+				<element ref="wms:LayerLimit" minOccurs="0"/>
+				<element ref="wms:MaxWidth" minOccurs="0"/>
+				<element ref="wms:MaxHeight" minOccurs="0"/>
+			</sequence>
+		</complexType>
+	</element>
+	<element name="ContactInformation">
+		<annotation>
+			<documentation>
+        Information about a contact person for the service.
+      </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="wms:ContactPersonPrimary" minOccurs="0"/>
+				<element ref="wms:ContactPosition" minOccurs="0"/>
+				<element ref="wms:ContactAddress" minOccurs="0"/>
+				<element ref="wms:ContactVoiceTelephone" minOccurs="0"/>
+				<element ref="wms:ContactFacsimileTelephone" minOccurs="0"/>
+				<element ref="wms:ContactElectronicMailAddress" minOccurs="0"/>
+			</sequence>
+		</complexType>
+	</element>
+	<element name="ContactPersonPrimary">
+		<complexType>
+			<sequence>
+				<element ref="wms:ContactPerson"/>
+				<element ref="wms:ContactOrganization"/>
+			</sequence>
+		</complexType>
+	</element>
+	<element name="ContactPerson" type="string"/>
+	<element name="ContactOrganization" type="string"/>
+	<element name="ContactPosition" type="string"/>
+	<element name="ContactAddress">
+		<complexType>
+			<sequence>
+				<element ref="wms:AddressType"/>
+				<element ref="wms:Address"/>
+				<element ref="wms:City"/>
+				<element ref="wms:StateOrProvince"/>
+				<element ref="wms:PostCode"/>
+				<element ref="wms:Country"/>
+			</sequence>
+		</complexType>
+	</element>
+	<element name="AddressType" type="string"/>
+	<element name="Address" type="string"/>
+	<element name="City" type="string"/>
+	<element name="StateOrProvince" type="string"/>
+	<element name="PostCode" type="string"/>
+	<element name="Country" type="string"/>
+	<element name="ContactVoiceTelephone" type="string"/>
+	<element name="ContactFacsimileTelephone" type="string"/>
+	<element name="ContactElectronicMailAddress" type="string"/>
+	<element name="Fees" type="string"/>
+	<element name="AccessConstraints" type="string"/>
+	<element name="LayerLimit" type="positiveInteger"/>
+	<element name="MaxWidth" type="positiveInteger"/>
+	<element name="MaxHeight" type="positiveInteger"/>
+	<!-- ********************************************************************* -->
+	<!-- **  The Capability Element.                                        ** -->
+	<!-- ********************************************************************* -->
+	<element name="Capability">
+		<annotation>
+			<documentation>
+        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.
+      </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="wms:Request"/>
+				<element ref="wms:Exception"/>
+				<element ref="wms:_ExtendedCapabilities" minOccurs="0" maxOccurs="unbounded"/>
+				<element ref="wms:Layer" minOccurs="0"/>
+			</sequence>
+		</complexType>
+	</element>
+	<!-- ********************************************************************* -->
+	<!-- **  The Request Element.                                           ** -->
+	<!-- ********************************************************************* -->
+	<element name="Request">
+		<annotation>
+			<documentation>
+        Available WMS Operations are listed in a Request element.
+      </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="wms:GetCapabilities"/>
+				<element ref="wms:GetMap"/>
+				<element ref="wms:GetFeatureInfo" minOccurs="0"/>
+				<element ref="wms:_ExtendedOperation" minOccurs="0" maxOccurs="unbounded"/>
+			</sequence>
+		</complexType>
+	</element>
+	<element name="GetCapabilities" type="wms:OperationType"/>
+	<element name="GetMap" type="wms:OperationType"/>
+	<element name="GetFeatureInfo" type="wms:OperationType"/>
+	<element name="_ExtendedOperation" type="wms:OperationType" abstract="true"/>
+	<complexType name="OperationType">
+		<annotation>
+			<documentation>
+        For each operation offered by the server, list the available output
+        formats and the online resource.
+      </documentation>
+		</annotation>
+		<sequence>
+			<element ref="wms:Format" maxOccurs="unbounded"/>
+			<element ref="wms:DCPType" maxOccurs="unbounded"/>
+		</sequence>
+	</complexType>
+	<element name="DCPType">
+		<annotation>
+			<documentation>
+        Available Distributed Computing Platforms (DCPs) are listed here.
+        At present, only HTTP is defined.
+      </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="wms:HTTP"/>
+			</sequence>
+		</complexType>
+	</element>
+	<element name="HTTP">
+		<annotation>
+			<documentation>
+        Available HTTP request methods.  At least "Get" shall be supported.
+      </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="wms:Get"/>
+				<element ref="wms:Post" minOccurs="0"/>
+			</sequence>
+		</complexType>
+	</element>
+	<element name="Get">
+		<annotation>
+			<documentation>
+        The URL prefix for the HTTP "Get" request method.
+      </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="wms:OnlineResource"/>
+			</sequence>
+		</complexType>
+	</element>
+	<element name="Post">
+		<annotation>
+			<documentation>
+        The URL prefix for the HTTP "Post" request method.
+      </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="wms:OnlineResource"/>
+			</sequence>
+		</complexType>
+	</element>
+	<!-- ********************************************************************* -->
+	<!-- **  The Exception Element.                                         ** -->
+	<!-- ********************************************************************* -->
+	<element name="Exception">
+		<annotation>
+			<documentation>
+        An Exception element indicates which error-reporting formats are
+        supported.
+      </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="wms:Format" maxOccurs="unbounded"/>
+			</sequence>
+		</complexType>
+	</element>
+	<!-- ********************************************************************* -->
+	<!-- **  Extended Capabilities.                                         ** -->
+	<!-- ********************************************************************* -->
+	<element name="_ExtendedCapabilities" abstract="true">
+		<annotation>
+			<documentation>
+        Individual service providers may use this element to report extended
+        capabilities.
+      </documentation>
+		</annotation>
+	</element>
+	<!-- ********************************************************************* -->
+	<!-- **  The Layer Element.                                             ** -->
+	<!-- ********************************************************************* -->
+	<element name="Layer">
+		<annotation>
+			<documentation>
+        Nested list of zero or more map Layers offered by this server.
+      </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="wms:Name" minOccurs="0"/>
+				<element ref="wms:Title"/>
+				<element ref="wms:Abstract" minOccurs="0"/>
+				<element ref="wms:KeywordList" minOccurs="0"/>
+				<element ref="wms:CRS" minOccurs="0" maxOccurs="unbounded"/>
+				<element ref="wms:EX_GeographicBoundingBox" minOccurs="0"/>
+				<element ref="wms:BoundingBox" minOccurs="0" maxOccurs="unbounded"/>
+				<element ref="wms:Dimension" minOccurs="0" maxOccurs="unbounded"/>
+				<element ref="wms:Attribution" minOccurs="0"/>
+				<element ref="wms:AuthorityURL" minOccurs="0" maxOccurs="unbounded"/>
+				<element ref="wms:Identifier" minOccurs="0" maxOccurs="unbounded"/>
+				<element ref="wms:MetadataURL" minOccurs="0" maxOccurs="unbounded"/>
+				<element ref="wms:DataURL" minOccurs="0" maxOccurs="unbounded"/>
+				<element ref="wms:FeatureListURL" minOccurs="0" maxOccurs="unbounded"/>
+				<element ref="wms:Style" minOccurs="0" maxOccurs="unbounded"/>
+				<element ref="wms:MinScaleDenominator" minOccurs="0"/>
+				<element ref="wms:MaxScaleDenominator" minOccurs="0"/>
+				<element ref="wms:Layer" minOccurs="0" maxOccurs="unbounded"/>
+			</sequence>
+			<attribute name="queryable" type="boolean" default="0"/>
+			<attribute name="cascaded" type="nonNegativeInteger"/>
+			<attribute name="opaque" type="boolean" default="0"/>
+			<attribute name="noSubsets" type="boolean" default="0"/>
+			<attribute name="fixedWidth" type="nonNegativeInteger"/>
+			<attribute name="fixedHeight" type="nonNegativeInteger"/>
+		</complexType>
+	</element>
+	<element name="CRS" type="string">
+		<annotation>
+			<documentation>
+        Identifier for a single Coordinate Reference System (CRS).
+      </documentation>
+		</annotation>
+	</element>
+	<element name="EX_GeographicBoundingBox">
+		<annotation>
+			<documentation>
+        The EX_GeographicBoundingBox attributes indicate the limits of the enclosing
+        rectangle in longitude and latitude decimal degrees.
+      </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element name="westBoundLongitude" type="wms:longitudeType"/>
+				<element name="eastBoundLongitude" type="wms:longitudeType"/>
+				<element name="southBoundLatitude" type="wms:latitudeType"/>
+				<element name="northBoundLatitude" type="wms:latitudeType"/>
+			</sequence>
+		</complexType>
+	</element>
+	<element name="BoundingBox">
+		<annotation>
+			<documentation>
+        The BoundingBox attributes indicate the limits of the bounding box
+        in units of the specified coordinate reference system.
+      </documentation>
+		</annotation>
+		<complexType>
+			<attribute name="CRS" type="string" use="required"/>
+			<attribute name="minx" type="double" use="required"/>
+			<attribute name="miny" type="double" use="required"/>
+			<attribute name="maxx" type="double" use="required"/>
+			<attribute name="maxy" type="double" use="required"/>
+			<attribute name="resx" type="double"/>
+			<attribute name="resy" type="double"/>
+		</complexType>
+	</element>
+	<element name="Dimension">
+		<annotation>
+			<documentation>
+        The Dimension element declares the existence of a dimension and indicates what
+        values along a dimension are valid.
+      </documentation>
+		</annotation>
+		<complexType>
+			<simpleContent>
+				<extension base="string">
+					<attribute name="name" type="string" use="required"/>
+					<attribute name="units" type="string" use="required"/>
+					<attribute name="unitSymbol" type="string"/>
+					<attribute name="default" type="string"/>
+					<attribute name="multipleValues" type="boolean"/>
+					<attribute name="nearestValue" type="boolean"/>
+					<attribute name="current" type="boolean"/>
+				</extension>
+			</simpleContent>
+		</complexType>
+	</element>
+	<element name="Attribution">
+		<annotation>
+			<documentation>
+        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.
+      </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="wms:Title" minOccurs="0"/>
+				<element ref="wms:OnlineResource" minOccurs="0"/>
+				<element ref="wms:LogoURL" minOccurs="0"/>
+			</sequence>
+		</complexType>
+	</element>
+	<element name="LogoURL">
+		<complexType>
+			<sequence>
+				<element ref="wms:Format"/>
+				<element ref="wms:OnlineResource"/>
+			</sequence>
+			<attribute name="width" type="positiveInteger"/>
+			<attribute name="height" type="positiveInteger"/>
+		</complexType>
+	</element>
+	<element name="MetadataURL">
+		<annotation>
+			<documentation>
+        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.
+      </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="wms:Format"/>
+				<element ref="wms:OnlineResource"/>
+			</sequence>
+			<attribute name="type" type="NMTOKEN" use="required"/>
+		</complexType>
+	</element>
+	<element name="AuthorityURL">
+		<annotation>
+			<documentation>
+        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.
+      </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="wms:OnlineResource"/>
+			</sequence>
+			<attribute name="name" type="NMTOKEN" use="required"/>
+		</complexType>
+	</element>
+	<element name="Identifier">
+		<complexType>
+			<simpleContent>
+				<extension base="string">
+					<attribute name="authority" type="string" use="required"/>
+				</extension>
+			</simpleContent>
+		</complexType>
+	</element>
+	<element name="DataURL">
+		<annotation>
+			<documentation>
+        A Map Server may use DataURL offer a link to the underlying data represented
+        by a particular layer.
+      </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="wms:Format"/>
+				<element ref="wms:OnlineResource"/>
+			</sequence>
+		</complexType>
+	</element>
+	<element name="FeatureListURL">
+		<annotation>
+			<documentation>
+        A Map Server may use FeatureListURL to point to a list of the
+        features represented in a Layer.
+      </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="wms:Format"/>
+				<element ref="wms:OnlineResource"/>
+			</sequence>
+		</complexType>
+	</element>
+	<element name="Style">
+		<annotation>
+			<documentation>
+        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.
+      </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="wms:Name"/>
+				<element ref="wms:Title"/>
+				<element ref="wms:Abstract" minOccurs="0"/>
+				<element ref="wms:LegendURL" minOccurs="0" maxOccurs="unbounded"/>
+				<element ref="wms:StyleSheetURL" minOccurs="0"/>
+				<element ref="wms:StyleURL" minOccurs="0"/>
+			</sequence>
+		</complexType>
+	</element>
+	<element name="LegendURL">
+		<annotation>
+			<documentation>
+        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.
+      </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="wms:Format"/>
+				<element ref="wms:OnlineResource"/>
+			</sequence>
+			<attribute name="width" type="positiveInteger"/>
+			<attribute name="height" type="positiveInteger"/>
+		</complexType>
+	</element>
+	<element name="StyleSheetURL">
+		<annotation>
+			<documentation>
+        StyleSheeetURL provides symbology information for each Style of a Layer.
+      </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="wms:Format"/>
+				<element ref="wms:OnlineResource"/>
+			</sequence>
+		</complexType>
+	</element>
+	<element name="StyleURL">
+		<annotation>
+			<documentation>
+        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.
+      </documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="wms:Format"/>
+				<element ref="wms:OnlineResource"/>
+			</sequence>
+		</complexType>
+	</element>
+	<element name="MinScaleDenominator" type="double">
+		<annotation>
+			<documentation>
+        Minimum scale denominator for which it is appropriate to
+        display this layer.
+      </documentation>
+		</annotation>
+	</element>
+	<element name="MaxScaleDenominator" type="double">
+		<annotation>
+			<documentation>
+        Maximum scale denominator for which it is appropriate to
+        display this layer.
+      </documentation>
+		</annotation>
+	</element>
+	<!-- ********************************************************************* -->
+	<!-- **  Type Definitions.                                              ** -->
+	<!-- ********************************************************************* -->
+	<simpleType name="longitudeType">
+		<restriction base="double">
+			<minInclusive value="-180"/>
+			<maxInclusive value="180"/>
+		</restriction>
+	</simpleType>
+	<simpleType name="latitudeType">
+		<restriction base="double">
+			<minInclusive value="-90"/>
+			<maxInclusive value="90"/>
+		</restriction>
+	</simpleType>
+</schema>
diff --git a/mapproxy/test/schemas/wms/1.3.0/exceptions_1_3_0.xml b/mapproxy/test/schemas/wms/1.3.0/exceptions_1_3_0.xml
new file mode 100644
index 0000000..fd75ec7
--- /dev/null
+++ b/mapproxy/test/schemas/wms/1.3.0/exceptions_1_3_0.xml
@@ -0,0 +1,34 @@
+<?xml version='1.0' encoding="UTF-8"?>
+<ServiceExceptionReport version="1.3.0"
+  xmlns="http://www.opengis.net/ogc"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://www.opengis.net/ogc http://schemas.opengis.net/wms/1.3.0/exceptions_1_3_0.xsd">
+<ServiceException>
+Plain text message about an error.
+</ServiceException>
+<ServiceException code="InvalidUpdateSequence">
+Another error message, this one with a service exception code supplied.
+</ServiceException>
+<ServiceException>
+<![CDATA[
+Error in module <foo.c>, 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:
+]]>
+</ServiceException>
+<ServiceException>
+<![CDATA[
+<Module>foo.c</Module>
+<Error>An error occurred</Error>
+<Explanation>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.</Explanation>
+]]>
+</ServiceException>
+</ServiceExceptionReport>
diff --git a/mapproxy/test/schemas/wms/1.3.0/exceptions_1_3_0.xsd b/mapproxy/test/schemas/wms/1.3.0/exceptions_1_3_0.xsd
new file mode 100644
index 0000000..c088264
--- /dev/null
+++ b/mapproxy/test/schemas/wms/1.3.0/exceptions_1_3_0.xsd
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xsd:schema
+    targetNamespace="http://www.opengis.net/ogc"
+    xmlns:ogc="http://www.opengis.net/ogc"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+    elementFormDefault="qualified">
+
+    <xsd:element name="ServiceExceptionReport">
+       <xsd:complexType>
+          <xsd:sequence>
+             <xsd:element name="ServiceException"
+                          type="ogc:ServiceExceptionType"
+                          minOccurs="0" maxOccurs="unbounded"/>
+          </xsd:sequence>
+          <xsd:attribute name="version" type="xsd:string" fixed="1.3.0"/>
+       </xsd:complexType>
+    </xsd:element>
+
+    <xsd:complexType name="ServiceExceptionType">
+       <xsd:simpleContent>
+          <xsd:extension base="xsd:string">
+             <xsd:attribute name="code" type="xsd:string"/>
+             <xsd:attribute name="locator" type="xsd:string"/>
+          </xsd:extension>
+       </xsd:simpleContent>
+    </xsd:complexType>
+</xsd:schema>
diff --git a/mapproxy/test/schemas/wmsc/1.1.1/OGC-exception.xsd b/mapproxy/test/schemas/wmsc/1.1.1/OGC-exception.xsd
new file mode 100644
index 0000000..9ed19c2
--- /dev/null
+++ b/mapproxy/test/schemas/wmsc/1.1.1/OGC-exception.xsd
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xsd:schema
+   targetNamespace="http://www.opengis.net/ogc"
+   xmlns:ogc="http://www.opengis.net/ogc"
+   xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+   elementFormDefault="qualified">
+
+   <xsd:element name="ServiceExceptionReport">
+      <xsd:annotation>
+         <xsd:documentation>
+            The ServiceExceptionReport element contains one
+            or more ServiceException elements that describe
+            a service exception.
+         </xsd:documentation>
+      </xsd:annotation>
+      <xsd:complexType>
+         <xsd:sequence>
+            <xsd:element name="ServiceException"
+                         type="ogc:ServiceExceptionType"
+                         minOccurs="0" maxOccurs="unbounded">
+               <xsd:annotation>
+                  <xsd:documentation>
+                     The Service exception element is used to describe 
+                     a service exception.
+                  </xsd:documentation>
+               </xsd:annotation>
+            </xsd:element>
+         </xsd:sequence>
+         <xsd:attribute name="version" type="xsd:string" fixed="1.2.0"/>
+      </xsd:complexType>
+   </xsd:element>
+
+   <xsd:complexType name="ServiceExceptionType">
+      <xsd:annotation>
+         <xsd:documentation>
+            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.
+         </xsd:documentation>
+      </xsd:annotation>
+      <xsd:simpleContent>
+         <xsd:extension base="xsd:string">
+            <xsd:attribute name="code" type="xsd:string">
+               <xsd:annotation>
+                  <xsd:documentation>
+                     A service may associate a code with an exception
+                     by using the code attribute.
+                  </xsd:documentation>
+               </xsd:annotation>
+            </xsd:attribute>
+            <xsd:attribute name="locator" type="xsd:string">
+               <xsd:annotation>
+                  <xsd:documentation>
+                     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 ...
+                  </xsd:documentation>
+               </xsd:annotation>
+            </xsd:attribute>
+         </xsd:extension>
+      </xsd:simpleContent>
+   </xsd:complexType>
+</xsd:schema>
diff --git a/mapproxy/test/schemas/wmsc/1.1.1/WMS_DescribeLayerResponse.dtd b/mapproxy/test/schemas/wmsc/1.1.1/WMS_DescribeLayerResponse.dtd
new file mode 100644
index 0000000..e601ec7
--- /dev/null
+++ b/mapproxy/test/schemas/wmsc/1.1.1/WMS_DescribeLayerResponse.dtd
@@ -0,0 +1,22 @@
+<!-- WMS_DescribeLayerResponse: the document is returned in response to a DescribeLayer request made on a WMS. -->
+
+<!ELEMENT WMS_DescribeLayerResponse (LayerDescription*) >
+<!ATTLIST WMS_DescribeLayerResponse
+            version CDATA #REQUIRED >
+
+<!-- LayerDescription: describes the contents of a NamedLayer, the name of which is specified in the "name" attribute.  If the NamedLayer is not feature based, then the LayerDescription has no further contents.  If the NamedLayer is feature based then the "wfs" attribute gives the URL prefix for the WFS containing the feature data.  Equivalently, the "owsType" and "owsURL" attributes can be used to indicate the OWS type & base URL of a service.  The "wfs" attribute is retained for greate [...]
+
+The LayerDescription contains one or more Query elements that specify the feature-types present in the NamedLayer. -->
+
+<!ELEMENT LayerDescription (Query*) >
+<!ATTLIST LayerDescription
+            name CDATA #REQUIRED
+            wfs CDATA #IMPLIED
+            owsType CDATA #IMPLIED
+            owsURL CDATA #IMPLIED >
+
+<!-- Query: a Query uses the "typeName" attribute to identify a feature/coverage-type.  This is a stripped down version of the Query element used in the WFS. -->
+
+<!ELEMENT Query EMPTY >
+<!ATTLIST Query
+            typeName CDATA #REQUIRED >
diff --git a/mapproxy/test/schemas/wmsc/1.1.1/WMS_MS_Capabilities.dtd b/mapproxy/test/schemas/wmsc/1.1.1/WMS_MS_Capabilities.dtd
new file mode 100644
index 0000000..d322439
--- /dev/null
+++ b/mapproxy/test/schemas/wmsc/1.1.1/WMS_MS_Capabilities.dtd
@@ -0,0 +1,283 @@
+<!ELEMENT WMT_MS_Capabilities (Service, Capability) >
+
+<!ATTLIST WMT_MS_Capabilities
+	  version CDATA #FIXED "1.1.1"
+	  updateSequence CDATA #IMPLIED>
+
+<!-- Elements used in multiple places. -->
+
+<!-- The Name is typically for machine-to-machine communication. -->
+<!ELEMENT Name (#PCDATA) >
+
+<!-- The Title is for informative display to a human. -->
+<!ELEMENT Title (#PCDATA) >
+
+<!-- The abstract is a longer narrative description of an object. -->
+<!ELEMENT Abstract (#PCDATA) > 
+
+<!-- An OnlineResource is typically an HTTP URL.  The URL is placed in the
+xlink:href attribute.  The xmlns:xlink attribute is a required XML namespace
+declaration. -->
+<!ELEMENT OnlineResource EMPTY>
+<!ATTLIST OnlineResource
+          xmlns:xlink CDATA #FIXED "http://www.w3.org/1999/xlink"
+          xlink:type CDATA #FIXED "simple"
+          xlink:href CDATA #REQUIRED >
+
+<!-- A container for listing an available format's MIME type. -->
+<!ELEMENT Format (#PCDATA) >
+
+
+<!-- General service metadata. -->
+
+<!ELEMENT Service (Name, Title, Abstract?, KeywordList?, OnlineResource,
+                   ContactInformation?, Fees?, AccessConstraints?) >
+
+<!-- List of keywords or keyword phrases to help catalog searching. -->
+<!ELEMENT KeywordList (Keyword*) >
+
+<!-- A single keyword or phrase. -->
+<!ELEMENT Keyword (#PCDATA) >
+
+<!-- Information about a contact person for the service. -->
+<!ELEMENT ContactInformation  (ContactPersonPrimary?, ContactPosition?,
+                               ContactAddress?, ContactVoiceTelephone?,
+                               ContactFacsimileTelephone?,
+                               ContactElectronicMailAddress?) >
+
+<!--The primary contact person.-->
+<!ELEMENT ContactPersonPrimary  (ContactPerson, ContactOrganization) >
+
+<!--The person to contact.-->
+<!ELEMENT ContactPerson  (#PCDATA) >
+
+<!--The organization supplying the service.-->
+<!ELEMENT ContactOrganization  (#PCDATA) >
+
+<!--The position title for the contact person.-->
+<!ELEMENT ContactPosition  (#PCDATA) >
+
+<!--The address for the contact supplying the service.-->
+<!ELEMENT ContactAddress  (AddressType,Address,City,StateOrProvince,PostCode,
+               Country) >
+
+<!--The type of address.-->
+<!ELEMENT AddressType  (#PCDATA) >
+
+<!--The street address.-->
+<!ELEMENT Address  (#PCDATA) >
+
+<!--The address city.-->
+<!ELEMENT City  (#PCDATA) >
+
+<!--The state or province.-->
+<!ELEMENT StateOrProvince  (#PCDATA) >
+
+<!--The zip or postal code.-->
+<!ELEMENT PostCode  (#PCDATA) >
+
+<!--The address country.-->
+<!ELEMENT Country  (#PCDATA) >
+
+<!--Contact telephone number.-->
+<!ELEMENT ContactVoiceTelephone  (#PCDATA) >
+
+<!--The contact fax number.-->
+<!ELEMENT ContactFacsimileTelephone  (#PCDATA) >
+
+<!--The e-mail address for the contact.-->
+<!ELEMENT ContactElectronicMailAddress  (#PCDATA) >
+
+
+<!-- Elements indicating what fees or access constraints are imposed. -->
+<!ELEMENT Fees (#PCDATA)>
+<!ELEMENT AccessConstraints (#PCDATA)>
+
+
+<!-- A Capability lists available request types, how exceptions
+may be reported, and whether any vendor-specific capabilities are defined.  It
+also includes an optional list of map layers available from this server. -->
+<!ELEMENT Capability 
+          (Request, Exception, VendorSpecificCapabilities?,
+	   UserDefinedSymbolization?, Layer?) >
+
+<!-- Available WMS Operations are listed in a Request element. -->
+<!ELEMENT Request (GetCapabilities, GetMap, GetFeatureInfo?,
+                   DescribeLayer?, GetLegendGraphic?, GetStyles?, PutStyles?) >
+
+<!-- For each operation offered by the server, list the available output
+formats and the online resource. -->
+<!ELEMENT GetCapabilities (Format+, DCPType+)>
+<!ELEMENT GetMap (Format+, DCPType+)>
+<!ELEMENT GetFeatureInfo (Format+, DCPType+)>
+<!-- The following optional operations only apply to SLD-enabled WMS -->
+<!ELEMENT DescribeLayer (Format+, DCPType+)>
+<!ELEMENT GetLegendGraphic (Format+, DCPType+)>
+<!ELEMENT GetStyles (Format+, DCPType+)>
+<!ELEMENT PutStyles (Format+, DCPType+)>
+
+<!-- Available Distributed Computing Platforms (DCPs) are
+listed here.  At present, only HTTP is defined. -->
+<!ELEMENT DCPType (HTTP) >
+
+<!-- Available HTTP request methods.  One or both may be supported. -->
+<!ELEMENT HTTP (Get | Post)+ >
+
+<!-- URL prefix for each HTTP request method. -->
+<!ELEMENT Get (OnlineResource) >
+<!ELEMENT Post (OnlineResource) >
+
+<!-- An Exception element indicates which error-reporting formats are supported. -->
+<!ELEMENT Exception (Format+)>
+
+<!-- Optional user-defined symbolization (used only by SLD-enabled WMSes). -->
+<!ELEMENT UserDefinedSymbolization EMPTY >
+<!ATTLIST UserDefinedSymbolization
+          SupportSLD (0 | 1) "0"
+          UserLayer (0 | 1) "0"
+          UserStyle (0 | 1) "0"
+          RemoteWFS (0 | 1) "0" >
+
+<!-- Nested list of zero or more map Layers offered by this server. -->
+<!ELEMENT Layer ( Name?, Title, Abstract?, KeywordList?, SRS*,
+                  LatLonBoundingBox?, BoundingBox*, Dimension*, Extent*,
+                  Attribution?, AuthorityURL*, Identifier*, MetadataURL*, DataURL*,
+                  FeatureListURL*, Style*, ScaleHint?, Layer* ) >
+
+<!-- Optional attributes-->
+<!ATTLIST Layer queryable (0 | 1) "0"
+                cascaded CDATA #IMPLIED
+                opaque (0 | 1) "0"
+                noSubsets (0 | 1) "0"
+                fixedWidth CDATA #IMPLIED
+                fixedHeight CDATA #IMPLIED >
+          
+<!-- Identifier for a single Spatial Reference Systems (SRS). -->
+<!ELEMENT SRS (#PCDATA) >
+
+<!-- The LatLonBoundingBox attributes indicate the edges of the enclosing
+rectangle in latitude/longitude decimal degrees (as in SRS EPSG:4326 [WGS1984
+lat/lon]). -->
+<!ELEMENT LatLonBoundingBox EMPTY>
+<!ATTLIST LatLonBoundingBox 
+          minx CDATA #REQUIRED
+          miny CDATA #REQUIRED
+          maxx CDATA #REQUIRED
+          maxy CDATA #REQUIRED>
+
+<!-- The BoundingBox attributes indicate the edges of the bounding box
+in units of the specified spatial reference system. -->
+<!ELEMENT BoundingBox EMPTY>
+<!ATTLIST BoundingBox 
+          SRS CDATA #REQUIRED
+          minx CDATA #REQUIRED
+          miny CDATA #REQUIRED
+          maxx CDATA #REQUIRED
+          maxy CDATA #REQUIRED
+          resx CDATA #IMPLIED
+          resy CDATA #IMPLIED>
+
+<!-- The Dimension element declares the _existence_ of a dimension. -->
+<!ELEMENT Dimension EMPTY >
+<!ATTLIST Dimension
+          name CDATA #REQUIRED
+          units CDATA #REQUIRED
+          unitSymbol CDATA #IMPLIED>
+
+<!-- The Extent element indicates what _values_ along a dimension are valid. -->
+<!ELEMENT Extent (#PCDATA) >
+<!ATTLIST Extent
+          name CDATA #REQUIRED
+          default CDATA #IMPLIED
+          nearestValue (0 | 1) "0">
+
+<!-- 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. -->
+<!ELEMENT Attribution ( Title?, OnlineResource?, LogoURL? )>
+<!ELEMENT LogoURL (Format, OnlineResource) >
+<!ATTLIST LogoURL
+          width NMTOKEN #REQUIRED
+          height NMTOKEN #REQUIRED>
+
+<!-- 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.  Two types
+are defined at present: 'TC211' = ISO TC211 19115; 'FGDC' = FGDC CSDGM.  The
+format element indicates how the metadata is structured. -->
+<!ELEMENT MetadataURL (Format, OnlineResource) >
+<!ATTLIST MetadataURL
+          type ( TC211 | FGDC ) #REQUIRED>
+
+<!-- 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. -->
+
+<!ELEMENT AuthorityURL (OnlineResource) >
+<!ATTLIST AuthorityURL
+          name NMTOKEN #REQUIRED >
+<!ELEMENT Identifier (#PCDATA) >
+<!ATTLIST Identifier
+          authority CDATA #REQUIRED >
+
+<!-- A Map Server may use DataURL to offer more information about the data
+underneath a particular layer. While the semantics are not well-defined, as
+long as the results of an HTTP GET request against the DataURL are properly
+MIME-typed, Viewer Clients and Cascading Map Servers can make use of this. -->
+<!ELEMENT DataURL (Format, OnlineResource) >
+
+<!-- A Map Server may use FeatureListURL to point to a list of the features
+represented in a Layer. -->
+<!ELEMENT FeatureListURL (Format, OnlineResource) >
+
+<!-- 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. -->
+<!ELEMENT Style ( Name, Title, Abstract?,
+                  LegendURL*, StyleSheetURL?, StyleURL? ) >
+
+<!-- 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 are
+provided to assist client applications in laying out space to display the
+legend. -->
+<!ELEMENT LegendURL (Format, OnlineResource) >
+<!ATTLIST LegendURL
+          width NMTOKEN #REQUIRED
+          height NMTOKEN #REQUIRED>
+
+<!-- StyleSheeetURL provides symbology information foreach Style of a Layer. -->
+<!ELEMENT StyleSheetURL (Format, OnlineResource) >
+
+<!-- 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. -->
+<!ELEMENT StyleURL (Format, OnlineResource) >
+
+<!-- Minimum and maximum scale hints for which it is appropriate to
+display this layer. -->
+<!ELEMENT ScaleHint EMPTY>
+<!ATTLIST ScaleHint
+          min CDATA #REQUIRED
+          max CDATA #REQUIRED>
+
+<!-- WMS-C VendorSpecificCapabilities -->
+
+ <!ELEMENT VendorSpecificCapabilities (TileSet*) >
+ <!ELEMENT TileSet (SRS, BoundingBox?, Resolutions, Width, Height, Format, Layers*, Styles*) >
+ <!ELEMENT Resolutions (#PCDATA) >
+ <!ELEMENT Width (#PCDATA) >
+ <!ELEMENT Height (#PCDATA) >
+ <!ELEMENT Layers (#PCDATA) >
+ <!ELEMENT Styles (#PCDATA) >
+
+
diff --git a/mapproxy/test/schemas/wmsc/1.1.1/WMS_exception_1_1_1.dtd b/mapproxy/test/schemas/wmsc/1.1.1/WMS_exception_1_1_1.dtd
new file mode 100644
index 0000000..88be051
--- /dev/null
+++ b/mapproxy/test/schemas/wmsc/1.1.1/WMS_exception_1_1_1.dtd
@@ -0,0 +1,5 @@
+<!ELEMENT ServiceExceptionReport (ServiceException*)>
+<!ATTLIST ServiceExceptionReport version CDATA #FIXED "1.1.1">
+
+<!ELEMENT ServiceException (#PCDATA)>
+<!ATTLIST ServiceException code CDATA #IMPLIED>
diff --git a/mapproxy/test/schemas/wmsc/1.1.1/capabilities_1_1_1.dtd b/mapproxy/test/schemas/wmsc/1.1.1/capabilities_1_1_1.dtd
new file mode 100644
index 0000000..83f89eb
--- /dev/null
+++ b/mapproxy/test/schemas/wmsc/1.1.1/capabilities_1_1_1.dtd
@@ -0,0 +1,276 @@
+<!ELEMENT WMT_MS_Capabilities (Service, Capability) >
+
+<!ATTLIST WMT_MS_Capabilities
+	  version CDATA #FIXED "1.1.1"
+	  updateSequence CDATA #IMPLIED>
+
+<!-- Elements used in multiple places. -->
+
+<!-- The Name is typically for machine-to-machine communication. -->
+<!ELEMENT Name (#PCDATA) >
+
+<!-- The Title is for informative display to a human. -->
+<!ELEMENT Title (#PCDATA) >
+
+<!-- The abstract is a longer narrative description of an object. -->
+<!ELEMENT Abstract (#PCDATA) > 
+
+<!-- An OnlineResource is typically an HTTP URL.  The URL is placed in the
+xlink:href attribute.  The xmlns:xlink attribute is a required XML namespace
+declaration. -->
+<!ELEMENT OnlineResource EMPTY>
+<!ATTLIST OnlineResource
+          xmlns:xlink CDATA #FIXED "http://www.w3.org/1999/xlink"
+          xlink:type CDATA #FIXED "simple"
+          xlink:href CDATA #REQUIRED >
+
+<!-- A container for listing an available format's MIME type. -->
+<!ELEMENT Format (#PCDATA) >
+
+
+<!-- General service metadata. -->
+
+<!ELEMENT Service (Name, Title, Abstract?, KeywordList?, OnlineResource,
+                   ContactInformation?, Fees?, AccessConstraints?) >
+
+<!-- List of keywords or keyword phrases to help catalog searching. -->
+<!ELEMENT KeywordList (Keyword*) >
+
+<!-- A single keyword or phrase. -->
+<!ELEMENT Keyword (#PCDATA) >
+
+<!-- Information about a contact person for the service. -->
+<!ELEMENT ContactInformation  (ContactPersonPrimary?, ContactPosition?,
+                               ContactAddress?, ContactVoiceTelephone?,
+                               ContactFacsimileTelephone?,
+                               ContactElectronicMailAddress?) >
+
+<!--The primary contact person.-->
+<!ELEMENT ContactPersonPrimary  (ContactPerson, ContactOrganization) >
+
+<!--The person to contact.-->
+<!ELEMENT ContactPerson  (#PCDATA) >
+
+<!--The organization supplying the service.-->
+<!ELEMENT ContactOrganization  (#PCDATA) >
+
+<!--The position title for the contact person.-->
+<!ELEMENT ContactPosition  (#PCDATA) >
+
+<!--The address for the contact supplying the service.-->
+<!ELEMENT ContactAddress  (AddressType,Address,City,StateOrProvince,PostCode,
+               Country) >
+
+<!--The type of address.-->
+<!ELEMENT AddressType  (#PCDATA) >
+
+<!--The street address.-->
+<!ELEMENT Address  (#PCDATA) >
+
+<!--The address city.-->
+<!ELEMENT City  (#PCDATA) >
+
+<!--The state or province.-->
+<!ELEMENT StateOrProvince  (#PCDATA) >
+
+<!--The zip or postal code.-->
+<!ELEMENT PostCode  (#PCDATA) >
+
+<!--The address country.-->
+<!ELEMENT Country  (#PCDATA) >
+
+<!--Contact telephone number.-->
+<!ELEMENT ContactVoiceTelephone  (#PCDATA) >
+
+<!--The contact fax number.-->
+<!ELEMENT ContactFacsimileTelephone  (#PCDATA) >
+
+<!--The e-mail address for the contact.-->
+<!ELEMENT ContactElectronicMailAddress  (#PCDATA) >
+
+
+<!-- Elements indicating what fees or access constraints are imposed. -->
+<!ELEMENT Fees (#PCDATA)>
+<!ELEMENT AccessConstraints (#PCDATA)>
+
+
+<!-- A Capability lists available request types, how exceptions
+may be reported, and whether any vendor-specific capabilities are defined.  It
+also includes an optional list of map layers available from this server. -->
+<!ELEMENT Capability 
+          (Request, Exception, VendorSpecificCapabilities?,
+	   UserDefinedSymbolization?, Layer?) >
+
+<!-- Available WMS Operations are listed in a Request element. -->
+<!ELEMENT Request (GetCapabilities, GetMap, GetFeatureInfo?,
+                   DescribeLayer?, GetLegendGraphic?, GetStyles?, PutStyles?) >
+
+<!-- For each operation offered by the server, list the available output
+formats and the online resource. -->
+<!ELEMENT GetCapabilities (Format+, DCPType+)>
+<!ELEMENT GetMap (Format+, DCPType+)>
+<!ELEMENT GetFeatureInfo (Format+, DCPType+)>
+<!-- The following optional operations only apply to SLD-enabled WMS -->
+<!ELEMENT DescribeLayer (Format+, DCPType+)>
+<!ELEMENT GetLegendGraphic (Format+, DCPType+)>
+<!ELEMENT GetStyles (Format+, DCPType+)>
+<!ELEMENT PutStyles (Format+, DCPType+)>
+
+<!-- Available Distributed Computing Platforms (DCPs) are
+listed here.  At present, only HTTP is defined. -->
+<!ELEMENT DCPType (HTTP) >
+
+<!-- Available HTTP request methods.  One or both may be supported. -->
+<!ELEMENT HTTP (Get | Post)+ >
+
+<!-- URL prefix for each HTTP request method. -->
+<!ELEMENT Get (OnlineResource) >
+<!ELEMENT Post (OnlineResource) >
+
+<!-- An Exception element indicates which error-reporting formats are supported. -->
+<!ELEMENT Exception (Format+)>
+
+<!-- Optional user-defined symbolization (used only by SLD-enabled WMSes). -->
+<!ELEMENT UserDefinedSymbolization EMPTY >
+<!ATTLIST UserDefinedSymbolization
+          SupportSLD (0 | 1) "0"
+          UserLayer (0 | 1) "0"
+          UserStyle (0 | 1) "0"
+          RemoteWFS (0 | 1) "0" >
+
+<!-- Nested list of zero or more map Layers offered by this server. -->
+<!ELEMENT Layer ( Name?, Title, Abstract?, KeywordList?, SRS*,
+                  LatLonBoundingBox?, BoundingBox*, Dimension*, Extent*,
+                  Attribution?, AuthorityURL*, Identifier*, MetadataURL*, DataURL*,
+                  FeatureListURL*, Style*, ScaleHint?, Layer* ) >
+
+<!-- Optional attributes-->
+<!ATTLIST Layer queryable (0 | 1) "0"
+                cascaded CDATA #IMPLIED
+                opaque (0 | 1) "0"
+                noSubsets (0 | 1) "0"
+                fixedWidth CDATA #IMPLIED
+                fixedHeight CDATA #IMPLIED >
+          
+<!-- Identifier for a single Spatial Reference Systems (SRS). -->
+<!ELEMENT SRS (#PCDATA) >
+
+<!-- The LatLonBoundingBox attributes indicate the edges of the enclosing
+rectangle in latitude/longitude decimal degrees (as in SRS EPSG:4326 [WGS1984
+lat/lon]). -->
+<!ELEMENT LatLonBoundingBox EMPTY>
+<!ATTLIST LatLonBoundingBox 
+          minx CDATA #REQUIRED
+          miny CDATA #REQUIRED
+          maxx CDATA #REQUIRED
+          maxy CDATA #REQUIRED>
+
+<!-- The BoundingBox attributes indicate the edges of the bounding box
+in units of the specified spatial reference system. -->
+<!ELEMENT BoundingBox EMPTY>
+<!ATTLIST BoundingBox 
+          SRS CDATA #REQUIRED
+          minx CDATA #REQUIRED
+          miny CDATA #REQUIRED
+          maxx CDATA #REQUIRED
+          maxy CDATA #REQUIRED
+          resx CDATA #IMPLIED
+          resy CDATA #IMPLIED>
+
+<!-- The Dimension element declares the _existence_ of a dimension. -->
+<!ELEMENT Dimension EMPTY >
+<!ATTLIST Dimension
+          name CDATA #REQUIRED
+          units CDATA #REQUIRED
+          unitSymbol CDATA #IMPLIED>
+
+<!-- The Extent element indicates what _values_ along a dimension are valid. -->
+<!ELEMENT Extent (#PCDATA) >
+<!ATTLIST Extent
+          name CDATA #REQUIRED
+          default CDATA #IMPLIED
+          nearestValue (0 | 1) "0"
+          multipleValues (0 | 1) "0"
+          current (0 | 1) "0">
+
+<!-- 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. -->
+<!ELEMENT Attribution ( Title?, OnlineResource?, LogoURL? )>
+<!ELEMENT LogoURL (Format, OnlineResource) >
+<!ATTLIST LogoURL
+          width NMTOKEN #REQUIRED
+          height NMTOKEN #REQUIRED>
+
+<!-- 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.  Two types
+are defined at present: 'TC211' = ISO TC211 19115; 'FGDC' = FGDC CSDGM.  The
+format element indicates how the metadata is structured. -->
+<!ELEMENT MetadataURL (Format, OnlineResource) >
+<!ATTLIST MetadataURL
+          type ( TC211 | FGDC ) #REQUIRED>
+
+<!-- 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. -->
+
+<!ELEMENT AuthorityURL (OnlineResource) >
+<!ATTLIST AuthorityURL
+          name NMTOKEN #REQUIRED >
+<!ELEMENT Identifier (#PCDATA) >
+<!ATTLIST Identifier
+          authority CDATA #REQUIRED >
+
+<!-- A Map Server may use DataURL to offer more information about the data
+underneath a particular layer. While the semantics are not well-defined, as
+long as the results of an HTTP GET request against the DataURL are properly
+MIME-typed, Viewer Clients and Cascading Map Servers can make use of this. -->
+<!ELEMENT DataURL (Format, OnlineResource) >
+
+<!-- A Map Server may use FeatureListURL to point to a list of the features
+represented in a Layer. -->
+<!ELEMENT FeatureListURL (Format, OnlineResource) >
+
+<!-- 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. -->
+<!ELEMENT Style ( Name, Title, Abstract?,
+                  LegendURL*, StyleSheetURL?, StyleURL? ) >
+
+<!-- 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 are
+provided to assist client applications in laying out space to display the
+legend. -->
+<!ELEMENT LegendURL (Format, OnlineResource) >
+<!ATTLIST LegendURL
+          width NMTOKEN #REQUIRED
+          height NMTOKEN #REQUIRED>
+
+<!-- StyleSheeetURL provides symbology information foreach Style of a Layer. -->
+<!ELEMENT StyleSheetURL (Format, OnlineResource) >
+
+<!-- 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. -->
+<!ELEMENT StyleURL (Format, OnlineResource) >
+
+<!-- Minimum and maximum scale hints for which it is appropriate to
+display this layer. -->
+<!ELEMENT ScaleHint EMPTY>
+<!ATTLIST ScaleHint
+          min CDATA #REQUIRED
+          max CDATA #REQUIRED>
+
+
+
diff --git a/mapproxy/test/schemas/wmsc/1.1.1/capabilities_1_1_1.xml b/mapproxy/test/schemas/wmsc/1.1.1/capabilities_1_1_1.xml
new file mode 100644
index 0000000..cf701c9
--- /dev/null
+++ b/mapproxy/test/schemas/wmsc/1.1.1/capabilities_1_1_1.xml
@@ -0,0 +1,303 @@
+<?xml version='1.0' encoding="UTF-8" standalone="no" ?>
+<!-- The DTD (Document Type Definition) given here must correspond to the version number declared in the WMT_MS_Capabilities element below. -->
+<!DOCTYPE WMT_MS_Capabilities SYSTEM
+ "http://www.digitalearth.gov/wmt/xml/capabilities_1_1_1.dtd"
+ [
+ <!-- Vendor-specific elements are defined here if needed. -->
+ <!-- If not needed, just leave this EMPTY declaration.  Do not
+  delete the declaration entirely. -->
+ <!ELEMENT VendorSpecificCapabilities EMPTY>
+ ]>  <!-- end of DOCTYPE declaration -->
+
+<!-- Note: this XML is just an EXAMPLE that attempts to show all
+required and optional elements for illustration.  Consult the Web Map
+Service 1.1.0 specification and the DTD for guidance on what to actually
+include and what to leave out. -->
+
+<!-- The version number listed in the WMT_MS_Capabilities element here must
+correspond to the DTD declared above.  See the WMT specification document for
+how to respond when a client requests a version number not implemented by the
+server. -->
+<WMT_MS_Capabilities version="1.1.1" updateSequence="0">
+<!-- Service Metadata -->
+<Service>
+  <!-- The WMT-defined name for this type of service -->
+  <Name>OGC:WMS</Name>
+  <!-- Human-readable title for pick lists -->
+  <Title>Acme Corp. Map Server</Title>
+  <!-- Narrative description providing additional information -->
+  <Abstract>WMT Map Server maintained by Acme Corporation.  Contact: webmaster at wmt.acme.com.  High-quality maps showing roadrunner nests and possible ambush locations.</Abstract>
+  <KeywordList>
+    <Keyword>bird</Keyword>
+    <Keyword>roadrunner</Keyword>
+    <Keyword>ambush</Keyword>
+  </KeywordList>
+  <!-- Top-level web address of service or service provider.  See also OnlineResource
+  elements under <DCPType>. -->
+  <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
+   xlink:href="http://hostname/" />
+  <!-- Contact information -->
+  <ContactInformation>
+    <ContactPersonPrimary>
+      <ContactPerson>Jeff deLaBeaujardiere</ContactPerson>
+      <ContactOrganization>NASA</ContactOrganization>
+    </ContactPersonPrimary>
+    <ContactPosition>Computer Scientist</ContactPosition>
+    <ContactAddress>
+      <AddressType>postal</AddressType>
+      <Address>NASA Goddard Space Flight Center, Code 933</Address>
+      <City>Greenbelt</City>
+      <StateOrProvince>MD</StateOrProvince>
+      <PostCode>20771</PostCode>
+      <Country>USA</Country>
+    </ContactAddress>
+    <ContactVoiceTelephone>+1 301 286-1569</ContactVoiceTelephone>
+    <ContactFacsimileTelephone>+1 301 286-1777</ContactFacsimileTelephone>
+    <ContactElectronicMailAddress>delabeau at iniki.gsfc.nasa.gov</ContactElectronicMailAddress>
+  </ContactInformation>
+  <!-- Fees or access constraints imposed. -->
+  <Fees>none</Fees>
+  <AccessConstraints>none</AccessConstraints>
+</Service>
+<Capability>
+  <Request>
+    <GetCapabilities>
+      <Format>application/vnd.ogc.wms_xml</Format>
+      <DCPType>
+        <HTTP>
+          <Get>
+            <!-- The URL here for invoking GetCapabilities using HTTP GET
+            is only a prefix to which a query string is appended. -->
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:type="simple"
+             xlink:href="http://hostname:port/path" />
+          </Get>
+          <Post>
+            <!-- The URL here for invoking GetCapabilities using HTTP POST
+            includes the complete address to which a query would be sent in
+            XML format.  This is here for future expansion; no POST encoding
+	    has yet been defined. -->
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:type="simple"
+             xlink:href="http://hostname:port/path" />
+          </Post>
+        </HTTP>
+      </DCPType>
+    </GetCapabilities>
+    <GetMap>
+      <Format>image/gif</Format>
+      <Format>image/png</Format>
+      <Format>image/jpeg</Format>
+      <DCPType>
+        <HTTP>
+          <Get>
+            <!-- The URL here for invoking GetCapabilities using HTTP GET
+            is only a prefix to which a query string is appended. -->
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:type="simple"
+             xlink:href="http://hostname:port/path" />
+          </Get>
+        </HTTP>
+      </DCPType>
+    </GetMap>
+    <GetFeatureInfo>
+      <Format>application/vnd.ogc.gml</Format>
+      <Format>text/plain</Format>
+      <Format>text/html</Format>
+      <DCPType>
+        <HTTP>
+          <Get>
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:type="simple"
+             xlink:href="http://hostname:port/path" />
+          </Get>
+        </HTTP>
+      </DCPType>
+    </GetFeatureInfo>
+    <DescribeLayer><!--optional; used only by SLD-enabled WMS-->
+      <Format>application/vnd.ogc.gml</Format>
+      <DCPType>
+        <HTTP>
+          <Get>
+            <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+             xlink:type="simple"
+             xlink:href="http://hostname:port/path" />
+          </Get>
+        </HTTP>
+      </DCPType>
+    </DescribeLayer>
+  </Request>
+  <Exception>
+    <Format>application/vnd.ogc.se_xml</Format>
+    <Format>application/vnd.ogc.se_inimage</Format>
+    <Format>application/vnd.ogc.se_blank</Format>
+  </Exception>
+  <!-- Any text or markup is allowed here, as required to describe
+       vendor-specific capabilities.  Please define elements and attributes
+       in the DOCTYPE declaration at the start of the document. -->
+  <!-- This example is empty because no VSPs were defined in preamble -->
+  <VendorSpecificCapabilities />
+  <!-- Place-holder for Styled Layer Descriptor (SLD)-enabled WMSes.
+       This element is absent for a basic WMS. -->
+  <UserDefinedSymbolization SupportSLD="1" UserLayer="1" UserStyle="1"
+	RemoteWFS="1" />
+  <Layer>
+    <Title>Acme Corp. Map Server</Title>
+    <SRS>EPSG:4326</SRS> <!-- all layers are available in at least this SRS -->
+    <AuthorityURL name="DIF_ID">
+      <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
+       xlink:href="http://gcmd.gsfc.nasa.gov/difguide/whatisadif.html" />
+    </AuthorityURL>
+    <Layer>
+      <!-- This parent layer has a Name and can therefore be requested from a Map Server, yielding a map of all subsidiary layers. -->
+      <Name>ROADS_RIVERS</Name> 
+      <Title>Roads and Rivers</Title>
+      <!-- See the spec to learn how some characteristics are inherited by
+           subsidiary layers. -->
+      <SRS>EPSG:26986</SRS> <!-- An additional SRS for this layer --> 
+      <LatLonBoundingBox minx="-71.63" miny="41.75" maxx="-70.78" maxy="42.90"/>
+      <!-- The optional resx and resy attributes below indicate the X and Y spatial
+           resolution in the units of that SRS. -->
+      <!-- The EPSG:4326 BoundingBox duplicates some of the info in LatLonBoundingBox
+           and is therefore optional, but using it here allows the additional
+           resolution attributes. -->
+      <BoundingBox SRS="EPSG:4326"
+       minx="-71.63" miny="41.75" maxx="-70.78" maxy="42.90" resx="0.01" resy="0.01"/>
+      <BoundingBox SRS="EPSG:26986"
+       minx="189000" miny="834000" maxx="285000" maxy="962000" resx="1" resy="1" />
+      <!-- Optional Title, URL and logo image of data provider. -->
+      <Attribution>
+        <Title>State College University</Title>
+        <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
+         xlink:href="http://www.university.edu/" />
+        <LogoURL width="100" height="100">
+          <Format>image/gif</Format>
+          <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+           xlink:type="simple"
+           xlink:href="http://www.university.edu/icons/logo.gif" />
+        </LogoURL>
+      </Attribution>
+      <!-- Identifier whose meaning is defined in an AuthorityURL element -->
+      <Identifier authority="DIF_ID">123456</Identifier>
+      <FeatureListURL>
+        <Format>application/vnd.ogc.se_xml"</Format>
+        <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
+         xlink:href="http://www.university.edu/data/roads_rivers.gml" />
+      </FeatureListURL>
+      <Style>
+        <Name>USGS</Name>
+        <Title>USGS Topo Map Style</Title>
+        <Abstract>Features are shown in a style like that used in USGS topographic maps.</Abstract>
+        <!-- A picture of a legend for a Layer in this Style -->
+        <LegendURL width="72" height="72">
+          <Format>image/gif</Format>
+          <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+           xlink:type="simple"
+           xlink:href="http://www.university.edu/legends/usgs.gif" />
+        </LegendURL>
+        <!-- An XSL stylesheet describing how GML feature data will rendered to create
+             a map of this layer. -->
+        <StyleSheetURL>
+          <Format>text/xsl</Format>
+          <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+           xlink:type="simple"
+           xlink:href="http://www.university.edu/stylesheets/usgs.xsl" />
+        </StyleSheetURL>
+      </Style>
+      <ScaleHint min="4000" max="35000"></ScaleHint>
+      <Layer queryable="1">
+	<Name>ROADS_1M</Name> 
+	<Title>Roads at 1:1M scale</Title>
+	<Abstract>Roads at a scale of 1 to 1 million.</Abstract>
+	<KeywordList>
+          <Keyword>road</Keyword>
+          <Keyword>transportation</Keyword>
+          <Keyword>atlas</Keyword>
+	</KeywordList>
+	<Identifier authority="DIF_ID">123456</Identifier>
+        <!-- Metadata specific to this particular layer.  The same FGDC metadata is offered in two formats. -->
+	<MetadataURL type="FGDC">
+          <Format>text/plain</Format>
+          <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+           xlink:type="simple"
+           xlink:href="http://www.university.edu/metadata/roads.txt" />
+        </MetadataURL>
+	<MetadataURL type="FGDC">
+           <Format>text/xml</Format>
+           <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+            xlink:type="simple"
+            xlink:href="http://www.university.edu/metadata/roads.xml" />
+        </MetadataURL>
+        <!-- In addition to the Style specified in the parent Layer, this Layer is available in this style. -->
+	<Style>
+	  <Name>ATLAS</Name>
+	  <Title>Road atlas style</Title>
+	  <Abstract>Roads are shown in a style like that used in a commercial road atlas.</Abstract>
+        <LegendURL width="72" height="72">
+          <Format>image/gif</Format>
+          <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+           xlink:type="simple"
+           xlink:href="http://www.university.edu/legends/atlas.gif" />
+        </LegendURL>
+	</Style>
+      </Layer>
+      <Layer queryable="1">
+	<Name>RIVERS_1M</Name>
+	<Title>Rivers at 1:1M scale</Title>
+	<Abstract>Rivers at a scale of 1 to 1 million.</Abstract>
+	<KeywordList>
+          <Keyword>river</Keyword>
+          <Keyword>canal</Keyword>
+          <Keyword>waterway</Keyword>
+	</KeywordList>
+      </Layer>
+    </Layer>
+    <Layer queryable="1">
+      <Title>Weather Forecast Data</Title>
+      <SRS>EPSG:4326</SRS> <!-- harmless repetition of common SRS -->
+      <LatLonBoundingBox minx="-180" miny="-90" maxx="180" maxy="90" />
+      <!-- These weather data are available daily from 1999-01-01 through
+           2000-08-22. -->
+      <Dimension name="time" units="ISO8601" />
+      <Extent name="time" default="2000-08-22">1999-01-01/2000-08-22/P1D</Extent>
+      <Layer>
+	<Name>Clouds</Name> 
+	<Title>Forecast cloud cover</Title>
+      </Layer>
+      <Layer>
+	<Name>Temperature</Name> 
+	<Title>Forecast temperature</Title>
+      </Layer>
+      <Layer>
+	<Name>Pressure</Name> 
+	<Title>Forecast barometric pressure</Title>
+        <!-- Pressure is available at several elevations.
+         EPSG:5030 is WGS 84 ellipsoid, units in metres.
+         Pressure is also available at several times.
+         NOTE: first list all Dimension elements, then all Extent elements. -->
+         <Dimension name="time" units="ISO8601" />
+         <Dimension name="elevation" units="EPSG:5030" />
+         <Extent name="time" default="2000-08-22">1999-01-01/2000-08-22/P1D</Extent>
+         <Extent name="elevation" default="0" nearestValue="1">0,1000,3000,5000,10000</Extent>
+      </Layer>
+    </Layer>
+    <!-- Example of a layer which is a static map of fixed
+         size which the server cannot subset or make transparent -->
+    <Layer opaque="1" noSubsets="1" fixedWidth="512" fixedHeight="256">
+      <Name>ozone_image</Name>
+      <Title>Global ozone distribution (1992)</Title>
+      <LatLonBoundingBox minx="-180" miny="-90" maxx="180" maxy="90" />
+      <Extent name="time" default="1992">1992</Extent>
+    </Layer>
+    <!-- Example of a layer which originated from another WMS and has been
+         "cascaded" by this WMS. -->
+    <Layer cascaded="1">
+      <Name>population</Name>
+      <Title>World population, annual</Title>
+      <LatLonBoundingBox minx="-180" miny="-90" maxx="180" maxy="90" />
+      <Extent name="time" default="2000">1990/2000/P1Y</Extent>
+    </Layer>
+  </Layer>
+</Capability>
+</WMT_MS_Capabilities>
+
diff --git a/mapproxy/test/schemas/wmsc/1.1.1/exception_1_1_1.dtd b/mapproxy/test/schemas/wmsc/1.1.1/exception_1_1_1.dtd
new file mode 100644
index 0000000..055044c
--- /dev/null
+++ b/mapproxy/test/schemas/wmsc/1.1.1/exception_1_1_1.dtd
@@ -0,0 +1,6 @@
+<!ELEMENT ServiceExceptionReport (ServiceException*)>
+<!ATTLIST ServiceExceptionReport version CDATA #FIXED "1.1.1">
+
+<!ELEMENT ServiceException (#PCDATA)>
+<!ATTLIST ServiceException code CDATA #IMPLIED>
+
diff --git a/mapproxy/test/schemas/wmsc/1.1.1/exception_1_1_1.xml b/mapproxy/test/schemas/wmsc/1.1.1/exception_1_1_1.xml
new file mode 100644
index 0000000..e7a4792
--- /dev/null
+++ b/mapproxy/test/schemas/wmsc/1.1.1/exception_1_1_1.xml
@@ -0,0 +1,33 @@
+<?xml version='1.0' encoding="UTF-8" standalone="no" ?>
+<!DOCTYPE ServiceExceptionReport SYSTEM
+ "http://www.digitalearth.gov/wmt/xml/exception_1_1_1.dtd">
+<ServiceExceptionReport version="1.1.1">
+  <ServiceException>
+    Plain text message about an error.
+  </ServiceException>
+  <ServiceException code="InvalidUpdateSequence">
+    Another message, this one with a Service Exception code supplied.
+  </ServiceException>
+  <ServiceException>
+    <![CDATA[
+    Error in module <foo.c>, 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:
+    ]]>
+  </ServiceException>
+  <ServiceException>
+    <![CDATA[
+      <Module>foo.c</Module>
+      <Error>An error occurred</Error>
+      <Explanation>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.</Explanation>
+    ]]>
+  </ServiceException>
+</ServiceExceptionReport>
diff --git a/mapproxy/test/schemas/wmts/1.0/ReadMe.txt b/mapproxy/test/schemas/wmts/1.0/ReadMe.txt
new file mode 100644
index 0000000..ca1c774
--- /dev/null
+++ b/mapproxy/test/schemas/wmts/1.0/ReadMe.txt
@@ -0,0 +1,32 @@
+OpenGIS(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.
+
+-----------------------------------------------------------------------
diff --git a/mapproxy/test/schemas/wmts/1.0/wmts.xsd b/mapproxy/test/schemas/wmts/1.0/wmts.xsd
new file mode 100644
index 0000000..05d701e
--- /dev/null
+++ b/mapproxy/test/schemas/wmts/1.0/wmts.xsd
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/wmts/1.0"
+	xmlns="http://www.w3.org/2001/XMLSchema" 
+	xmlns:wmts="http://www.opengis.net/wmts/1.0" 
+	elementFormDefault="qualified" xml:lang="en"
+	version="1.0.0">
+	<annotation>
+		<appinfo>wmts 2009-02-14</appinfo>
+		<documentation>
+			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/.
+		</documentation>
+	</annotation>
+	<!-- ==============================================================
+		includes and imports
+	============================================================== -->
+	<include schemaLocation="wmtsKVP.xsd"/>
+	<include schemaLocation="wmtsGetCapabilities_request.xsd"/>
+	<include schemaLocation="wmtsGetCapabilities_response.xsd"/>
+	<include schemaLocation="wmtsGetTile_request.xsd"/>
+	<include schemaLocation="wmtsPayload_response.xsd"/>
+	<include schemaLocation="wmtsGetFeatureInfo_request.xsd"/>
+	<include schemaLocation="wmtsGetFeatureInfo_response.xsd"/>
+</schema>
diff --git a/mapproxy/test/schemas/wmts/1.0/wmtsAbstract.wsdl b/mapproxy/test/schemas/wmts/1.0/wmtsAbstract.wsdl
new file mode 100644
index 0000000..97c6001
--- /dev/null
+++ b/mapproxy/test/schemas/wmts/1.0/wmtsAbstract.wsdl
@@ -0,0 +1,151 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<definitions 
+	targetNamespace="http://www.opengis.net/wmts_wsdl/1.0"
+	xmlns="http://schemas.xmlsoap.org/wsdl/"
+	xmlns:wmts_wsdl="http://www.opengis.net/wmts_wsdl/1.0"
+	xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+	xmlns:wmts="http://www.opengis.net/wmts/1.0"
+	xmlns:ows="http://www.opengis.net/ows/1.1"
+	name="WMTS">
+	
+	<!-- version 1.0.0 -->
+	
+	<xsd:annotation>
+		<xsd:appinfo>wmts_abstract.wsdl 2009-02-14</xsd:appinfo>
+		<xsd:documentation>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/.
+		</xsd:documentation>
+	</xsd:annotation>
+
+	<types>
+		<xsd:schema targetNamespace="http://www.opengis.net/wmts_wsdl/1.0">
+		    <xsd:import namespace="http://www.opengis.net/ows/1.1" 
+		    	schemaLocation="http://schemas.opengis.net/ows/1.1.0/owsCommon.xsd"/>
+		    <xsd:import namespace="http://www.opengis.net/wmts/1.0"
+		    	schemaLocation="http://schemas.opengis.net/wmts/1.0/wmts.xsd"/>
+		</xsd:schema>
+	</types>
+	
+	<!-- ****************************************
+                          Messages
+         **************************************** -->
+         
+	<message name="GetCapabilitiesMessage_GET">
+		<part name="service" type="wmts:RequestServiceType"/>
+		<part name="request" type="wmts:GetCapabilitiesValueType"/>
+		<part name="AcceptVersions" type="wmts:VersionType"/>
+		<part name="Sections" type="wmts:SectionsType"/>
+		<part name="updateSequence" type="xsd:string"/>
+		<part name="AcceptFormats" type="wmts:AcceptedFormatsType"/>
+	</message>
+	
+	<message name="GetCapabilitiesMessage_POST">
+		<part name="request" element="wmts:GetCapabilities"/>
+	</message>
+	
+	<message name="GetCapabilitiesResult">
+		<part name="response" element="wmts:Capabilities"/>
+	</message>
+	
+	<message name="GetTileMessage_GET">
+		<part name="service" type="wmts:RequestServiceType"/>
+		<part name="request" type="wmts:GetTileValueType"/>
+		<part name="version" type="wmts:VersionType"/>
+		<part name="layer" type="xsd:string"/>
+		<part name="style" type="xsd:string"/>
+		<part name="format" type="ows:MimeType"/>
+		<part name="TileMatrixSet" type="xsd:string"/>
+		<part name="TileMatrix" type="xsd:string"/>
+		<part name="TileRow" type="xsd:unsignedInt"/>
+		<part name="TileCol" type="xsd:unsignedInt"/>
+	</message>
+	
+	<message name="GetTileMessage_POST">
+		<part name="request" element="wmts:GetTile"/>
+	</message>
+	
+	<message name="GetTileResult">
+		<part name="response1" type="xsd:base64Binary"/>
+	</message>
+
+	<message name="GetTileResult_SOAP">
+		<part name="body" element="wmts:BinaryPayload" />
+	</message>
+	
+	<message name="GetFeatureInfoMessage_GET">
+		<part name="service" type="wmts:RequestServiceType"/>
+		<part name="request" type="wmts:GetFeatureInfoValueType"/>
+		<part name="version" type="wmts:VersionType"/>
+		<part name="layer" type="xsd:string"/>
+		<part name="style" type="xsd:string"/>
+		<part name="format" type="ows:MimeType"/>
+		<part name="TileMatrixSet" type="xsd:string"/>
+		<part name="TileMatrix" type="xsd:string"/>
+		<part name="TileRow" type="xsd:unsignedInt"/>
+		<part name="TileCol" type="xsd:unsignedInt"/>
+		<part name="J" type="xsd:unsignedInt"/>
+		<part name="I" type="xsd:unsignedInt"/>
+		<part name="InfoFormat" type="xsd:string"/>
+	</message>
+	
+	<message name="GetFeatureInfoMessage_POST">
+		<part name="request" element="wmts:GetFeatureInfo"/>
+	</message>
+	
+	<message name="GetFeatureInfoResult">
+		<part name="response1" type="xsd:anyType"/>
+	</message>
+
+	<message name="GetFeatureInfoResult_SOAP">
+		<part name="body" element="wmts:FeatureInfoResponse" />
+	</message>
+
+	<message name="ServiceExceptionMessage">
+		<part name="exception" element="ows:ExceptionReport"/>
+	</message>
+
+	<!-- ****************************************
+                           Ports
+         **************************************** -->
+	
+	<portType name="WMTS_HTTP_Port_GET">
+		<operation name="GetCapabilities">
+			<input message="wmts_wsdl:GetCapabilitiesMessage_GET"/>
+			<output message="wmts_wsdl:GetCapabilitiesResult"/>
+			<fault name="exception" message="wmts_wsdl:ServiceExceptionMessage"/>
+		</operation>
+		<operation name="GetTile">
+			<input message="wmts_wsdl:GetTileMessage_GET"/>
+			<output message="wmts_wsdl:GetTileResult"/>
+			<fault name="exception" message="wmts_wsdl:ServiceExceptionMessage"/>
+		</operation>
+		<operation name="GetFeatureInfo">
+			<input message="wmts_wsdl:GetFeatureInfoMessage_GET"/>
+			<output message="wmts_wsdl:GetFeatureInfoResult"/>
+			<fault name="exception" message="wmts_wsdl:ServiceExceptionMessage"/>
+		</operation>
+	</portType>
+	
+	<portType name="WMTS_HTTP_Port_SOAP">
+		<operation name="GetCapabilities">
+			<input message="wmts_wsdl:GetCapabilitiesMessage_POST"/>
+			<output message="wmts_wsdl:GetCapabilitiesResult"/>
+			<fault name="exception" message="wmts_wsdl:ServiceExceptionMessage"/>
+		</operation>
+		<operation name="GetTile">
+			<input message="wmts_wsdl:GetTileMessage_POST"/>
+			<output message="wmts_wsdl:GetTileResult_SOAP"/>
+			<fault name="exception" message="wmts_wsdl:ServiceExceptionMessage"/>
+		</operation>
+		<operation name="GetFeatureInfo">
+			<input message="wmts_wsdl:GetFeatureInfoMessage_POST"/>
+			<output message="wmts_wsdl:GetFeatureInfoResult_SOAP"/>
+			<fault name="exception" message="wmts_wsdl:ServiceExceptionMessage"/>
+		</operation>
+	</portType>
+
+</definitions>
diff --git a/mapproxy/test/schemas/wmts/1.0/wmtsGetCapabilities_request.xsd b/mapproxy/test/schemas/wmts/1.0/wmtsGetCapabilities_request.xsd
new file mode 100644
index 0000000..460014a
--- /dev/null
+++ b/mapproxy/test/schemas/wmts/1.0/wmtsGetCapabilities_request.xsd
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/wmts/1.0"
+	xmlns="http://www.w3.org/2001/XMLSchema" 
+	xmlns:ows="http://www.opengis.net/ows/1.1" 
+	xmlns:wmts="http://www.opengis.net/wmts/1.0" 
+	elementFormDefault="qualified" xml:lang="en"
+	version="1.0.0">
+	<annotation>
+		<appinfo>wmtsGetCapabilities_request 2009-01-31</appinfo>
+		<documentation>
+			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/.
+		</documentation>
+	</annotation>
+	<!-- =============================================================
+		includes and imports
+	============================================================== -->
+	<import namespace="http://www.opengis.net/ows/1.1" schemaLocation="http://schemas.opengis.net/ows/1.1.0/owsAll.xsd"/>
+	<!-- =============================================================
+		elements and types
+	============================================================== -->
+	<element name="GetCapabilities">
+		<annotation>
+			<documentation>WMTS GetCapabilities operation request.</documentation>
+		</annotation>
+		<complexType>
+			<complexContent>
+				<extension base="ows:GetCapabilitiesType">
+					<attribute name="service" type="ows:ServiceType" use="required" fixed="WMTS"/>
+				</extension>
+			</complexContent>
+		</complexType>
+	</element>
+</schema>
diff --git a/mapproxy/test/schemas/wmts/1.0/wmtsGetCapabilities_response.xsd b/mapproxy/test/schemas/wmts/1.0/wmtsGetCapabilities_response.xsd
new file mode 100644
index 0000000..8358f72
--- /dev/null
+++ b/mapproxy/test/schemas/wmts/1.0/wmtsGetCapabilities_response.xsd
@@ -0,0 +1,564 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/wmts/1.0"
+	xmlns="http://www.w3.org/2001/XMLSchema" 
+	xmlns:wmts="http://www.opengis.net/wmts/1.0" 
+	xmlns:ows="http://www.opengis.net/ows/1.1" 
+	xmlns:gml="http://www.opengis.net/gml" 
+	xmlns:xlink="http://www.w3.org/1999/xlink" 
+	elementFormDefault="qualified" xml:lang="en"
+	version="1.0.0">
+	<annotation>
+		<appinfo>wmtsGetCapabilities_response 2009-01-31</appinfo>
+		<documentation>
+			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/.
+		</documentation>
+	</annotation>
+	<!-- *********************************************************************
+	             Includes and imports.                            
+          ********************************************************************* -->
+	<import namespace="http://www.opengis.net/ows/1.1" schemaLocation="../../ows/1.1.0/owsAll.xsd"/>
+	<import namespace="http://www.w3.org/1999/xlink" schemaLocation="../../xlink/1.0.0/xlinks.xsd"/>
+	<!-- *********************************************************************
+	          The top-level Capabilities element.                            
+          ********************************************************************* -->
+	<element name="Capabilities">
+		<annotation>
+			<documentation>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.
+			</documentation>
+		</annotation>
+		<complexType>
+			<complexContent>
+				<extension base="ows:CapabilitiesBaseType">
+					<sequence>
+						<element name="Contents" type="wmts:ContentsType" minOccurs="0">
+							<annotation>
+								<documentation>Metadata about the data served by this server. 
+								For WMTS, this section SHALL contain data about layers and 
+								TileMatrixSets</documentation>
+							</annotation>
+						</element>
+						<element ref="wmts:Themes" minOccurs="0" maxOccurs="unbounded">
+							<annotation>
+								<documentation>
+								Metadata describing a theme hierarchy for the layers
+								</documentation>
+							</annotation>
+						</element>
+						<element name="WSDL" type="ows:OnlineResourceType" minOccurs="0" maxOccurs="unbounded">
+							<annotation>
+								<documentation>Reference to a WSDL resource</documentation>
+							</annotation>
+						</element>
+						<element name="ServiceMetadataURL" type="ows:OnlineResourceType" minOccurs="0" maxOccurs="unbounded">
+							<annotation>
+								<documentation>
+								Reference to a ServiceMetadata resource on resource 
+								oriented architectural style
+								</documentation>
+							</annotation>
+						</element>
+					</sequence>
+				</extension>
+			</complexContent>
+		</complexType>
+	</element>
+	<complexType name="ContentsType">
+		<complexContent>
+			<extension base="ows:ContentsBaseType">
+				<sequence>
+					<element ref="wmts:TileMatrixSet" minOccurs="0" maxOccurs="unbounded">
+						<annotation>
+							<documentation>A description of the geometry of a tile fragmentation</documentation>
+						</annotation>
+					</element>
+				</sequence>
+			</extension>
+		</complexContent>
+	</complexType>
+	<!-- ********************************************************************* -->
+	<!-- **  The Layer element.                                                      ** -->
+	<!-- ********************************************************************* -->
+	<element name="Layer" type="wmts:LayerType" substitutionGroup="ows:DatasetDescriptionSummary"/>
+	<complexType name="LayerType">
+		<complexContent>
+			<extension base="ows:DatasetDescriptionSummaryBaseType">
+				<sequence>
+					<element ref="wmts:Style" maxOccurs="unbounded">
+						<annotation>
+							<documentation>Metadata about the styles of this layer</documentation>
+						</annotation>
+					</element>
+					<element name="Format" type="ows:MimeType" maxOccurs="unbounded">
+						<annotation>
+							<documentation>Supported valid output MIME types for a tile</documentation>
+						</annotation>
+					</element>
+					<element name="InfoFormat" type="ows:MimeType" minOccurs="0" maxOccurs="unbounded">
+						<annotation>
+							<documentation>
+							Supported valid output MIME types for a FeatureInfo. 
+							If there isn't any, The server do not support FeatureInfo requests
+							for this layer.</documentation>
+						</annotation>
+					</element>
+					<element ref="wmts:Dimension" minOccurs="0" maxOccurs="unbounded">
+						<annotation>
+							<documentation>Extra dimensions for a tile and FeatureInfo requests.</documentation>
+						</annotation>
+					</element>
+					<element ref="wmts:TileMatrixSetLink" maxOccurs="unbounded">
+						<annotation>
+							<documentation>Reference to a tileMatrixSet and limits</documentation>
+						</annotation>
+					</element>
+					<element name="ResourceURL" type="wmts:URLTemplateType" 
+														minOccurs="0" maxOccurs="unbounded">
+						<annotation>
+							<documentation>
+								URL template to a tile or a FeatureInfo resource on 
+								resource oriented architectural style
+							</documentation>
+						</annotation>
+					</element>
+				</sequence>
+			</extension>
+		</complexContent>
+	</complexType>
+	<!-- ********************************************************************* -->
+	<!-- **  Style and LegendURL elements                                   ** -->
+	<!-- ********************************************************************* -->
+	<element name="Style">
+		<complexType>
+			<complexContent>
+				<extension base="ows:DescriptionType">
+					<sequence>
+						<element ref="ows:Identifier">
+							<annotation>
+								<documentation>
+									An unambiguous reference to this style, identifying 
+									a specific version when needed, normally used by software
+								</documentation>
+							</annotation>
+						</element>
+						<element ref="wmts:LegendURL" minOccurs="0" maxOccurs="unbounded">
+							<annotation>
+								<documentation>Description of an image that represents 
+								the legend of the map</documentation>
+							</annotation>
+						</element>
+					</sequence>
+					<attribute name="isDefault" type="boolean">
+						<annotation>
+							<documentation>This style is used when no style is specified</documentation>
+						</annotation>
+					</attribute>
+				</extension>
+			</complexContent>
+		</complexType>
+	</element>
+	<element name="LegendURL">
+		<annotation>
+			<documentation>
+        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.
+      </documentation>
+		</annotation>
+		<complexType>
+			<complexContent>
+				<extension base="ows:OnlineResourceType">
+					<annotation>
+						<documentation>The URL from which the legend image can be retrieved</documentation>
+					</annotation>
+					<attribute name="format" type="ows:MimeType">
+						<annotation>
+							<documentation>A supported output format for the legend image</documentation>
+						</annotation>
+					</attribute>
+					<attribute name="minScaleDenominator" type="double">
+						<annotation>
+							<documentation>Denominator of the minimum scale (inclusive) for which this legend image is valid</documentation>
+						</annotation>
+					</attribute>
+					<attribute name="maxScaleDenominator" type="double">
+						<annotation>
+							<documentation>Denominator of the maximum scale (exclusive) for which this legend image is valid</documentation>
+						</annotation>
+					</attribute>
+					<attribute name="width" type="positiveInteger">
+						<annotation>
+							<documentation>Width (in pixels) of the legend image</documentation>
+						</annotation>
+					</attribute>
+					<attribute name="height" type="positiveInteger">
+						<annotation>
+							<documentation>Height (in pixels) of the legend image</documentation>
+						</annotation>
+					</attribute>
+				</extension>
+				<!--/attributeGroup-->
+			</complexContent>
+		</complexType>
+	</element>
+	<!-- ********************************************************************* -->
+	<!-- **  The Dimension element.                                               ** -->
+	<!-- ********************************************************************* -->
+	<element name="Dimension">
+		<annotation>
+			<documentation>
+				Metadata about a particular dimension that the tiles of 
+				a layer are available.
+			</documentation>
+		</annotation>
+		<complexType>
+			<complexContent>
+				<extension base="ows:DescriptionType">
+					<sequence>
+						<element ref="ows:Identifier">
+							<annotation>
+								<documentation>A name of dimensional axis</documentation>
+							</annotation>
+						</element>
+						<element ref="ows:UOM" minOccurs="0">
+							<annotation>
+								<documentation>Units of measure of dimensional axis.</documentation>
+							</annotation>
+						</element>
+						<element name="UnitSymbol" type="string" minOccurs="0">
+							<annotation>
+								<documentation>Symbol of the units.</documentation>
+							</annotation>
+						</element>
+						<element name="Default" type="string" minOccurs="0">
+							<annotation>
+								<documentation>
+									Default value that will be used if a tile request does 
+									not specify a value or uses the keyword 'default'.
+								</documentation>
+							</annotation>
+						</element>
+						<element name="Current" type="boolean" minOccurs="0">
+							<annotation>
+								<documentation>
+									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'.
+								</documentation>
+							</annotation>
+						</element>
+						<element name="Value" type="string" maxOccurs="unbounded">
+							<annotation>
+								<documentation>Available value for this dimension.</documentation>
+							</annotation>
+						</element>
+					</sequence>
+				</extension>
+			</complexContent>
+		</complexType>
+	</element>
+	<!-- ****************************************************************************************** -->
+	<!-- **  The TileMatrixSetLink, TileMatrixSetLimits and TileMatrixLimits element. ** -->
+	<!-- ****************************************************************************************** -->
+	<element name="TileMatrixSetLink">
+		<annotation>
+			<documentation>Metadata about the TileMatrixSet reference.</documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element name="TileMatrixSet" type="string">
+					<annotation>
+						<documentation>Reference to a tileMatrixSet</documentation>
+					</annotation>
+				</element>
+				<element ref="wmts:TileMatrixSetLimits" minOccurs="0">
+					<annotation>
+						<documentation>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.</documentation>
+					</annotation>
+				</element>
+			</sequence>
+		</complexType>
+	</element>
+	<element name="TileMatrixSetLimits">
+		<annotation>
+			<documentation>
+				Metadata about a the limits of the tile row and tile col indices.
+			</documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="wmts:TileMatrixLimits" maxOccurs="unbounded">
+					<annotation>
+						<documentation>
+							Metadata describing the limits of the TileMatrixSet indices. 
+							Multiplicity must be the multiplicity of TileMatrix in this 
+							TileMatrixSet.
+						</documentation>
+					</annotation>
+				</element>
+			</sequence>
+		</complexType>
+	</element>
+	<element name="TileMatrixLimits">
+		<annotation>
+			<documentation>Metadata describing the limits of a TileMatrix 
+						for this layer.</documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element name="TileMatrix" type="string">
+					<annotation>
+						<documentation>Reference to a TileMatrix identifier</documentation>
+					</annotation>
+				</element>
+				<element name="MinTileRow" type="positiveInteger">
+					<annotation>
+						<documentation>Minimum tile row index valid for this 
+						layer. From 0 to maxTileRow</documentation>
+					</annotation>
+				</element>
+				<element name="MaxTileRow" type="positiveInteger">
+					<annotation>
+						<documentation>Maximim tile row index valid for this 
+						layer. From minTileRow to matrixWidth-1 of the tileMatrix 
+						section of this tileMatrixSet</documentation>
+					</annotation>
+				</element>
+				<element name="MinTileCol" type="positiveInteger">
+					<annotation>
+						<documentation>Minimum tile column index valid for this 
+						layer. From 0 to maxTileCol</documentation>
+					</annotation>
+				</element>
+				<element name="MaxTileCol" type="positiveInteger">
+					<annotation>
+						<documentation>Maximim tile column index valid for this layer. 
+						From minTileCol to tileHeight-1 of the tileMatrix section 
+						of this tileMatrixSet.</documentation>
+					</annotation>
+				</element>
+			</sequence>
+		</complexType>
+	</element>
+	<!-- ********************************************* -->
+	<!-- **  The URLTemplateType data type. ** -->
+	<!-- ********************************************* -->
+	<complexType name="URLTemplateType">
+		<attribute name="format" type="ows:MimeType" use="required">
+			<annotation>
+				<documentation>Format of the resource representation that can 
+				be retrieved one resolved the URL template.</documentation>
+			</annotation>
+		</attribute>
+		<attribute name="resourceType" use="required">
+			<annotation>
+				<documentation>Resource type to be retrieved. It can only 
+				be "tile" or "FeatureInfo"</documentation>
+			</annotation>
+			<simpleType>
+				<restriction base="string">
+					<enumeration value="tile"/>
+					<enumeration value="FeatureInfo"/>
+				</restriction>
+			</simpleType>
+		</attribute>
+		<attribute name="template" use="required">
+			<annotation>
+				<documentation>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.</documentation>
+			</annotation>
+			<simpleType>
+				<restriction base="string">
+					<pattern value="([A-Za-z0-9\-_\.!~\*'\(\);/\?:@\+:$,#\{\}=&]|%[A-Fa-f0-9][A-Fa-f0-9])+"/>
+				</restriction>
+			</simpleType>
+		</attribute>
+	</complexType>
+	<!-- ********************************************************************* -->
+	<!-- **  The TileMatrixSet element.                                         ** -->
+	<!-- ********************************************************************* -->
+	<element name="TileMatrixSet">
+		<annotation>
+			<documentation>Describes a particular set of tile matrices.</documentation>
+		</annotation>
+		<complexType>
+			<complexContent>
+				<extension base="ows:DescriptionType">
+					<sequence>
+						<element ref="ows:Identifier">
+							<annotation>
+								<documentation>Tile matrix set identifier</documentation>
+							</annotation>
+						</element>
+						<element ref="ows:BoundingBox" minOccurs="0">
+							<annotation>
+								<documentation>
+									Minimum bounding rectangle surrounding 
+									the visible layer presented by this tile matrix 
+									set, in the supported CRS </documentation>
+							</annotation>
+						</element>
+						<element ref="ows:SupportedCRS">
+							<annotation>
+								<documentation>Reference to one coordinate reference 
+								system (CRS).</documentation>
+							</annotation>
+						</element>
+						<element name="WellKnownScaleSet" type="anyURI" minOccurs="0">
+							<annotation>
+								<documentation>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.
+								</documentation>
+							</annotation>
+						</element>
+						<element ref="wmts:TileMatrix" maxOccurs="unbounded">
+							<annotation>
+								<documentation>Describes a scale level and its tile matrix.</documentation>
+							</annotation>
+						</element>
+					</sequence>
+				</extension>
+			</complexContent>
+		</complexType>
+	</element>
+	<!-- ********************************************************************* -->
+	<!-- **  The TileMatrix element.                                                ** -->
+	<!-- ********************************************************************* -->
+	<element name="TileMatrix">
+		<annotation>
+			<documentation>Describes a particular tile matrix.</documentation>
+		</annotation>
+		<complexType>
+			<complexContent>
+				<extension base="ows:DescriptionType">
+					<sequence>
+						<element ref="ows:Identifier">
+							<annotation>
+								<documentation>Tile matrix identifier. Typically an abreviation of 
+								the ScaleDenominator value or its equivalent pixel size</documentation>
+							</annotation>
+						</element>
+						<element name="ScaleDenominator" type="double">
+							<annotation>
+								<documentation>Scale denominator level of this tile matrix</documentation>
+							</annotation>
+						</element>
+						<element name="TopLeftCorner" type="ows:PositionType">
+							<annotation>
+								<documentation>
+									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.
+								</documentation>
+							</annotation>
+						</element>
+						<element name="TileWidth" type="positiveInteger">
+							<annotation>
+								<documentation>Width of each tile of this tile matrix in pixels.</documentation>
+							</annotation>
+						</element>
+						<element name="TileHeight" type="positiveInteger">
+							<annotation>
+								<documentation>Height of each tile of this tile matrix in pixels</documentation>
+							</annotation>
+						</element>
+						<element name="MatrixWidth" type="positiveInteger">
+							<annotation>
+								<documentation>Width of the matrix (number of tiles in width)</documentation>
+							</annotation>
+						</element>
+						<element name="MatrixHeight" type="positiveInteger">
+							<annotation>
+								<documentation>Height of the matrix (number of tiles in height)</documentation>
+							</annotation>
+						</element>
+					</sequence>
+				</extension>
+			</complexContent>
+		</complexType>
+	</element>
+	<!-- ********************************************************************* -->
+	<!-- **  The Themes, Theme and LayerRef elements.                       ** -->
+	<!-- ********************************************************************* -->
+	<element name="Themes">
+		<annotation>
+			<documentation>
+				Provides a set of hierarchical themes that the 
+				client can use to categorize the layers by.
+			</documentation>
+		</annotation>
+		<complexType>
+			<sequence>
+				<element ref="wmts:Theme" minOccurs="0" maxOccurs="unbounded">
+					<annotation>
+						<documentation>
+							Metadata describing the top-level themes where 
+							layers available on this server can be classified.
+						</documentation>
+					</annotation>
+				</element>
+			</sequence>
+		</complexType>
+	</element>
+	<element name="Theme">
+		<complexType>
+			<complexContent>
+				<extension base="ows:DescriptionType">
+					<sequence>
+						<element ref="ows:Identifier">
+							<annotation>
+								<documentation>Name of the theme</documentation>
+							</annotation>
+						</element>
+						<element ref="wmts:Theme" minOccurs="0" maxOccurs="unbounded">
+							<annotation>
+								<documentation>
+									Metadata describing the child (subordinate) themes 
+									of this theme where layers available on this server 
+									can be classified
+								</documentation>
+							</annotation>
+						</element>
+						<element name="LayerRef" type="anyURI" minOccurs="0" maxOccurs="unbounded">
+							<annotation>
+								<documentation>Reference to layer</documentation>
+							</annotation>
+						</element>
+					</sequence>
+				</extension>
+			</complexContent>
+		</complexType>
+	</element>
+</schema>
diff --git a/mapproxy/test/schemas/wmts/1.0/wmtsGetFeatureInfo_request.xsd b/mapproxy/test/schemas/wmts/1.0/wmtsGetFeatureInfo_request.xsd
new file mode 100644
index 0000000..18fd4dc
--- /dev/null
+++ b/mapproxy/test/schemas/wmts/1.0/wmtsGetFeatureInfo_request.xsd
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/wmts/1.0" 
+	xmlns="http://www.w3.org/2001/XMLSchema" 
+	xmlns:ows="http://www.opengis.net/ows/1.1" 
+	xmlns:wmts="http://www.opengis.net/wmts/1.0" 
+	elementFormDefault="qualified" xml:lang="en"
+	version="1.0.0">
+	<annotation>
+		<appinfo>wmtsGetFeatureInfo_request 2009-01-31</appinfo>
+		<documentation>
+			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/.
+		</documentation>
+	</annotation>
+	<!-- =============================================================
+		includes and imports
+	============================================================== -->
+	<import namespace="http://www.opengis.net/ows/1.1" 
+		schemaLocation="http://schemas.opengis.net/ows/1.1.0/owsAll.xsd"/>
+	<include schemaLocation="wmtsGetTile_request.xsd"/>
+	<!-- =============================================================
+		elements and types
+	============================================================== -->
+	<element name="GetFeatureInfo">
+		<complexType>
+			<sequence>
+				<element ref="wmts:GetTile">
+					<annotation>
+						<documentation>The corresponding GetTile request parameters</documentation>
+					</annotation>
+				</element>
+				<element name="J" type="nonNegativeInteger">
+					<annotation>
+						<documentation>Row index of a pixel in the tile</documentation>
+					</annotation>
+				</element>
+				<element name="I" type="nonNegativeInteger">
+					<annotation>
+						<documentation>Column index of a pixel in the tile</documentation>
+					</annotation>
+				</element>
+				<element name="InfoFormat" type="ows:MimeType">
+					<annotation>
+						<documentation>Output MIME type format of the 
+						retrieved information</documentation>
+					</annotation>
+				</element>
+			</sequence>
+			<attribute name="service" type="string" use="required" fixed="WMTS"/>
+			<attribute name="version" type="string" use="required" fixed="1.0.0"/>
+		</complexType>
+	</element>
+</schema>
diff --git a/mapproxy/test/schemas/wmts/1.0/wmtsGetFeatureInfo_response.xsd b/mapproxy/test/schemas/wmts/1.0/wmtsGetFeatureInfo_response.xsd
new file mode 100644
index 0000000..5dcc9fc
--- /dev/null
+++ b/mapproxy/test/schemas/wmts/1.0/wmtsGetFeatureInfo_response.xsd
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/wmts/1.0" 
+	xmlns="http://www.w3.org/2001/XMLSchema" 
+	xmlns:gml="http://www.opengis.net/gml"
+	xmlns:ows="http://www.opengis.net/ows/1.1"
+	xmlns:wmts="http://www.opengis.net/wmts/1.0" 
+	elementFormDefault="qualified" xml:lang="en"
+	version="1.0.0">
+	<annotation>
+		<appinfo>wmtsGetFeatureInfo_response 2009-06-14</appinfo>
+		<documentation>
+			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/.
+		</documentation>
+	</annotation>
+	<!-- =============================================================
+		includes and imports
+	============================================================== -->
+	<include schemaLocation="wmtsPayload_response.xsd"/>
+	<import namespace="http://www.opengis.net/ows/1.1" schemaLocation="http://schemas.opengis.net/ows/1.1.0/owsAll.xsd"/>
+	<import namespace="http://www.opengis.net/gml" schemaLocation="http://schemas.opengis.net/gml/3.1.1/base/gml.xsd"/>
+	<!-- =============================================================
+		elements and types
+	============================================================== -->
+	<element name="FeatureInfoResponse">
+		<complexType>
+			<choice>
+				<element ref="gml:_FeatureCollection">
+					<annotation>
+						<documentation>
+							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.
+						</documentation>
+					</annotation>
+				</element>
+				<element ref="wmts:TextPayload">
+					<annotation>
+						<documentation>
+							This allows to include any text format that is not a gml:_FeatureCollection 
+							like HTML, TXT, etc
+						</documentation>
+					</annotation>
+				</element>
+				<element ref="wmts:BinaryPayload">
+					<annotation>
+						<documentation>
+							This allows to include any binary format. Binary formats are not 
+							common response for a GeFeatureInfo requests but possible for 
+							some imaginative implementations.
+						</documentation>
+					</annotation>
+				</element>
+				<element name="AnyContent" type="anyType">
+					<annotation>
+						<documentation>
+							This allows to include any XML content that it is not any of 
+							the previous ones.
+						</documentation>
+					</annotation>
+				</element>
+			</choice>
+		</complexType>
+	</element>
+</schema>
diff --git a/mapproxy/test/schemas/wmts/1.0/wmtsGetTile_request.xsd b/mapproxy/test/schemas/wmts/1.0/wmtsGetTile_request.xsd
new file mode 100644
index 0000000..ed15bfe
--- /dev/null
+++ b/mapproxy/test/schemas/wmts/1.0/wmtsGetTile_request.xsd
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/wmts/1.0"
+	xmlns="http://www.w3.org/2001/XMLSchema" 
+	xmlns:ows="http://www.opengis.net/ows/1.1" 
+	xmlns:wmts="http://www.opengis.net/wmts/1.0" 
+	elementFormDefault="qualified" xml:lang="en"
+	version="1.0.0">
+	<annotation>
+		<appinfo>wmtsGetTile_request 2009-01-31</appinfo>
+		<documentation>
+			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/.
+		</documentation>
+	</annotation>
+	<!-- =============================================================
+		includes and imports
+	============================================================== -->
+	<import namespace="http://www.opengis.net/ows/1.1" 
+				schemaLocation="http://schemas.opengis.net/ows/1.1.0/owsAll.xsd"/>
+	<!-- =============================================================
+		elements and types
+	============================================================== -->
+	<element name="GetTile">
+		<complexType>
+			<sequence>
+				<element name="Layer" type="string">
+					<annotation>
+						<documentation>A layer identifier has to be referenced</documentation>
+					</annotation>
+				</element>
+				<element name="Style" type="string">
+					<annotation>
+						<documentation>A style identifier has to be referenced.</documentation>
+					</annotation>
+				</element>
+				<element name="Format" type="ows:MimeType">
+					<annotation>
+						<documentation>Output format of the tile</documentation>
+					</annotation>
+				</element>
+				<element ref="wmts:DimensionNameValue" minOccurs="0" maxOccurs="unbounded">
+					<annotation>
+						<documentation>Dimension name and value</documentation>
+					</annotation>
+				</element>
+				<element name="TileMatrixSet" type="string">
+					<annotation>
+						<documentation>A TileMatrixSet identifier has to be referenced</documentation>
+					</annotation>
+				</element>
+				<element name="TileMatrix" type="string">
+					<annotation>
+						<documentation>A TileMatrix identifier has to be referenced</documentation>
+					</annotation>
+				</element>
+				<element name="TileRow" type="nonNegativeInteger">
+					<annotation>
+						<documentation>Row index of tile matrix</documentation>
+					</annotation>
+				</element>
+				<element name="TileCol" type="nonNegativeInteger">
+					<annotation>
+						<documentation>Column index of tile matrix</documentation>
+					</annotation>
+				</element>
+			</sequence>
+			<attribute name="service" type="string" use="required" fixed="WMTS"/>
+			<attribute name="version" type="string" use="required" fixed="1.0.0"/>
+		</complexType>
+	</element>
+	<element name="DimensionNameValue">
+		<complexType>
+			<simpleContent>
+				<extension base="string">
+					<annotation>
+						<documentation>Dimension value</documentation>
+					</annotation>
+					<attribute name="name" type="string" use="required">
+						<annotation>
+							<documentation>Dimension name</documentation>
+						</annotation>
+					</attribute>
+				</extension>
+			</simpleContent>
+		</complexType>
+	</element>
+</schema>
diff --git a/mapproxy/test/schemas/wmts/1.0/wmtsKVP.xsd b/mapproxy/test/schemas/wmts/1.0/wmtsKVP.xsd
new file mode 100644
index 0000000..65493ff
--- /dev/null
+++ b/mapproxy/test/schemas/wmts/1.0/wmtsKVP.xsd
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/wmts/1.0"
+	xmlns="http://www.w3.org/2001/XMLSchema" 
+	xmlns:ows="http://www.opengis.net/ows/1.1" 
+	xmlns:wmts="http://www.opengis.net/wmts/1.0" 
+	elementFormDefault="qualified" xml:lang="en"
+	version="1.0.0">
+	<annotation>
+		<appinfo>wmtsGetTile_request 2009-01-31</appinfo>
+		<documentation>
+			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/.
+		</documentation>
+	</annotation>
+	<!-- =============================================================
+		includes and imports
+	============================================================== -->
+	<import namespace="http://www.opengis.net/ows/1.1" 
+				schemaLocation="http://schemas.opengis.net/ows/1.1.0/owsAll.xsd"/>
+	<!-- =============================================================
+		elements and types
+	============================================================== -->
+	<simpleType name="RequestServiceType">
+		<restriction base="string">
+			<enumeration value="WMTS"/>
+		</restriction>
+	</simpleType>
+	<simpleType name="VersionType">
+		<restriction base="string">
+			<enumeration value="1.0.0"/>
+		</restriction>
+	</simpleType>
+	<simpleType name="GetCapabilitiesValueType">
+		<restriction base="string">
+			<enumeration value="GetCapabilities"/>
+		</restriction>
+	</simpleType>
+	<simpleType name="GetTileValueType">
+		<restriction base="string">
+			<enumeration value="GetTile"/>
+		</restriction>
+	</simpleType>
+	<simpleType name="GetFeatureInfoValueType">
+		<restriction base="string">
+			<enumeration value="GetFeatureInfo"/>
+		</restriction>
+	</simpleType>
+	<simpleType name="SectionsType">
+		<annotation>
+			<documentation>
+				XML encoded identifier comma separated list of a standard 
+				MIME type, possibly a parameterized MIME type. 
+			</documentation>
+		</annotation>
+		<restriction base="string">
+			<annotation>
+				<documentation>Comma separated list of available 
+				ServiceMetadata root elements. </documentation>
+			</annotation>
+			<pattern value="(ServiceIdentification|ServiceProvider|OperationsMetadata|Contents|Themes)(,(ServiceIdentification|ServiceProvider|OperationsMetadata|Contents|Themes))*"/>
+		</restriction>
+	</simpleType>
+	<simpleType name="AcceptedFormatsType">
+		<annotation>
+			<documentation>Comma separated list of a standard MIME type, 
+			possibly a parameterized MIME type. </documentation>
+		</annotation>
+		<restriction base="string">
+			<pattern value="((application|audio|image|text|video|message|multipart|model)/.+(;\s*.+=.+)*)(,(application|audio|image|text|video|message|multipart|model)/.+(;\s*.+=.+)*)"/>
+		</restriction>
+	</simpleType>
+</schema>
diff --git a/mapproxy/test/schemas/wmts/1.0/wmtsPayload_response.xsd b/mapproxy/test/schemas/wmts/1.0/wmtsPayload_response.xsd
new file mode 100644
index 0000000..a1809f1
--- /dev/null
+++ b/mapproxy/test/schemas/wmts/1.0/wmtsPayload_response.xsd
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="http://www.opengis.net/wmts/1.0"
+	xmlns="http://www.w3.org/2001/XMLSchema" 
+	xmlns:ows="http://www.opengis.net/ows/1.1" 
+	xmlns:wmts="http://www.opengis.net/wmts/1.0" 
+	elementFormDefault="qualified" xml:lang="en"
+	version="1.0.0">
+	<annotation>
+		<appinfo>wmtsPayload_response 2009-06-15</appinfo>
+		<documentation>
+			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/.
+		</documentation>
+	</annotation>
+	<!-- =============================================================
+		includes and imports
+	============================================================== -->
+	<import namespace="http://www.opengis.net/ows/1.1" 
+				schemaLocation="http://schemas.opengis.net/ows/1.1.0/owsAll.xsd"/>
+	<!-- =============================================================
+		elements and types
+	============================================================== -->
+	<element name="BinaryPayload">
+		<complexType>
+			<sequence>
+				<element name="Format" type="ows:MimeType">
+					<annotation>
+						<documentation>
+							MIMEType format of the PayloadContent 
+							once base64 decodified.
+						</documentation>
+					</annotation>
+				</element>
+				<element name="BinaryContent" type="base64Binary">
+					<annotation>
+						<documentation>
+							Binary content encoded in base64. It could be useful to 
+							enclose it in a CDATA element to avoid XML parsing.
+						</documentation>
+					</annotation>
+				</element>
+			</sequence>
+		</complexType>
+	</element>
+	<element name="TextPayload">
+		<complexType>
+			<sequence>
+				<element name="Format" type="ows:MimeType">
+					<annotation>
+						<documentation>MIMEType format of the TextContent</documentation>
+					</annotation>
+				</element>
+				<element name="TextContent" type="string">
+					<annotation>
+						<documentation>
+							Text string like HTML, XHTML, XML or TXT. HTML and TXT data has 
+							to be enclosed in a CDATA element to avoid XML parsing.
+						</documentation>
+					</annotation>
+				</element>
+			</sequence>
+		</complexType>
+	</element>
+</schema>
diff --git a/mapproxy/test/schemas/xlink/1.0.0/ReadMe.txt b/mapproxy/test/schemas/xlink/1.0.0/ReadMe.txt
new file mode 100644
index 0000000..1499a92
--- /dev/null
+++ b/mapproxy/test/schemas/xlink/1.0.0/ReadMe.txt
@@ -0,0 +1,6 @@
+This 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
+
diff --git a/mapproxy/test/schemas/xlink/1.0.0/xlinks.xsd b/mapproxy/test/schemas/xlink/1.0.0/xlinks.xsd
new file mode 100644
index 0000000..faef81d
--- /dev/null
+++ b/mapproxy/test/schemas/xlink/1.0.0/xlinks.xsd
@@ -0,0 +1,122 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- File: xlinks.xsd  -->
+<schema targetNamespace="http://www.w3.org/1999/xlink" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2001/XMLSchema" xmlns:xsd="http://www.w3.org/2001/XMLSchema" version="2.0">
+	<annotation>
+		<appinfo source="urn:opengis:specification:gml:schema-xlinks:v3.0c2">xlinks.xsd v3.0b2 2001-07</appinfo>
+		<documentation>
+			GML 3.0 candidate xlinks schema. Copyright (c) 2001 OGC, All Rights Reserved.
+		</documentation>
+	</annotation>
+	<!-- ==============================================================
+       global declarations
+  	=============================================================== -->
+	<!-- locator attribute -->
+	<attribute name="href" type="anyURI"/>
+	<!-- semantic attributes -->
+	<attribute name="role" type="anyURI"/>
+	<attribute name="arcrole" type="anyURI"/>
+	<attribute name="title" type="string"/>
+	<!-- behavior attributes -->
+	<attribute name="show">
+		<annotation>
+			<documentation>
+        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 
+      </documentation>
+		</annotation>
+		<simpleType>
+			<restriction base="string">
+				<enumeration value="new"/>
+				<enumeration value="replace"/>
+				<enumeration value="embed"/>
+				<enumeration value="other"/>
+				<enumeration value="none"/>
+			</restriction>
+		</simpleType>
+	</attribute>
+	<attribute name="actuate">
+		<annotation>
+			<documentation>
+        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
+      </documentation>
+		</annotation>
+		<simpleType>
+			<restriction base="string">
+				<enumeration value="onLoad"/>
+				<enumeration value="onRequest"/>
+				<enumeration value="other"/>
+				<enumeration value="none"/>
+			</restriction>
+		</simpleType>
+	</attribute>
+	<!-- traversal attributes -->
+	<attribute name="label" type="string"/>
+	<attribute name="from" type="string"/>
+	<attribute name="to" type="string"/>
+	<!-- ==============================================================
+       Attributes grouped by XLink type, as specified in the W3C 
+       Proposed Recommendation (dated 2000-12-20)
+	============================================================== -->
+	<attributeGroup name="simpleLink">
+		<attribute name="type" type="string" fixed="simple" form="qualified"/>
+		<attribute ref="xlink:href" use="optional"/>
+		<attribute ref="xlink:role" use="optional"/>
+		<attribute ref="xlink:arcrole" use="optional"/>
+		<attribute ref="xlink:title" use="optional"/>
+		<attribute ref="xlink:show" use="optional"/>
+		<attribute ref="xlink:actuate" use="optional"/>
+	</attributeGroup>
+	<attributeGroup name="extendedLink">
+		<attribute name="type" type="string" fixed="extended" form="qualified"/>
+		<attribute ref="xlink:role" use="optional"/>
+		<attribute ref="xlink:title" use="optional"/>
+	</attributeGroup>
+	<attributeGroup name="locatorLink">
+		<attribute name="type" type="string" fixed="locator" form="qualified"/>
+		<attribute ref="xlink:href" use="required"/>
+		<attribute ref="xlink:role" use="optional"/>
+		<attribute ref="xlink:title" use="optional"/>
+		<attribute ref="xlink:label" use="optional"/>
+	</attributeGroup>
+	<attributeGroup name="arcLink">
+		<attribute name="type" type="string" fixed="arc" form="qualified"/>
+		<attribute ref="xlink:arcrole" use="optional"/>
+		<attribute ref="xlink:title" use="optional"/>
+		<attribute ref="xlink:show" use="optional"/>
+		<attribute ref="xlink:actuate" use="optional"/>
+		<attribute ref="xlink:from" use="optional"/>
+		<attribute ref="xlink:to" use="optional"/>
+	</attributeGroup>
+	<attributeGroup name="resourceLink">
+		<attribute name="type" type="string" fixed="resource" form="qualified"/>
+		<attribute ref="xlink:role" use="optional"/>
+		<attribute ref="xlink:title" use="optional"/>
+		<attribute ref="xlink:label" use="optional"/>
+	</attributeGroup>
+	<attributeGroup name="titleLink">
+		<attribute name="type" type="string" fixed="title" form="qualified"/>
+	</attributeGroup>
+	<attributeGroup name="emptyLink">
+		<attribute name="type" type="string" fixed="none" form="qualified"/>
+	</attributeGroup>
+</schema>
diff --git a/mapproxy/test/schemas/xml.xsd b/mapproxy/test/schemas/xml.xsd
new file mode 100644
index 0000000..aea7d0d
--- /dev/null
+++ b/mapproxy/test/schemas/xml.xsd
@@ -0,0 +1,287 @@
+<?xml version='1.0'?>
+<?xml-stylesheet href="../2008/09/xsd.xsl" type="text/xsl"?>
+<xs:schema targetNamespace="http://www.w3.org/XML/1998/namespace" 
+  xmlns:xs="http://www.w3.org/2001/XMLSchema" 
+  xmlns   ="http://www.w3.org/1999/xhtml"
+  xml:lang="en">
+
+ <xs:annotation>
+  <xs:documentation>
+   <div>
+    <h1>About the XML namespace</h1>
+
+    <div class="bodytext">
+     <p>
+      This schema document describes the XML namespace, in a form
+      suitable for import by other schema documents.
+     </p>
+     <p>
+      See <a href="http://www.w3.org/XML/1998/namespace.html">
+      http://www.w3.org/XML/1998/namespace.html</a> and
+      <a href="http://www.w3.org/TR/REC-xml">
+      http://www.w3.org/TR/REC-xml</a> for information 
+      about this namespace.
+     </p>
+     <p>
+      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.
+     </p>
+     <p>   
+      See further below in this document for more information about <a
+      href="#usage">how to refer to this schema document from your own
+      XSD schema documents</a> and about <a href="#nsversioning">the
+      namespace-versioning policy governing this schema document</a>.
+     </p>
+    </div>
+   </div>
+  </xs:documentation>
+ </xs:annotation>
+
+ <xs:attribute name="lang">
+  <xs:annotation>
+   <xs:documentation>
+    <div>
+     
+      <h3>lang (as an attribute name)</h3>
+      <p>
+       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.</p>
+     
+    </div>
+    <div>
+     <h4>Notes</h4>
+     <p>
+      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.  
+     </p>
+     <p>
+      See BCP 47 at <a href="http://www.rfc-editor.org/rfc/bcp/bcp47.txt">
+       http://www.rfc-editor.org/rfc/bcp/bcp47.txt</a>
+      and the IANA language subtag registry at
+      <a href="http://www.iana.org/assignments/language-subtag-registry">
+       http://www.iana.org/assignments/language-subtag-registry</a>
+      for further information.
+     </p>
+     <p>
+      The union allows for the 'un-declaration' of xml:lang with
+      the empty string.
+     </p>
+    </div>
+   </xs:documentation>
+  </xs:annotation>
+  <xs:simpleType>
+   <xs:union memberTypes="xs:language">
+    <xs:simpleType>    
+     <xs:restriction base="xs:string">
+      <xs:enumeration value=""/>
+     </xs:restriction>
+    </xs:simpleType>
+   </xs:union>
+  </xs:simpleType>
+ </xs:attribute>
+
+ <xs:attribute name="space">
+  <xs:annotation>
+   <xs:documentation>
+    <div>
+     
+      <h3>space (as an attribute name)</h3>
+      <p>
+       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.</p>
+     
+    </div>
+   </xs:documentation>
+  </xs:annotation>
+  <xs:simpleType>
+   <xs:restriction base="xs:NCName">
+    <xs:enumeration value="default"/>
+    <xs:enumeration value="preserve"/>
+   </xs:restriction>
+  </xs:simpleType>
+ </xs:attribute>
+ 
+ <xs:attribute name="base" type="xs:anyURI"> <xs:annotation>
+   <xs:documentation>
+    <div>
+     
+      <h3>base (as an attribute name)</h3>
+      <p>
+       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.</p>
+     
+     <p>
+      See <a
+      href="http://www.w3.org/TR/xmlbase/">http://www.w3.org/TR/xmlbase/</a>
+      for information about this attribute.
+     </p>
+    </div>
+   </xs:documentation>
+  </xs:annotation>
+ </xs:attribute>
+ 
+ <xs:attribute name="id" type="xs:ID">
+  <xs:annotation>
+   <xs:documentation>
+    <div>
+     
+      <h3>id (as an attribute name)</h3> 
+      <p>
+       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.</p>
+     
+     <p>
+      See <a
+      href="http://www.w3.org/TR/xml-id/">http://www.w3.org/TR/xml-id/</a>
+      for information about this attribute.
+     </p>
+    </div>
+   </xs:documentation>
+  </xs:annotation>
+ </xs:attribute>
+
+ <xs:attributeGroup name="specialAttrs">
+  <xs:attribute ref="xml:base"/>
+  <xs:attribute ref="xml:lang"/>
+  <xs:attribute ref="xml:space"/>
+  <xs:attribute ref="xml:id"/>
+ </xs:attributeGroup>
+
+ <xs:annotation>
+  <xs:documentation>
+   <div>
+   
+    <h3>Father (in any context at all)</h3> 
+
+    <div class="bodytext">
+     <p>
+      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:
+     </p>
+     <blockquote>
+       <p>
+	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".
+       </p>
+     </blockquote>
+    </div>
+   </div>
+  </xs:documentation>
+ </xs:annotation>
+
+ <xs:annotation>
+  <xs:documentation>
+   <div xml:id="usage" id="usage">
+    <h2><a name="usage">About this schema document</a></h2>
+
+    <div class="bodytext">
+     <p>
+      This schema defines attributes and an attribute group suitable
+      for use by schemas wishing to allow <code>xml:base</code>,
+      <code>xml:lang</code>, <code>xml:space</code> or
+      <code>xml:id</code> attributes on elements they define.
+     </p>
+     <p>
+      To enable this, such a schema must import this schema for
+      the XML namespace, e.g. as follows:
+     </p>
+     <pre>
+          <schema . . .>
+           . . .
+           <import namespace="http://www.w3.org/XML/1998/namespace"
+                      schemaLocation="http://www.w3.org/2001/xml.xsd"/>
+     </pre>
+     <p>
+      or
+     </p>
+     <pre>
+           <import namespace="http://www.w3.org/XML/1998/namespace"
+                      schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
+     </pre>
+     <p>
+      Subsequently, qualified reference to any of the attributes or the
+      group defined below will have the desired effect, e.g.
+     </p>
+     <pre>
+          <type . . .>
+           . . .
+           <attributeGroup ref="xml:specialAttrs"/>
+     </pre>
+     <p>
+      will define a type which will schema-validate an instance element
+      with any of those attributes.
+     </p>
+    </div>
+   </div>
+  </xs:documentation>
+ </xs:annotation>
+
+ <xs:annotation>
+  <xs:documentation>
+   <div id="nsversioning" xml:id="nsversioning">
+    <h2><a name="nsversioning">Versioning policy for this schema document</a></h2>
+    <div class="bodytext">
+     <p>
+      In keeping with the XML Schema WG's standard versioning
+      policy, this schema document will persist at
+      <a href="http://www.w3.org/2009/01/xml.xsd">
+       http://www.w3.org/2009/01/xml.xsd</a>.
+     </p>
+     <p>
+      At the date of issue it can also be found at
+      <a href="http://www.w3.org/2001/xml.xsd">
+       http://www.w3.org/2001/xml.xsd</a>.
+     </p>
+     <p>
+      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 <a href="http://www.w3.org/2001/xml.xsd">
+       http://www.w3.org/2001/xml.xsd 
+      </a> 
+      will change accordingly; the version at 
+      <a href="http://www.w3.org/2009/01/xml.xsd">
+       http://www.w3.org/2009/01/xml.xsd 
+      </a> 
+      will not change.
+     </p>
+     <p>
+      Previous dated (and unchanging) versions of this schema 
+      document are at:
+     </p>
+     <ul>
+      <li><a href="http://www.w3.org/2009/01/xml.xsd">
+	http://www.w3.org/2009/01/xml.xsd</a></li>
+      <li><a href="http://www.w3.org/2007/08/xml.xsd">
+	http://www.w3.org/2007/08/xml.xsd</a></li>
+      <li><a href="http://www.w3.org/2004/10/xml.xsd">
+	http://www.w3.org/2004/10/xml.xsd</a></li>
+      <li><a href="http://www.w3.org/2001/03/xml.xsd">
+	http://www.w3.org/2001/03/xml.xsd</a></li>
+     </ul>
+    </div>
+   </div>
+  </xs:documentation>
+ </xs:annotation>
+
+</xs:schema>
+
diff --git a/mapproxy/test/system/__init__.py b/mapproxy/test/system/__init__.py
new file mode 100644
index 0000000..c3bbd71
--- /dev/null
+++ b/mapproxy/test/system/__init__.py
@@ -0,0 +1,91 @@
+# 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 with_statement, 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)
diff --git a/mapproxy/test/system/fixture/auth.yaml b/mapproxy/test/system/fixture/auth.yaml
new file mode 100644
index 0000000..d78e057
--- /dev/null
+++ b/mapproxy/test/system/fixture/auth.yaml
@@ -0,0 +1,67 @@
+services:
+  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.png
\ No newline at end of file
diff --git a/mapproxy/test/system/fixture/cache.mbtiles b/mapproxy/test/system/fixture/cache.mbtiles
new file mode 100644
index 0000000..6573f93
Binary files /dev/null and b/mapproxy/test/system/fixture/cache.mbtiles differ
diff --git a/mapproxy/test/system/fixture/cache_data/wms_cache_EPSG900913/01/000/000/000/000/000/001.jpeg b/mapproxy/test/system/fixture/cache_data/wms_cache_EPSG900913/01/000/000/000/000/000/001.jpeg
new file mode 100644
index 0000000..8b7710c
Binary files /dev/null and b/mapproxy/test/system/fixture/cache_data/wms_cache_EPSG900913/01/000/000/000/000/000/001.jpeg differ
diff --git a/mapproxy/test/system/fixture/cache_data/wms_cache_transparent_EPSG900913/01/000/000/000/000/000/001.png b/mapproxy/test/system/fixture/cache_data/wms_cache_transparent_EPSG900913/01/000/000/000/000/000/001.png
new file mode 100644
index 0000000..f7e16fe
Binary files /dev/null and b/mapproxy/test/system/fixture/cache_data/wms_cache_transparent_EPSG900913/01/000/000/000/000/000/001.png differ
diff --git a/mapproxy/test/system/fixture/cache_grid_names.yaml b/mapproxy/test/system/fixture/cache_grid_names.yaml
new file mode 100644
index 0000000..9006b67
--- /dev/null
+++ b/mapproxy/test/system/fixture/cache_grid_names.yaml
@@ -0,0 +1,50 @@
+globals:
+  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
diff --git a/mapproxy/test/system/fixture/cache_mbtiles.yaml b/mapproxy/test/system/fixture/cache_mbtiles.yaml
new file mode 100644
index 0000000..a83cf31
--- /dev/null
+++ b/mapproxy/test/system/fixture/cache_mbtiles.yaml
@@ -0,0 +1,28 @@
+globals:
+  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
+
diff --git a/mapproxy/test/system/fixture/cache_source.yaml b/mapproxy/test/system/fixture/cache_source.yaml
new file mode 100644
index 0000000..27ff572
--- /dev/null
+++ b/mapproxy/test/system/fixture/cache_source.yaml
@@ -0,0 +1,81 @@
+services:
+  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
diff --git a/mapproxy/test/system/fixture/cgi.py b/mapproxy/test/system/fixture/cgi.py
new file mode 100644
index 0000000..f6c9f25
--- /dev/null
+++ b/mapproxy/test/system/fixture/cgi.py
@@ -0,0 +1,16 @@
+#! /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')
\ No newline at end of file
diff --git a/mapproxy/test/system/fixture/combined_sources.yaml b/mapproxy/test/system/fixture/combined_sources.yaml
new file mode 100644
index 0000000..c0ccfa7
--- /dev/null
+++ b/mapproxy/test/system/fixture/combined_sources.yaml
@@ -0,0 +1,130 @@
+globals:
+  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
diff --git a/mapproxy/test/system/fixture/coverage.yaml b/mapproxy/test/system/fixture/coverage.yaml
new file mode 100644
index 0000000..46aed1b
--- /dev/null
+++ b/mapproxy/test/system/fixture/coverage.yaml
@@ -0,0 +1,79 @@
+globals:
+  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 at 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/)
+
+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
diff --git a/mapproxy/test/system/fixture/disable_storage.yaml b/mapproxy/test/system/fixture/disable_storage.yaml
new file mode 100644
index 0000000..e66be9d
--- /dev/null
+++ b/mapproxy/test/system/fixture/disable_storage.yaml
@@ -0,0 +1,25 @@
+globals:
+  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
diff --git a/mapproxy/test/system/fixture/empty_ogrdata.geojson b/mapproxy/test/system/fixture/empty_ogrdata.geojson
new file mode 100644
index 0000000..188bb9e
--- /dev/null
+++ b/mapproxy/test/system/fixture/empty_ogrdata.geojson
@@ -0,0 +1 @@
+{"type": "FeatureCollection", "features": []}
\ No newline at end of file
diff --git a/mapproxy/test/system/fixture/formats.yaml b/mapproxy/test/system/fixture/formats.yaml
new file mode 100644
index 0000000..ee44277
--- /dev/null
+++ b/mapproxy/test/system/fixture/formats.yaml
@@ -0,0 +1,74 @@
+globals:
+  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 at 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/)
+
+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
diff --git a/mapproxy/test/system/fixture/inspire.yaml b/mapproxy/test/system/fixture/inspire.yaml
new file mode 100644
index 0000000..487b23a
--- /dev/null
+++ b/mapproxy/test/system/fixture/inspire.yaml
@@ -0,0 +1,103 @@
+globals:
+  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 at 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/)
+    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'
diff --git a/mapproxy/test/system/fixture/inspire_full.yaml b/mapproxy/test/system/fixture/inspire_full.yaml
new file mode 100644
index 0000000..6b4d701
--- /dev/null
+++ b/mapproxy/test/system/fixture/inspire_full.yaml
@@ -0,0 +1,126 @@
+globals:
+  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 at 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/)
+      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 at 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'
diff --git a/mapproxy/test/system/fixture/kml_layer.yaml b/mapproxy/test/system/fixture/kml_layer.yaml
new file mode 100644
index 0000000..0b74217
--- /dev/null
+++ b/mapproxy/test/system/fixture/kml_layer.yaml
@@ -0,0 +1,66 @@
+globals:
+  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,bar
\ No newline at end of file
diff --git a/mapproxy/test/system/fixture/layer.yaml b/mapproxy/test/system/fixture/layer.yaml
new file mode 100644
index 0000000..0d9a5ce
--- /dev/null
+++ b/mapproxy/test/system/fixture/layer.yaml
@@ -0,0 +1,232 @@
+globals:
+  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 at 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/)
+
+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
+  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
diff --git a/mapproxy/test/system/fixture/layergroups.yaml b/mapproxy/test/system/fixture/layergroups.yaml
new file mode 100644
index 0000000..cc9fb2f
--- /dev/null
+++ b/mapproxy/test/system/fixture/layergroups.yaml
@@ -0,0 +1,57 @@
+services:
+  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
diff --git a/mapproxy/test/system/fixture/layergroups_root.yaml b/mapproxy/test/system/fixture/layergroups_root.yaml
new file mode 100644
index 0000000..ba7a828
--- /dev/null
+++ b/mapproxy/test/system/fixture/layergroups_root.yaml
@@ -0,0 +1,106 @@
+services:
+  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:
+# 
diff --git a/mapproxy/test/system/fixture/legendgraphic.yaml b/mapproxy/test/system/fixture/legendgraphic.yaml
new file mode 100644
index 0000000..6e909f3
--- /dev/null
+++ b/mapproxy/test/system/fixture/legendgraphic.yaml
@@ -0,0 +1,95 @@
+globals:
+  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 at 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/)
+
+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
+
diff --git a/mapproxy/test/system/fixture/mapnik_source.yaml b/mapproxy/test/system/fixture/mapnik_source.yaml
new file mode 100644
index 0000000..7a073df
--- /dev/null
+++ b/mapproxy/test/system/fixture/mapnik_source.yaml
@@ -0,0 +1,54 @@
+services:
+  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: False
\ No newline at end of file
diff --git a/mapproxy/test/system/fixture/mapproxy_export.yaml b/mapproxy/test/system/fixture/mapproxy_export.yaml
new file mode 100644
index 0000000..5f48a02
--- /dev/null
+++ b/mapproxy/test/system/fixture/mapproxy_export.yaml
@@ -0,0 +1,12 @@
+globals:
+    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
+
diff --git a/mapproxy/test/system/fixture/mapserver.yaml b/mapproxy/test/system/fixture/mapserver.yaml
new file mode 100644
index 0000000..de61ef0
--- /dev/null
+++ b/mapproxy/test/system/fixture/mapserver.yaml
@@ -0,0 +1,23 @@
+services:
+  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
diff --git a/mapproxy/test/system/fixture/mixed_mode.yaml b/mapproxy/test/system/fixture/mixed_mode.yaml
new file mode 100644
index 0000000..6819e38
--- /dev/null
+++ b/mapproxy/test/system/fixture/mixed_mode.yaml
@@ -0,0 +1,51 @@
+globals:
+  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 at 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/)
+
+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
+
diff --git a/mapproxy/test/system/fixture/multiapp1.yaml b/mapproxy/test/system/fixture/multiapp1.yaml
new file mode 100644
index 0000000..e873718
--- /dev/null
+++ b/mapproxy/test/system/fixture/multiapp1.yaml
@@ -0,0 +1,20 @@
+services:
+  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
diff --git a/mapproxy/test/system/fixture/multiapp2.yaml b/mapproxy/test/system/fixture/multiapp2.yaml
new file mode 100644
index 0000000..a827079
--- /dev/null
+++ b/mapproxy/test/system/fixture/multiapp2.yaml
@@ -0,0 +1,19 @@
+services:
+  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
diff --git a/mapproxy/test/system/fixture/renderd_client.yaml b/mapproxy/test/system/fixture/renderd_client.yaml
new file mode 100644
index 0000000..8980dca
--- /dev/null
+++ b/mapproxy/test/system/fixture/renderd_client.yaml
@@ -0,0 +1,55 @@
+globals:
+  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
+
+
diff --git a/mapproxy/test/system/fixture/scalehints.yaml b/mapproxy/test/system/fixture/scalehints.yaml
new file mode 100644
index 0000000..a182b87
--- /dev/null
+++ b/mapproxy/test/system/fixture/scalehints.yaml
@@ -0,0 +1,72 @@
+globals:
+  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 at 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/)
+
+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
diff --git a/mapproxy/test/system/fixture/seed.yaml b/mapproxy/test/system/fixture/seed.yaml
new file mode 100644
index 0000000..bc50750
--- /dev/null
+++ b/mapproxy/test/system/fixture/seed.yaml
@@ -0,0 +1,94 @@
+coverages:
+  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'
\ No newline at end of file
diff --git a/mapproxy/test/system/fixture/seed_mapproxy.yaml b/mapproxy/test/system/fixture/seed_mapproxy.yaml
new file mode 100644
index 0000000..d5aac9c
--- /dev/null
+++ b/mapproxy/test/system/fixture/seed_mapproxy.yaml
@@ -0,0 +1,36 @@
+globals:
+  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: baz
\ No newline at end of file
diff --git a/mapproxy/test/system/fixture/seed_old.yaml b/mapproxy/test/system/fixture/seed_old.yaml
new file mode 100644
index 0000000..8e26d7f
--- /dev/null
+++ b/mapproxy/test/system/fixture/seed_old.yaml
@@ -0,0 +1,12 @@
+views:
+  one:
+    bbox: [-180, -90, 180, 90]
+    bbox_srs: 'EPSG:4326'
+    srs: ['EPSG:4326']
+    level: [0, 0]
+
+seeds:
+  one:
+    views: [one]
+    remove_before:
+      days: 1
\ No newline at end of file
diff --git a/mapproxy/test/system/fixture/seed_timeouts.yaml b/mapproxy/test/system/fixture/seed_timeouts.yaml
new file mode 100644
index 0000000..959f80b
--- /dev/null
+++ b/mapproxy/test/system/fixture/seed_timeouts.yaml
@@ -0,0 +1,12 @@
+seeds:
+    test:
+        caches: [wms_cache]
+        grids: [GLOBAL_GEODETIC]
+        coverages: [world]
+        levels:
+            to: 2
+
+coverages:
+  world:
+    bbox: [-180, -90, 180, 90]
+    bbox_srs: 'EPSG:4326'
\ No newline at end of file
diff --git a/mapproxy/test/system/fixture/seed_timeouts_mapproxy.yaml b/mapproxy/test/system/fixture/seed_timeouts_mapproxy.yaml
new file mode 100644
index 0000000..2debd8e
--- /dev/null
+++ b/mapproxy/test/system/fixture/seed_timeouts_mapproxy.yaml
@@ -0,0 +1,27 @@
+globals:
+  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.2
\ No newline at end of file
diff --git a/mapproxy/test/system/fixture/seedonly.yaml b/mapproxy/test/system/fixture/seedonly.yaml
new file mode 100644
index 0000000..e0f3396
--- /dev/null
+++ b/mapproxy/test/system/fixture/seedonly.yaml
@@ -0,0 +1,53 @@
+globals:
+  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 at 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/)
+
+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,bar
\ No newline at end of file
diff --git a/mapproxy/test/system/fixture/sld.yaml b/mapproxy/test/system/fixture/sld.yaml
new file mode 100644
index 0000000..77fe637
--- /dev/null
+++ b/mapproxy/test/system/fixture/sld.yaml
@@ -0,0 +1,35 @@
+services:
+  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:
+          <sld:StyledLayerDescriptor />
diff --git a/mapproxy/test/system/fixture/source_errors.yaml b/mapproxy/test/system/fixture/source_errors.yaml
new file mode 100644
index 0000000..19ce69d
--- /dev/null
+++ b/mapproxy/test/system/fixture/source_errors.yaml
@@ -0,0 +1,82 @@
+globals:
+  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
diff --git a/mapproxy/test/system/fixture/source_errors_raise.yaml b/mapproxy/test/system/fixture/source_errors_raise.yaml
new file mode 100644
index 0000000..bfe90f2
--- /dev/null
+++ b/mapproxy/test/system/fixture/source_errors_raise.yaml
@@ -0,0 +1,82 @@
+globals:
+  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
diff --git a/mapproxy/test/system/fixture/tileservice_origin.yaml b/mapproxy/test/system/fixture/tileservice_origin.yaml
new file mode 100644
index 0000000..51496a4
--- /dev/null
+++ b/mapproxy/test/system/fixture/tileservice_origin.yaml
@@ -0,0 +1,26 @@
+globals:
+  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
diff --git a/mapproxy/test/system/fixture/tilesource_minmax_res.yaml b/mapproxy/test/system/fixture/tilesource_minmax_res.yaml
new file mode 100644
index 0000000..4374afb
--- /dev/null
+++ b/mapproxy/test/system/fixture/tilesource_minmax_res.yaml
@@ -0,0 +1,22 @@
+services:
+  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
diff --git a/mapproxy/test/system/fixture/util-conf-base-grids.yaml b/mapproxy/test/system/fixture/util-conf-base-grids.yaml
new file mode 100644
index 0000000..57385a1
--- /dev/null
+++ b/mapproxy/test/system/fixture/util-conf-base-grids.yaml
@@ -0,0 +1,5 @@
+grids:
+    webmercator:
+      base: GLOBAL_WEBMERCATOR
+    geodetic:
+      base: GLOBAL_GEODETIC
diff --git a/mapproxy/test/system/fixture/util-conf-overwrite.yaml b/mapproxy/test/system/fixture/util-conf-overwrite.yaml
new file mode 100644
index 0000000..f81c003
--- /dev/null
+++ b/mapproxy/test/system/fixture/util-conf-overwrite.yaml
@@ -0,0 +1,13 @@
+caches:
+    __all__:
+        cache:
+            type: sqlite
+
+sources:
+    osm____:
+        req:
+          param: 42
+    ____roads_wms:
+        supported_srs: ['EPSG:3857']
+        coverage:
+            bbox: [0, 0, 90, 90]
diff --git a/mapproxy/test/system/fixture/util-conf-wms-111-cap.xml b/mapproxy/test/system/fixture/util-conf-wms-111-cap.xml
new file mode 100644
index 0000000..0b1c4a3
--- /dev/null
+++ b/mapproxy/test/system/fixture/util-conf-wms-111-cap.xml
@@ -0,0 +1,90 @@
+<?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">
+<Service>
+  <Name>OGC:WMS</Name>
+  <Title>Omniscale OpenStreetMap WMS</Title>
+  <Abstract>Omniscale OpenStreetMap WMS (powered by MapProxy)</Abstract>
+  <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://omniscale.de/"/>
+  <ContactInformation>
+      <ContactPersonPrimary>
+        <ContactPerson>Oliver Tonnhofer</ContactPerson>
+        <ContactOrganization>Omniscale</ContactOrganization>
+      </ContactPersonPrimary>
+      <ContactPosition>Technical Director</ContactPosition>
+      <ContactAddress>
+        <AddressType>postal</AddressType>
+        <Address>Nadorster Str. 60</Address>
+        <City>Oldenburg</City>
+        <StateOrProvince></StateOrProvince>
+        <PostCode>26123</PostCode>
+        <Country>Germany</Country>
+      </ContactAddress>
+      <ContactVoiceTelephone>+49(0)441-9392774-0</ContactVoiceTelephone>
+      <ContactFacsimileTelephone>+49(0)441-9392774-9</ContactFacsimileTelephone>
+      <ContactElectronicMailAddress>osm at omniscale.de</ContactElectronicMailAddress>
+  </ContactInformation>
+  <Fees>none</Fees>
+  <AccessConstraints>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/)</AccessConstraints>
+</Service>
+<Capability>
+  <Request>
+    <GetCapabilities>
+      <Format>application/vnd.ogc.wms_xml</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://osm.omniscale.net/proxy/service?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetCapabilities>
+    <GetMap>
+        <Format>image/jpeg</Format>
+        <Format>image/png</Format>
+        <Format>image/gif</Format>
+        <Format>image/GeoTIFF</Format>
+        <Format>image/tiff</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://osm.omniscale.net/proxy/service?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetMap>
+    <GetFeatureInfo>
+      <Format>text/plain</Format>
+      <Format>text/html</Format>
+      <Format>application/vnd.ogc.gml</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://osm.omniscale.net/proxy/service?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetFeatureInfo>
+  </Request>
+  <Exception>
+    <Format>application/vnd.ogc.se_xml</Format>
+    <Format>application/vnd.ogc.se_inimage</Format>
+    <Format>application/vnd.ogc.se_blank</Format>
+  </Exception>
+  <Layer>
+    <Title>Omniscale OpenStreetMap WMS</Title>
+    <SRS>EPSG:4326 EPSG:4258 CRS:84 EPSG:900913 EPSG:31466 EPSG:31467 EPSG:31468 EPSG:25831 EPSG:25832 EPSG:25833 EPSG:3857</SRS>
+    <LatLonBoundingBox minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+    <BoundingBox SRS="EPSG:4326" minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+    <Layer>
+      <Name>osm</Name>
+      <Title>OpenStreetMap (complete map)</Title>
+      <LatLonBoundingBox minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+      <BoundingBox SRS="EPSG:4326" minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+    </Layer>
+    <Layer>
+      <Name>osm_roads</Name>
+      <Title>OpenStreetMap (streets only)</Title>
+      <LatLonBoundingBox minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+      <BoundingBox SRS="EPSG:4326" minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+    </Layer>
+  </Layer>
+</Capability>
+</WMT_MS_Capabilities>
\ No newline at end of file
diff --git a/mapproxy/test/system/fixture/util_grids.yaml b/mapproxy/test/system/fixture/util_grids.yaml
new file mode 100644
index 0000000..7cdd1b6
--- /dev/null
+++ b/mapproxy/test/system/fixture/util_grids.yaml
@@ -0,0 +1,29 @@
+services:
+    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: 25
\ No newline at end of file
diff --git a/mapproxy/test/system/fixture/util_wms_capabilities111.xml b/mapproxy/test/system/fixture/util_wms_capabilities111.xml
new file mode 100644
index 0000000..f24db45
--- /dev/null
+++ b/mapproxy/test/system/fixture/util_wms_capabilities111.xml
@@ -0,0 +1,130 @@
+<?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">
+<Service>
+  <Name>OGC:WMS</Name>
+  <Title>MapProxy WMS Proxy</Title>
+  <Abstract>This is the fantastic MapProxy.</Abstract>
+  <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://mapproxy.org/"/>
+  <ContactInformation>
+      <ContactPersonPrimary>
+        <ContactPerson>Your Name Here</ContactPerson>
+        <ContactOrganization></ContactOrganization>
+      </ContactPersonPrimary>
+      <ContactPosition>Technical Director</ContactPosition>
+      <ContactAddress>
+        <AddressType>postal</AddressType>
+        <Address>Fakestreet 123</Address>
+        <City>Somewhere</City>
+        <StateOrProvince></StateOrProvince>
+        <PostCode>12345</PostCode>
+        <Country>Germany</Country>
+      </ContactAddress>
+      <ContactVoiceTelephone>+49(0)000-000000-0</ContactVoiceTelephone>
+      <ContactFacsimileTelephone>+49(0)000-000000-0</ContactFacsimileTelephone>
+      <ContactElectronicMailAddress>info at omniscale.de</ContactElectronicMailAddress>
+  </ContactInformation>
+  <Fees>None</Fees>
+  <AccessConstraints>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/)</AccessConstraints>
+</Service>
+<Capability>
+  <Request>
+    <GetCapabilities>
+      <Format>application/vnd.ogc.wms_xml</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://127.0.0.1:8080/service?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetCapabilities>
+    <GetMap>
+        <Format>image/gif</Format>
+        <Format>image/png</Format>
+        <Format>image/tiff</Format>
+        <Format>image/jpeg</Format>
+        <Format>image/GeoTIFF</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://127.0.0.1:8080/service?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetMap>
+    <GetFeatureInfo>
+      <Format>text/plain</Format>
+      <Format>text/html</Format>
+      <Format>application/vnd.ogc.gml</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://127.0.0.1:8080/service?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetFeatureInfo>
+  </Request>
+  <Exception>
+    <Format>application/vnd.ogc.se_xml</Format>
+    <Format>application/vnd.ogc.se_inimage</Format>
+    <Format>application/vnd.ogc.se_blank</Format>
+  </Exception>
+  <Layer>
+    <Title>MapProxy WMS Proxy</Title>
+    <SRS>EPSG:31467</SRS>
+    <SRS>EPSG:31466</SRS>
+    <SRS>EPSG:4326</SRS>
+    <SRS>EPSG:25831</SRS>
+    <SRS>EPSG:25833</SRS>
+    <SRS>EPSG:25832</SRS>
+    <SRS>EPSG:31468</SRS>
+    <SRS>EPSG:900913</SRS>
+    <SRS>CRS:84</SRS>
+    <SRS>EPSG:4258</SRS>
+    <LatLonBoundingBox minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+    <BoundingBox SRS="EPSG:900913" minx="-20037508.3428" miny="-20037508.3428" maxx="20037508.3428" maxy="20037508.3428" />
+    <BoundingBox SRS="EPSG:4326" minx="-180.0" miny="-85.0511287798" maxx="180.0" maxy="85.0511287798" />
+    <Layer>
+      <Name>osm</Name>
+      <Title>Omniscale OSM WMS - osm.omniscale.net</Title>
+      <LatLonBoundingBox minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+      <BoundingBox SRS="EPSG:900913" minx="-20037508.3428" miny="-20037508.3428" maxx="20037508.3428" maxy="20037508.3428" />
+      <BoundingBox SRS="EPSG:4326" minx="-180.0" miny="-85.0511287798" maxx="180.0" maxy="85.0511287798" />
+    </Layer>
+    <Layer>
+      <Name>root</Name>
+      <Title>Root Layer</Title>
+      <LatLonBoundingBox minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+      <BoundingBox SRS="EPSG:900913" minx="-20037508.3428" miny="-20037508.3428" maxx="20037508.3428" maxy="20037508.3428" />
+      <BoundingBox SRS="EPSG:4326" minx="-180.0" miny="-85.0511287798" maxx="180.0" maxy="85.0511287798" />
+      <Layer>
+        <Name>layer1</Name>
+        <Title>Title of Layer 1</Title>
+        <LatLonBoundingBox minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+        <BoundingBox SRS="EPSG:900913" minx="-20037508.3428" miny="-20037508.3428" maxx="20037508.3428" maxy="20037508.3428" />
+        <BoundingBox SRS="EPSG:4326" minx="-180.0" miny="-85.0511287798" maxx="180.0" maxy="85.0511287798" />
+        <Layer>
+          <Name>layer1a</Name>
+          <Title>Title of Layer 1a</Title>
+          <LatLonBoundingBox minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+          <BoundingBox SRS="EPSG:900913" minx="-20037508.3428" miny="-20037508.3428" maxx="20037508.3428" maxy="20037508.3428" />
+          <BoundingBox SRS="EPSG:4326" minx="-180.0" miny="-85.0511287798" maxx="180.0" maxy="85.0511287798" />
+        </Layer>
+        <Layer>
+          <Name>layer1b</Name>
+          <Title>Title of Layer 1b</Title>
+          <LatLonBoundingBox minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+          <BoundingBox SRS="EPSG:900913" minx="-20037508.3428" miny="-20037508.3428" maxx="20037508.3428" maxy="20037508.3428" />
+          <BoundingBox SRS="EPSG:4326" minx="-180.0" miny="-85.0511287798" maxx="180.0" maxy="85.0511287798" />
+        </Layer>
+      </Layer>
+      <Layer>
+        <Name>layer2</Name>
+        <Title>Title of Layer 2</Title>
+        <LatLonBoundingBox minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+        <BoundingBox SRS="EPSG:900913" minx="-20037508.3428" miny="-20037508.3428" maxx="20037508.3428" maxy="20037508.3428" />
+        <BoundingBox SRS="EPSG:4326" minx="-180.0" miny="-85.0511287798" maxx="180.0" maxy="85.0511287798" />
+      </Layer>
+    </Layer>
+  </Layer>
+</Capability>
+</WMT_MS_Capabilities>
\ No newline at end of file
diff --git a/mapproxy/test/system/fixture/util_wms_capabilities130.xml b/mapproxy/test/system/fixture/util_wms_capabilities130.xml
new file mode 100644
index 0000000..f8cb4a1
--- /dev/null
+++ b/mapproxy/test/system/fixture/util_wms_capabilities130.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<WMS_Capabilities xmlns="http://www.opengis.net/wms" xmlns:sld="http://www.opengis.net/sld" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.3.0" xsi:schemaLocation="http://www.opengis.net/wms http://schemas.opengis.net/wms/1.3.0/capabilities_1_3_0.xsd">
+<Service>
+  <Name>WMS</Name>
+  <Title>MapProxy WMS Proxy</Title>
+  <Abstract>This is the fantastic MapProxy.</Abstract>
+  <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://mapproxy.org/"/>
+  <ContactInformation>
+      <ContactPersonPrimary>
+        <ContactPerson>Your Name Here</ContactPerson>
+        <ContactOrganization></ContactOrganization>
+      </ContactPersonPrimary>
+      <ContactPosition>Technical Director</ContactPosition>
+      <ContactAddress>
+        <AddressType>postal</AddressType>
+        <Address>Fakestreet 123</Address>
+        <City>Somewhere</City>
+        <StateOrProvince></StateOrProvince>
+        <PostCode>12345</PostCode>
+        <Country>Germany</Country>
+      </ContactAddress>
+      <ContactVoiceTelephone>+49(0)000-000000-0</ContactVoiceTelephone>
+      <ContactFacsimileTelephone>+49(0)000-000000-0</ContactFacsimileTelephone>
+      <ContactElectronicMailAddress>info at omniscale.de</ContactElectronicMailAddress>
+  </ContactInformation>
+    <Fees>None</Fees>
+    <AccessConstraints>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/)</AccessConstraints>
+</Service>
+<Capability>
+  <Request>
+    <GetCapabilities>
+      <Format>text/xml</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xlink:href="http://127.0.0.1:8080/service?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetCapabilities>
+    <GetMap>
+      <Format>image/gif</Format>
+      <Format>image/png</Format>
+      <Format>image/tiff</Format>
+      <Format>image/jpeg</Format>
+      <Format>image/GeoTIFF</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xlink:href="http://127.0.0.1:8080/service?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetMap>
+    <GetFeatureInfo>
+      <Format>text/plain</Format>
+      <Format>text/html</Format>
+      <Format>text/xml</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xlink:href="http://127.0.0.1:8080/service?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetFeatureInfo>
+  </Request>
+  <Exception>
+    <Format>XML</Format>
+    <Format>INIMAGE</Format>
+    <Format>BLANK</Format>
+  </Exception>
+  <Layer>
+    <Title>MapProxy WMS Proxy</Title>
+    <CRS>EPSG:900913</CRS>
+    <CRS>EPSG:4326</CRS>
+    <CRS>EPSG:4258</CRS>
+    <CRS>CRS:84</CRS>
+    <CRS>EPSG:3857</CRS>
+    <EX_GeographicBoundingBox>
+      <westBoundLongitude>-180</westBoundLongitude>
+      <eastBoundLongitude>180</eastBoundLongitude>
+      <southBoundLatitude>-85.0511287798</southBoundLatitude>
+      <northBoundLatitude>85.0511287798</northBoundLatitude>
+    </EX_GeographicBoundingBox>
+    <BoundingBox CRS="CRS:84" minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+    <BoundingBox CRS="EPSG:900913" minx="-20037508.3428" miny="-20037508.3428" maxx="20037508.3428" maxy="20037508.3428" />
+    <BoundingBox CRS="EPSG:4326" minx="-85.0511287798" miny="-180.0" maxx="85.0511287798" maxy="180.0" />
+    <BoundingBox CRS="EPSG:3857" minx="-20037508.3428" miny="-20037508.3428" maxx="20037508.3428" maxy="20037508.3428" />
+    <Layer>
+      <Name>osm</Name>
+      <Title>Omniscale OSM WMS - osm.omniscale.net</Title>
+      <EX_GeographicBoundingBox>
+        <westBoundLongitude>-180</westBoundLongitude>
+        <eastBoundLongitude>180</eastBoundLongitude>
+        <southBoundLatitude>-85.0511287798</southBoundLatitude>
+        <northBoundLatitude>85.0511287798</northBoundLatitude>
+      </EX_GeographicBoundingBox>
+      <BoundingBox CRS="CRS:84" minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+      <BoundingBox CRS="EPSG:900913" minx="-20037508.3428" miny="-20037508.3428" maxx="20037508.3428" maxy="20037508.3428" />
+      <BoundingBox CRS="EPSG:4326" minx="-85.0511287798" miny="-180.0" maxx="85.0511287798" maxy="180.0" />
+      <BoundingBox CRS="EPSG:3857" minx="-20037508.3428" miny="-20037508.3428" maxx="20037508.3428" maxy="20037508.3428" />
+    </Layer>
+  </Layer>
+</Capability>
+</WMS_Capabilities>
\ No newline at end of file
diff --git a/mapproxy/test/system/fixture/util_wms_capabilities_service_exception.xml b/mapproxy/test/system/fixture/util_wms_capabilities_service_exception.xml
new file mode 100644
index 0000000..e661c3f
--- /dev/null
+++ b/mapproxy/test/system/fixture/util_wms_capabilities_service_exception.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE ServiceExceptionReport SYSTEM "http://schemas.opengis.net/wms/1.1.1/exception_1_1_1.dtd">
+<ServiceExceptionReport version="1.1.1">
+    <ServiceException>unknown WMS request type 'GetCapabilitie'</ServiceException>
+</ServiceExceptionReport>
\ No newline at end of file
diff --git a/mapproxy/test/system/fixture/watermark.yaml b/mapproxy/test/system/fixture/watermark.yaml
new file mode 100644
index 0000000..f09358b
--- /dev/null
+++ b/mapproxy/test/system/fixture/watermark.yaml
@@ -0,0 +1,50 @@
+globals:
+  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
diff --git a/mapproxy/test/system/fixture/wms_srs_extent.yaml b/mapproxy/test/system/fixture/wms_srs_extent.yaml
new file mode 100644
index 0000000..2f6c92a
--- /dev/null
+++ b/mapproxy/test/system/fixture/wms_srs_extent.yaml
@@ -0,0 +1,21 @@
+services:
+  wms:
+    image_formats: ['image/png', 'image/jpeg']
+    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: [0.0, 3500000.0, 1000000.0, 8500000.0]
+        srs: 'EPSG:25832'
+    md:
+      title: MapProxy test fixture ☃
+
+layers:
+  - name: direct
+    title: Direct Layer
+    sources: [direct]
+
+sources:
+  direct:
+    type: wms
+    req:
+      url: http://localhost:42423/service
+      layers: bar
diff --git a/mapproxy/test/system/fixture/wms_versions.yaml b/mapproxy/test/system/fixture/wms_versions.yaml
new file mode 100644
index 0000000..01bfaa6
--- /dev/null
+++ b/mapproxy/test/system/fixture/wms_versions.yaml
@@ -0,0 +1,40 @@
+services:
+  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 at 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/)
+
+layers:
+  - name: direct
+    title: Direct Layer
+    sources: [direct]
+
+sources:
+  direct:
+    type: wms
+    req:
+      url: http://localhost:42423/service
+      layers: bar
diff --git a/mapproxy/test/system/fixture/wmts.yaml b/mapproxy/test/system/fixture/wmts.yaml
new file mode 100644
index 0000000..a743619
--- /dev/null
+++ b/mapproxy/test/system/fixture/wmts.yaml
@@ -0,0 +1,111 @@
+globals:
+  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 at 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/)
+
+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: ul
\ No newline at end of file
diff --git a/mapproxy/test/system/fixture/wmts_dimensions.yaml b/mapproxy/test/system/fixture/wmts_dimensions.yaml
new file mode 100644
index 0000000..cc8bfa0
--- /dev/null
+++ b/mapproxy/test/system/fixture/wmts_dimensions.yaml
@@ -0,0 +1,57 @@
+services:
+  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']
diff --git a/mapproxy/test/system/fixture/xslt_featureinfo.yaml b/mapproxy/test/system/fixture/xslt_featureinfo.yaml
new file mode 100644
index 0000000..79a830e
--- /dev/null
+++ b/mapproxy/test/system/fixture/xslt_featureinfo.yaml
@@ -0,0 +1,53 @@
+services:
+  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_one
\ No newline at end of file
diff --git a/mapproxy/test/system/test_auth.py b/mapproxy/test/system/test_auth.py
new file mode 100644
index 0000000..02c44a4
--- /dev/null
+++ b/mapproxy/test/system/test_auth.py
@@ -0,0 +1,828 @@
+# 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 __future__ import with_statement, 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': [-40.0, -40.0, 0.0, 0.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)
+
diff --git a/mapproxy/test/system/test_behind_proxy.py b/mapproxy/test/system/test_behind_proxy.py
new file mode 100644
index 0000000..2e49d9d
--- /dev/null
+++ b/mapproxy/test/system/test_behind_proxy.py
@@ -0,0 +1,80 @@
+# 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 __future__ import with_statement, 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
+
diff --git a/mapproxy/test/system/test_cache_grid_names.py b/mapproxy/test/system/test_cache_grid_names.py
new file mode 100644
index 0000000..c9f9ee6
--- /dev/null
+++ b/mapproxy/test/system/test_cache_grid_names.py
@@ -0,0 +1,94 @@
+# 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 __future__ import with_statement
+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)
diff --git a/mapproxy/test/system/test_cache_mbtiles.py b/mapproxy/test/system/test_cache_mbtiles.py
new file mode 100644
index 0000000..006447c
--- /dev/null
+++ b/mapproxy/test/system/test_cache_mbtiles.py
@@ -0,0 +1,75 @@
+# 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 __future__ import with_statement, 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)
diff --git a/mapproxy/test/system/test_cache_source.py b/mapproxy/test/system/test_cache_source.py
new file mode 100644
index 0000000..c2b0680
--- /dev/null
+++ b/mapproxy/test/system/test_cache_source.py
@@ -0,0 +1,112 @@
+# 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 __future__ import with_statement
+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')
+
diff --git a/mapproxy/test/system/test_combined_sources.py b/mapproxy/test/system/test_combined_sources.py
new file mode 100644
index 0000000..b0c1db9
--- /dev/null
+++ b/mapproxy/test/system/test_combined_sources.py
@@ -0,0 +1,251 @@
+# 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 with_statement, 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)
+
diff --git a/mapproxy/test/system/test_coverage.py b/mapproxy/test/system/test_coverage.py
new file mode 100644
index 0000000..0b2187d
--- /dev/null
+++ b/mapproxy/test/system/test_coverage.py
@@ -0,0 +1,115 @@
+# 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 with_statement, 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')
+
diff --git a/mapproxy/test/system/test_decorate_img.py b/mapproxy/test/system/test_decorate_img.py
new file mode 100644
index 0000000..21bba0b
--- /dev/null
+++ b/mapproxy/test/system/test_decorate_img.py
@@ -0,0 +1,188 @@
+# 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.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}
+        )
diff --git a/mapproxy/test/system/test_disable_storage.py b/mapproxy/test/system/test_disable_storage.py
new file mode 100644
index 0000000..c00be1d
--- /dev/null
+++ b/mapproxy/test/system/test_disable_storage.py
@@ -0,0 +1,54 @@
+# 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 __future__ import with_statement
+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)
+        
\ No newline at end of file
diff --git a/mapproxy/test/system/test_formats.py b/mapproxy/test/system/test_formats.py
new file mode 100644
index 0000000..1f1ea72
--- /dev/null
+++ b/mapproxy/test/system/test_formats.py
@@ -0,0 +1,164 @@
+# 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 with_statement, 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)
diff --git a/mapproxy/test/system/test_inspire_vs.py b/mapproxy/test/system/test_inspire_vs.py
new file mode 100644
index 0000000..ba1f8d1
--- /dev/null
+++ b/mapproxy/test/system/test_inspire_vs.py
@@ -0,0 +1,140 @@
+# This file is part of the MapProxy project.
+# Copyright (C) 2015 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
+
+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)
diff --git a/mapproxy/test/system/test_kml.py b/mapproxy/test/system/test_kml.py
new file mode 100644
index 0000000..429e6e9
--- /dev/null
+++ b/mapproxy/test/system/test_kml.py
@@ -0,0 +1,236 @@
+# 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 with_statement
+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'], 'max-age=%d public' % 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
+
diff --git a/mapproxy/test/system/test_layergroups.py b/mapproxy/test/system/test_layergroups.py
new file mode 100644
index 0000000..b32f2b4
--- /dev/null
+++ b/mapproxy/test/system/test_layergroups.py
@@ -0,0 +1,143 @@
+# 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 with_statement, 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)
\ No newline at end of file
diff --git a/mapproxy/test/system/test_legendgraphic.py b/mapproxy/test/system/test_legendgraphic.py
new file mode 100644
index 0000000..8791f9d
--- /dev/null
+++ b/mapproxy/test/system/test_legendgraphic.py
@@ -0,0 +1,206 @@
+# 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 with_statement, 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')
+
diff --git a/mapproxy/test/system/test_mapnik.py b/mapproxy/test/system/test_mapnik.py
new file mode 100644
index 0000000..27fefb6
--- /dev/null
+++ b/mapproxy/test/system/test_mapnik.py
@@ -0,0 +1,155 @@
+# 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 __future__ import with_statement, 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"""
+<?xml version="1.0"?>
+<!DOCTYPE Map>
+<Map background-color="#ff0000" bgcolor="#ff0000" srs="+proj=latlong +datum=WGS84">
+    <Layer name="marker">
+        <StyleName>marker</StyleName>
+        <Datasource>
+            <Parameter name="type">ogr</Parameter>
+            <Parameter name="file">test_point.geojson</Parameter>
+            <Parameter name="layer">OGRGeoJSON</Parameter>
+        </Datasource>
+    </Layer>
+    <Style name="marker">
+        <Rule>
+            <MarkersSymbolizer fill="transparent" width="10" height="10" stroke="black" stroke-width="3" placement="point" marker-type="ellipse"/>
+        </Rule>
+    </Style>
+</Map>
+""".strip()
+
+test_point_geojson = b"""
+{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-45, -45]}, "properties": {}}
+""".strip()
+
+mapnik_transp_xml = b"""
+<?xml version="1.0"?>
+<!DOCTYPE Map>
+<Map background-color="transparent" srs="+proj=latlong +datum=WGS84">
+</Map>
+""".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)))
+
+
+
diff --git a/mapproxy/test/system/test_mapserver.py b/mapproxy/test/system/test_mapserver.py
new file mode 100644
index 0000000..4b17ef3
--- /dev/null
+++ b/mapproxy/test/system/test_mapserver.py
@@ -0,0 +1,69 @@
+# 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 __future__ import with_statement, 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))])
diff --git a/mapproxy/test/system/test_mixed_mode_format.py b/mapproxy/test/system/test_mixed_mode_format.py
new file mode 100644
index 0000000..18b7f0c
--- /dev/null
+++ b/mapproxy/test/system/test_mixed_mode_format.py
@@ -0,0 +1,153 @@
+# 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 with_statement, 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')
+
+ at 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
+
diff --git a/mapproxy/test/system/test_multiapp.py b/mapproxy/test/system/test_multiapp.py
new file mode 100644
index 0000000..c59807f
--- /dev/null
+++ b/mapproxy/test/system/test_multiapp.py
@@ -0,0 +1,103 @@
+# 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 with_statement, 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))
diff --git a/mapproxy/test/system/test_renderd_client.py b/mapproxy/test/system/test_renderd_client.py
new file mode 100644
index 0000000..de76b12
--- /dev/null
+++ b/mapproxy/test/system/test_renderd_client.py
@@ -0,0 +1,269 @@
+# 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 with_statement
+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')
diff --git a/mapproxy/test/system/test_scalehints.py b/mapproxy/test/system/test_scalehints.py
new file mode 100644
index 0000000..b8d8c5c
--- /dev/null
+++ b/mapproxy/test/system/test_scalehints.py
@@ -0,0 +1,112 @@
+# 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 with_statement, 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)
diff --git a/mapproxy/test/system/test_seed.py b/mapproxy/test/system/test_seed.py
new file mode 100644
index 0000000..d5f6abf
--- /dev/null
+++ b/mapproxy/test/system/test_seed.py
@@ -0,0 +1,387 @@
+# 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 with_statement
+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
+
diff --git a/mapproxy/test/system/test_seed_only.py b/mapproxy/test/system/test_seed_only.py
new file mode 100644
index 0000000..8dbee8c
--- /dev/null
+++ b/mapproxy/test/system/test_seed_only.py
@@ -0,0 +1,82 @@
+# 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 with_statement, 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))])
\ No newline at end of file
diff --git a/mapproxy/test/system/test_sld.py b/mapproxy/test/system/test_sld.py
new file mode 100644
index 0000000..7620c71
--- /dev/null
+++ b/mapproxy/test/system/test_sld.py
@@ -0,0 +1,88 @@
+# 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 with_statement, 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'<sld>')
+    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('<sld>'), '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('<sld:StyledLayerDescriptor />'),
+            'method': 'POST'},
+           {'body': create_tmp_image((200, 200), format='png')}
+          )]):
+            resp = self.app.get(self.common_map_req)
+            eq_(resp.content_type, 'image/png')
+
+
diff --git a/mapproxy/test/system/test_source_errors.py b/mapproxy/test/system/test_source_errors.py
new file mode 100644
index 0000000..b14d392
--- /dev/null
+++ b/mapproxy/test/system/test_source_errors.py
@@ -0,0 +1,256 @@
+# This file is part of the MapProxy project.
+# Copyright (C) 2010-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 __future__ import with_statement, 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)
diff --git a/mapproxy/test/system/test_tilesource_minmax_res.py b/mapproxy/test/system/test_tilesource_minmax_res.py
new file mode 100644
index 0000000..9990780
--- /dev/null
+++ b/mapproxy/test/system/test_tilesource_minmax_res.py
@@ -0,0 +1,50 @@
+# 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 __future__ import with_statement
+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')
diff --git a/mapproxy/test/system/test_tms.py b/mapproxy/test/system/test_tms.py
new file mode 100644
index 0000000..265355d
--- /dev/null
+++ b/mapproxy/test/system/test_tms.py
@@ -0,0 +1,247 @@
+# 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 with_statement
+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'], 'max-age=%d public' % 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')
diff --git a/mapproxy/test/system/test_tms_origin.py b/mapproxy/test/system/test_tms_origin.py
new file mode 100644
index 0000000..b2712b5
--- /dev/null
+++ b/mapproxy/test/system/test_tms_origin.py
@@ -0,0 +1,52 @@
+# 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 with_statement
+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)
+
+
+
diff --git a/mapproxy/test/system/test_util_conf.py b/mapproxy/test/system/test_util_conf.py
new file mode 100644
index 0000000..bff485b
--- /dev/null
+++ b/mapproxy/test/system/test_util_conf.py
@@ -0,0 +1,226 @@
+# -:- 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 __future__ import with_statement
+
+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)
+
+
+
diff --git a/mapproxy/test/system/test_util_export.py b/mapproxy/test/system/test_util_export.py
new file mode 100644
index 0000000..fc83313
--- /dev/null
+++ b/mapproxy/test/system/test_util_export.py
@@ -0,0 +1,132 @@
+# 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 __future__ import with_statement
+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')
+
+ at 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'))
diff --git a/mapproxy/test/system/test_util_grids.py b/mapproxy/test/system/test_util_grids.py
new file mode 100644
index 0000000..fff5000
--- /dev/null
+++ b/mapproxy/test/system/test_util_grids.py
@@ -0,0 +1,95 @@
+# 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 with_statement
+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
+
+
diff --git a/mapproxy/test/system/test_util_wms_capabilities.py b/mapproxy/test/system/test_util_wms_capabilities.py
new file mode 100644
index 0000000..676cb04
--- /dev/null
+++ b/mapproxy/test/system/test_util_wms_capabilities.py
@@ -0,0 +1,101 @@
+# 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 with_statement
+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')
+
diff --git a/mapproxy/test/system/test_watermark.py b/mapproxy/test/system/test_watermark.py
new file mode 100644
index 0000000..cf4abdf
--- /dev/null
+++ b/mapproxy/test/system/test_watermark.py
@@ -0,0 +1,74 @@
+# 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 __future__ import with_statement, 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))
diff --git a/mapproxy/test/system/test_wms.py b/mapproxy/test/system/test_wms.py
new file mode 100644
index 0000000..8c18d0c
--- /dev/null
+++ b/mapproxy/test/system/test_wms.py
@@ -0,0 +1,1098 @@
+# 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
+
+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:31467']),
+            [2750000.0, 5000000.0, 4250000.0, 6500000.0])
+        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, -80.0, 170.0, 80.0])
+
+        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)
+        is_111_exception(resp.lxml, 'Request too large or invalid BBOX.')
+
+    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=5197367.93088,5312902.73895,5311885.44223,5434731.78213'
+                                  '&styles=&VERSION=1.1.1&feature_count=100'
+                                  '&WIDTH=200&QUERY_LAYERS=foo,bar&X=14&Y=78'},
+                        {'body': b'info', 'headers': {'content-type': 'text/plain'}})
+
+        # out fi point at x=10,y=20
+        p_25832  = (3570269+10*(3643458 - 3570269)/200, 5540889+20*(5614078 - 5540889)/200)
+        # the transformed fi point at x=10,y=22
+        p_900913 = (5197367.93088+14*(5311885.44223 - 5197367.93088)/200,
+                    5312902.73895+78*(5434731.78213 - 5312902.73895)/200)
+
+        # are they the same?
+        # check with tolerance: pixel resolution is ~570 and x/y position is rounded to pizel
+        assert abs(SRS(25832).transform_to(SRS(900913), p_25832)[0] - p_900913[0]) < 570/2
+        assert abs(SRS(25832).transform_to(SRS(900913), p_25832)[1] - p_900913[1]) < 570/2
+
+        with mock_httpd(('localhost', 42423), [expected_req], bbox_aware_query_comparator=True):
+            self.common_fi_req.params['bbox'] = '3570269,5540889,3643458,5614078'
+            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']), -89.999999, 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')
+
diff --git a/mapproxy/test/system/test_wms_srs_extent.py b/mapproxy/test/system/test_wms_srs_extent.py
new file mode 100644
index 0000000..1ccd118
--- /dev/null
+++ b/mapproxy/test/system/test_wms_srs_extent.py
@@ -0,0 +1,98 @@
+# 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 __future__ import with_statement, division
+
+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 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 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])])
\ No newline at end of file
diff --git a/mapproxy/test/system/test_wms_version.py b/mapproxy/test/system/test_wms_version.py
new file mode 100644
index 0000000..72bca92
--- /dev/null
+++ b/mapproxy/test/system/test_wms_version.py
@@ -0,0 +1,57 @@
+# 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 __future__ import with_statement, 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)
diff --git a/mapproxy/test/system/test_wmsc.py b/mapproxy/test/system/test_wmsc.py
new file mode 100644
index 0000000..cd2ef11
--- /dev/null
+++ b/mapproxy/test/system/test_wmsc.py
@@ -0,0 +1,92 @@
+# 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 with_statement, 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')
diff --git a/mapproxy/test/system/test_wmts.py b/mapproxy/test/system/test_wmts.py
new file mode 100644
index 0000000..65c1f4d
--- /dev/null
+++ b/mapproxy/test/system/test_wmts.py
@@ -0,0 +1,158 @@
+# -:- encoding: utf8 -:-
+# 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 __future__ import with_statement, 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_(len(xml.xpath('//wmts:Layer', namespaces=ns_wmts)), 5)
+        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)
+
+    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')
+
diff --git a/mapproxy/test/system/test_wmts_dimensions.py b/mapproxy/test/system/test_wmts_dimensions.py
new file mode 100644
index 0000000..84df4ed
--- /dev/null
+++ b/mapproxy/test/system/test_wmts_dimensions.py
@@ -0,0 +1,164 @@
+# 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 __future__ import with_statement, 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')
+
diff --git a/mapproxy/test/system/test_wmts_restful.py b/mapproxy/test/system/test_wmts_restful.py
new file mode 100644
index 0000000..7def075
--- /dev/null
+++ b/mapproxy/test/system/test_wmts_restful.py
@@ -0,0 +1,114 @@
+# 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 __future__ import with_statement, 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')
+
+    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')
+
diff --git a/mapproxy/test/system/test_xslt_featureinfo.py b/mapproxy/test/system/test_xslt_featureinfo.py
new file mode 100644
index 0000000..0cd01c3
--- /dev/null
+++ b/mapproxy/test/system/test_xslt_featureinfo.py
@@ -0,0 +1,205 @@
+# 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 with_statement, 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"""
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+ <xsl:template match="/">
+   <baz>
+     <foo><xsl:value-of select="/a/b/text()" /></foo>
+   </baz>
+ </xsl:template>
+</xsl:stylesheet>""".strip()
+
+xslt_input_html = b"""
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+ <xsl:template match="/">
+   <baz>
+     <foo><xsl:value-of select="/html/body/p" /></foo>
+   </baz>
+ </xsl:template>
+</xsl:stylesheet>""".strip()
+
+
+xslt_output = b"""
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+ <xsl:template match="/">
+    <bars>
+      <xsl:apply-templates/>
+    </bars>
+ </xsl:template>
+
+ <xsl:template match="foo">
+     <bar><xsl:value-of select="text()" /></bar>
+ </xsl:template>
+</xsl:stylesheet>""".strip()
+
+xslt_output_html = b"""
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+ <xsl:template match="/">
+    <html>
+      <body>
+        <h1>Bars</h1>
+        <xsl:apply-templates/>
+      </body>
+    </html>
+ </xsl:template>
+
+ <xsl:template match="foo">
+     <p><xsl:value-of select="text()" /></p>
+ </xsl:template>
+</xsl:stylesheet>""".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"<a><b>Bar</b></a>"
+        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'<bars><bar>Bar</bar></bars>')
+
+    def test_get_featureinfo_130(self):
+        fi_body = b"<a><b>Bar</b></a>"
+        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'<bars><bar>Bar</bar></bars>')
+
+    def test_get_multiple_featureinfo(self):
+        fi_body1 = b"<a><b>Bar1</b></a>"
+        fi_body2 = b"<a><b>Bar2</b></a>"
+        fi_body3 = b"<body><h1>Hello<p>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'<bars><bar>Bar1</bar><bar>Bar2</bar><bar>Bar3</bar></bars>')
+
+    def test_get_multiple_featureinfo_html_out(self):
+        fi_body1 = b"<a><b>Bar1</b></a>"
+        fi_body2 = b"<a><b>Bar2</b></a>"
+        fi_body3 = b"<body><h1>Hello<p>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'<html><body><h1>Bars</h1><p>Bar1</p><p>Bar2</p><p>Bar3</p></body></html>')
+
+    def test_mixed_featureinfo(self):
+        fi_body1 = b"Hello"
+        fi_body2 = b"<a><b>Bar2</b></a>"
+        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'Hello<baz><foo>Bar2</foo></baz>')
\ No newline at end of file
diff --git a/mapproxy/test/test_http_helper.py b/mapproxy/test/test_http_helper.py
new file mode 100644
index 0000000..2d17fcf
--- /dev/null
+++ b/mapproxy/test/test_http_helper.py
@@ -0,0 +1,209 @@
+# 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.
+
+import requests
+from mapproxy.test.http import (
+    MockServ, RequestsMissmatchError, mock_httpd,
+    basic_auth_value,
+)
+
+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 RequestsMissmatchError 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 RequestsMissmatchError 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 RequestsMissmatchError as ex:
+            assert 'requests missmatch:\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 RequestsMissmatchError 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')
diff --git a/mapproxy/test/unit/__init__.py b/mapproxy/test/unit/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/mapproxy/test/unit/epsg b/mapproxy/test/unit/epsg
new file mode 100644
index 0000000..b00a908
--- /dev/null
+++ b/mapproxy/test/unit/epsg
@@ -0,0 +1,2 @@
+# test srs
+<1234> +proj=longlat +ellps=bessel +no_defs <>
diff --git a/mapproxy/test/unit/polygons/polygons.dbf b/mapproxy/test/unit/polygons/polygons.dbf
new file mode 100644
index 0000000..34ad153
Binary files /dev/null and b/mapproxy/test/unit/polygons/polygons.dbf differ
diff --git a/mapproxy/test/unit/polygons/polygons.shp b/mapproxy/test/unit/polygons/polygons.shp
new file mode 100644
index 0000000..6d80d56
Binary files /dev/null and b/mapproxy/test/unit/polygons/polygons.shp differ
diff --git a/mapproxy/test/unit/polygons/polygons.shx b/mapproxy/test/unit/polygons/polygons.shx
new file mode 100644
index 0000000..9a5c61c
Binary files /dev/null and b/mapproxy/test/unit/polygons/polygons.shx differ
diff --git a/mapproxy/test/unit/test_async.py b/mapproxy/test/unit/test_async.py
new file mode 100644
index 0000000..a9f7053
--- /dev/null
+++ b/mapproxy/test/unit/test_async.py
@@ -0,0 +1,340 @@
+# 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 __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.2
+
+        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.1
+
+        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'
+
diff --git a/mapproxy/test/unit/test_auth.py b/mapproxy/test/unit/test_auth.py
new file mode 100644
index 0000000..f817c9a
--- /dev/null
+++ b/mapproxy/test/unit/test_auth.py
@@ -0,0 +1,400 @@
+from 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)
diff --git a/mapproxy/test/unit/test_cache.py b/mapproxy/test/unit/test_cache.py
new file mode 100644
index 0000000..8d8465e
--- /dev/null
+++ b/mapproxy/test/unit/test_cache.py
@@ -0,0 +1,876 @@
+# 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 with_statement
+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 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
+
+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.low.transparent = False #TODO
+        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.requested
\ No newline at end of file
diff --git a/mapproxy/test/unit/test_cache_couchdb.py b/mapproxy/test/unit/test_cache_couchdb.py
new file mode 100644
index 0000000..7776b7a
--- /dev/null
+++ b/mapproxy/test/unit/test_cache_couchdb.py
@@ -0,0 +1,117 @@
+# 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 __future__ import with_statement
+
+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']
\ No newline at end of file
diff --git a/mapproxy/test/unit/test_cache_riak.py b/mapproxy/test/unit/test_cache_riak.py
new file mode 100644
index 0000000..eff1b32
--- /dev/null
+++ b/mapproxy/test/unit/test_cache_riak.py
@@ -0,0 +1,71 @@
+# 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 with_statement
+
+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'
\ No newline at end of file
diff --git a/mapproxy/test/unit/test_cache_tile.py b/mapproxy/test/unit/test_cache_tile.py
new file mode 100644
index 0000000..de3e8f4
--- /dev/null
+++ b/mapproxy/test/unit/test_cache_tile.py
@@ -0,0 +1,339 @@
+# This file is part of the MapProxy project.
+# Copyright (C) 2011-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 with_statement
+
+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.cache.base import CacheBackendError
+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_, assert_raises
+
+tile_image = create_tmp_image_buf((256, 256), color='blue')
+tile_image2 = create_tmp_image_buf((256, 256), color='red')
+
+def timestamp_is_now(timestamp, delta=5):
+    return abs(timestamp - time.time()) <= delta
+
+class TileCacheTestBase(object):
+    always_loads_metadata = 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=(0, 0, 4)):
+        return Tile(coord,
+            ImageSource(tile_image,
+                image_opts=ImageOptions(format='image/png')))
+
+    def create_another_tile(self, coord=(0, 0, 4)):
+        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((0, 0, 4)))
+
+    def test_is_cached_hit(self):
+        tile = self.create_tile()
+        self.create_cached_tile(tile)
+        assert self.cache.is_cached(Tile((0, 0, 4)))
+
+    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((0, 0, 4))
+        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((0, 0, 4))
+        assert self.cache.load_tile(tile) == True
+        assert not tile.is_missing()
+
+    def test_store_tiles(self):
+        tiles = [self.create_tile((x, 0, 4)) for x in range(4)]
+        tiles[0].stored = True
+        self.cache.store_tiles(tiles)
+
+        tiles = [Tile((x, 0, 4)) 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:
+            assert timestamp_is_now(tile.timestamp, delta=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((0, 0, 4), ImageSource(BytesIO(b'foo')))
+        tile.stored = True
+        self.cache.store_tile(tile)
+
+        assert self.cache.is_cached(tile)
+
+        tile = Tile((0, 0, 4))
+        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)))
+
+    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')
+
+
+class TestMBTileCache(TileCacheTestBase):
+    def setup(self):
+        TileCacheTestBase.setup(self)
+        self.cache = MBTilesCache(os.path.join(self.cache_dir, 'tmp.mbtiles'))
+
+    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_1001_tiles(self):
+        assert_raises(CacheBackendError, self.cache.load_tiles, [Tile((19, 1, 1))] * 1001)
+
+    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)))
diff --git a/mapproxy/test/unit/test_client.py b/mapproxy/test/unit/test_client.py
new file mode 100644
index 0000000..8ed0939
--- /dev/null
+++ b/mapproxy/test/unit/test_client.py
@@ -0,0 +1,365 @@
+# 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 with_statement
+
+import os
+import time
+import sys
+
+from mapproxy.client.http import HTTPClient, HTTPClientError
+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_no_ssl_module_error(self):
+        from mapproxy.client import http
+        old_ssl = http.ssl
+        try:
+            http.ssl = None
+            try:
+                self.client = HTTPClient('https://www.google.com/')
+            except ImportError:
+                pass
+            else:
+                assert False, 'no ImportError for missing ssl module'
+        finally:
+            http.ssl = old_ssl
+
+    @attr('online')
+    def test_https_no_ssl_module_insecure(self):
+        from mapproxy.client import http
+        old_ssl = http.ssl
+        try:
+            http.ssl = None
+            self.client = HTTPClient('https://www.google.com/', insecure=True)
+            self.client.open('https://www.google.com/')
+        finally:
+            http.ssl = old_ssl
+
+    @attr('online')
+    def test_https_valid_cert(self):
+        try:
+            import ssl; ssl
+        except ImportError:
+            raise SkipTest()
+
+        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_invalid_cert(self):
+        try:
+            import ssl; ssl
+        except ImportError:
+            raise SkipTest()
+
+        with TempFile() as tmp:
+            self.client = HTTPClient('https://www.google.com/', ssl_ca_certs=tmp)
+            try:
+                self.client.open('https://www.google.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.5) or True},
+                    {'body': b'nothing'})
+
+        import mapproxy.client.http
+
+        old_timeout = mapproxy.client.http._max_set_timeout
+        mapproxy.client.http._max_set_timeout = None
+
+        client1 = HTTPClient(timeout=0.1)
+        client2 = HTTPClient(timeout=0.2)
+        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
+
+        if sys.version_info >= (2, 6):
+            # check individual timeouts
+            assert 0.1 <= duration1 < 0.2
+            assert 0.2 <= duration2 < 0.3
+        else:
+            # use max timeout in Python 2.5
+            assert 0.2 <= duration1 < 0.3
+            assert 0.2 <= duration2 < 0.3
+
+        mapproxy.client.http._max_set_timeout = old_timeout
+
+# Equifax Secure Certificate Authority
+# Expires: 2018-08-22
+GOOGLE_ROOT_CERT = b"""
+-----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), (256, 256), '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&HEIGHT=512&SRS=EPSG%3A25832&info_format=text/plain'
+                           '&query_layers=foo'
+                           '&VERSION=1.1.1&WIDTH=512&STYLES=&x=259&y=255'
+                           '&BBOX=428333.552496,5538630.70275,500000.0,5650300.78652')
+
+    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), (256, 256), '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&HEIGHT=512&SRS=EPSG%3A25832&info_format=text/plain'
+                           '&query_layers=foo'
+                           '&VERSION=1.1.1&WIDTH=512&STYLES=&x=259&y=255'
+                           '&BBOX=428333.552496,5538630.70275,500000.0,5650300.78652')
+
+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
diff --git a/mapproxy/test/unit/test_client_cgi.py b/mapproxy/test/unit/test_client_cgi.py
new file mode 100644
index 0000000..878fe2c
--- /dev/null
+++ b/mapproxy/test/unit/test_client_cgi.py
@@ -0,0 +1,133 @@
+# 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 __future__ import with_statement
+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')
+
diff --git a/mapproxy/test/unit/test_collections.py b/mapproxy/test/unit/test_collections.py
new file mode 100644
index 0000000..59d71d5
--- /dev/null
+++ b/mapproxy/test/unit/test_collections.py
@@ -0,0 +1,115 @@
+# 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 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'
\ No newline at end of file
diff --git a/mapproxy/test/unit/test_concat_legends.py b/mapproxy/test/unit/test_concat_legends.py
new file mode 100644
index 0000000..150d8b1
--- /dev/null
+++ b/mapproxy/test/unit/test_concat_legends.py
@@ -0,0 +1,37 @@
+# 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 with_statement
+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())
diff --git a/mapproxy/test/unit/test_conf_loader.py b/mapproxy/test/unit/test_conf_loader.py
new file mode 100644
index 0000000..b3ae990
--- /dev/null
+++ b/mapproxy/test/unit/test_conf_loader.py
@@ -0,0 +1,891 @@
+# -:- 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 division, with_statement
+import yaml
+import time
+from mapproxy.srs import SRS
+from mapproxy.config.loader import (
+    ProxyConfiguration,
+    load_configuration,
+    merge_dict,
+    ConfigurationError,
+)
+from mapproxy.cache.tile import TileManager
+from mapproxy.test.helper import TempFile
+from mapproxy.test.unit.test_grid import assert_almost_equal_bbox
+from nose.tools import eq_, assert_raises
+from nose.plugins.skip import SkipTest
+
+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('''
+            layers:
+              title: Root Layer
+              layers:
+                - name: one
+                  title: Layer One
+        ''')
+        conf = ProxyConfiguration(conf)
+        try:
+            conf.wms_root_layer.wms_layer()
+        except ValueError:
+            pass
+        else:
+            assert False, 'expected ValueError'
+
+
+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')
+
+
+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')
+
diff --git a/mapproxy/test/unit/test_conf_validator.py b/mapproxy/test/unit/test_conf_validator.py
new file mode 100644
index 0000000..21ecb23
--- /dev/null
+++ b/mapproxy/test/unit/test_conf_validator.py
@@ -0,0 +1,331 @@
+# This file is part of the MapProxy project.
+# Copyright (C) 2015 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
+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_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_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, [])
diff --git a/mapproxy/test/unit/test_config.py b/mapproxy/test/unit/test_config.py
new file mode 100644
index 0000000..bd0e6e1
--- /dev/null
+++ b/mapproxy/test/unit/test_config.py
@@ -0,0 +1,118 @@
+# 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 with_statement
+
+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
+
+
diff --git a/mapproxy/test/unit/test_decorate_img.py b/mapproxy/test/unit/test_decorate_img.py
new file mode 100644
index 0000000..4ab52f8
--- /dev/null
+++ b/mapproxy/test/unit/test_decorate_img.py
@@ -0,0 +1,171 @@
+from 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
+        )
diff --git a/mapproxy/test/unit/test_exceptions.py b/mapproxy/test/unit/test_exceptions.py
new file mode 100644
index 0000000..92ee06b
--- /dev/null
+++ b/mapproxy/test/unit/test_exceptions.py
@@ -0,0 +1,255 @@
+# 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 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"""
+<?xml version="1.0"?>
+<!DOCTYPE ServiceExceptionReport SYSTEM "http://schemas.opengis.net/wms/1.1.1/exception_1_1_1.dtd">
+<ServiceExceptionReport version="1.1.1">
+    <ServiceException>the exception message</ServiceException>
+</ServiceExceptionReport>
+"""
+        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"""
+<?xml version="1.0"?>
+<!DOCTYPE ServiceExceptionReport SYSTEM "http://schemas.opengis.net/wms/1.1.1/exception_1_1_1.dtd">
+<ServiceExceptionReport version="1.1.1">
+    <ServiceException code="InvalidFormat">the exception message</ServiceException>
+</ServiceExceptionReport>
+"""
+        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"""
+<?xml version="1.0"?>
+<!DOCTYPE ServiceExceptionReport SYSTEM "http://schemas.opengis.net/wms/1.1.0/exception_1_1_0.dtd">
+<ServiceExceptionReport version="1.1.0">
+    <ServiceException>the exception message</ServiceException>
+</ServiceExceptionReport>
+"""
+        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"""
+<?xml version="1.0"?>
+<!DOCTYPE ServiceExceptionReport SYSTEM "http://schemas.opengis.net/wms/1.1.0/exception_1_1_0.dtd">
+<ServiceExceptionReport version="1.1.0">
+    <ServiceException code="InvalidFormat">the exception message</ServiceException>
+</ServiceExceptionReport>
+"""
+        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"""
+<?xml version="1.0"?>
+<ServiceExceptionReport version="1.3.0"
+  xmlns="http://www.opengis.net/ogc"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://www.opengis.net/ogc
+http://schemas.opengis.net/wms/1.3.0/exceptions_1_3_0.xsd">
+    <ServiceException>the exception message</ServiceException>
+</ServiceExceptionReport>
+"""
+        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"""
+<?xml version="1.0"?>
+<ServiceExceptionReport version="1.3.0"
+  xmlns="http://www.opengis.net/ogc"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://www.opengis.net/ogc
+http://schemas.opengis.net/wms/1.3.0/exceptions_1_3_0.xsd">
+    <ServiceException code="InvalidFormat">the exception message</ServiceException>
+</ServiceExceptionReport>
+"""
+        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"""
+<?xml version="1.0"?>
+<WMTException version="1.0.0">
+the exception message
+</WMTException>
+"""
+        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)
diff --git a/mapproxy/test/unit/test_featureinfo.py b/mapproxy/test/unit/test_featureinfo.py
new file mode 100644
index 0000000..1a441da
--- /dev/null
+++ b/mapproxy/test/unit/test_featureinfo.py
@@ -0,0 +1,179 @@
+# 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 __future__ import with_statement
+
+import os
+import tempfile
+
+from lxml import etree, html
+from nose.tools import eq_
+
+from mapproxy.featureinfo import (combined_inputs, XSLTransformer,
+    XMLFeatureInfoDoc, HTMLFeatureInfoDoc)
+from mapproxy.test.helper import strip_whitespace
+
+def test_combined_inputs():
+    foo = '<a><b>foo</b></a>'
+    bar = '<a><b>bar</b></a>'
+
+    result = combined_inputs([foo, bar])
+    result = etree.tostring(result)
+    eq_(result, b'<a><b>foo</b><b>bar</b></a>')
+
+
+class TestXSLTransformer(object):
+    def setup(self):
+        fd_, self.xsl_script = tempfile.mkstemp('.xsl')
+        xsl = b"""
+        <xsl:stylesheet version="1.0"
+         xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+         <xsl:template match="/">
+            <root>
+                <xsl:apply-templates select='/a/b'/>
+            </root>
+         </xsl:template>
+         <xsl:template match="/a/b">
+             <foo><xsl:value-of select="text()" /></foo>
+         </xsl:template>
+        </xsl:stylesheet>""".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('<a><b>Text</b></a>'))
+        eq_(strip_whitespace(doc.as_string()), b'<root><foo>Text</foo></root>')
+
+    def test_multiple(self):
+        t = XSLTransformer(self.xsl_script)
+        doc = t.transform(XMLFeatureInfoDoc.combine([
+            XMLFeatureInfoDoc(x) for x in
+                [b'<a><b>ab</b></a>',
+                 b'<a><b>ab1</b><b>ab2</b><b>ab3</b></a>',
+                 b'<a><b>ab1</b><c>ac</c><b>ab2</b></a>',
+            ]]))
+        eq_(strip_whitespace(doc.as_string()),
+            strip_whitespace(b'''
+            <root>
+              <foo>ab</foo>
+              <foo>ab1</foo><foo>ab2</foo><foo>ab3</foo>
+              <foo>ab1</foo><foo>ab2</foo>
+            </root>'''))
+        eq_(doc.info_type, 'xml')
+
+
+class TestXMLFeatureInfoDocs(object):
+    def test_as_string(self):
+        input_tree = etree.fromstring('<root></root>')
+        doc = XMLFeatureInfoDoc(input_tree)
+        eq_(strip_whitespace(doc.as_string()),
+            b'<root/>')
+
+    def test_as_etree(self):
+        doc = XMLFeatureInfoDoc('<root>hello</root>')
+        eq_(doc.as_etree().getroot().text, 'hello')
+
+    def test_combine(self):
+        docs = [
+            XMLFeatureInfoDoc('<root><a>foo</a></root>'),
+            XMLFeatureInfoDoc('<root><b>bar</b></root>'),
+            XMLFeatureInfoDoc('<other_root><a>baz</a></other_root>'),
+        ]
+        result = XMLFeatureInfoDoc.combine(docs)
+
+        eq_(strip_whitespace(result.as_string()),
+            strip_whitespace(b'<root><a>foo</a><b>bar</b><a>baz</a></root>'))
+        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'<root><a>foo</a></root>'),
+            XMLFeatureInfoDoc(b'<root><b>bar</b></root>'),
+            XMLFeatureInfoDoc(b'<other_root><a>baz</a></other_root>'),
+        ]
+        result = XMLFeatureInfoDoc.combine(docs)
+
+        eq_(b'<root><a>foo</a></root>\n<root><b>bar</b></root>\n<other_root><a>baz</a></other_root>',
+            result.as_string())
+        eq_(result.info_type, 'text')
+
+class TestHTMLFeatureInfoDocs(object):
+    def test_as_string(self):
+        input_tree = html.fromstring('<p>Foo')
+        doc = HTMLFeatureInfoDoc(input_tree)
+        assert b'<body><p>Foo</p></body>' in strip_whitespace(doc.as_string())
+
+    def test_as_etree(self):
+        doc = HTMLFeatureInfoDoc('<p>hello</p>')
+        eq_(doc.as_etree().find('body/p').text, 'hello')
+
+    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)
+        assert b'<title>Hello</title>' in result.as_string()
+        assert (b'<body><p>baz</p><p>baz2</p><p>foo</p><p>bar</p></body>' in
+            result.as_string())
+        eq_(result.info_type, 'html')
+
+    def test_combine_parts(self):
+        docs = [
+            HTMLFeatureInfoDoc('<p>foo</p>'),
+            HTMLFeatureInfoDoc('<body><p>bar</p></body>'),
+            HTMLFeatureInfoDoc('<html><head><title>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')
diff --git a/mapproxy/test/unit/test_file_lock_load.py b/mapproxy/test/unit/test_file_lock_load.py
new file mode 100644
index 0000000..6209261
--- /dev/null
+++ b/mapproxy/test/unit/test_file_lock_load.py
@@ -0,0 +1,42 @@
+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)
+
+
diff --git a/mapproxy/test/unit/test_geom.py b/mapproxy/test/unit/test_geom.py
new file mode 100644
index 0000000..3f39711
--- /dev/null
+++ b/mapproxy/test/unit/test_geom.py
@@ -0,0 +1,347 @@
+# 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, with_statement
+
+from mapproxy.srs import SRS, bbox_equals
+from mapproxy.util.geom import (
+    load_polygons,
+    transform_geometry,
+    geom_support,
+    bbox_polygon,
+    build_multipolygon,
+)
+from mapproxy.util.coverage import coverage, MultiCoverage
+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
+
+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(shapely.geos.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 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 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)
+
diff --git a/mapproxy/test/unit/test_grid.py b/mapproxy/test/unit/test_grid.py
new file mode 100644
index 0000000..4675f20
--- /dev/null
+++ b/mapproxy/test/unit/test_grid.py
@@ -0,0 +1,1228 @@
+# 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 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)
+
+
+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)
diff --git a/mapproxy/test/unit/test_image.py b/mapproxy/test/unit/test_image.py
new file mode 100644
index 0000000..69e5de9
--- /dev/null
+++ b/mapproxy/test/unit/test_image.py
@@ -0,0 +1,562 @@
+# -:- 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 with_statement
+
+import os
+from io import BytesIO
+from mapproxy.compat.image import Image, ImageDraw
+from mapproxy.image import ImageSource, ReadBufWrapper, is_single_color_image
+from mapproxy.image import peek_image_format
+from mapproxy.image.merge import merge_images
+from mapproxy.image import _make_transparent as make_transparent, SubImageSource, img_has_transparency, quantize
+from mapproxy.image.opts import ImageOptions
+from mapproxy.image.tile import TileMerger, TileSplitter
+from mapproxy.image.transform import ImageTransformer
+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_
+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_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)
+
+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_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))
+
+
+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, mesh_div=4):
+        transformer = ImageTransformer(self.src_srs, self.dst_srs, mesh_div=mesh_div)
+        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
+        assert result.size == (100, 150)
+
+    def _test_compare_mesh_div(self):
+        """
+        Create transformations with different div values.
+        """
+        for div in [1, 2, 4, 6, 8, 12, 16]:
+            transformer = ImageTransformer(self.src_srs, self.dst_srs, mesh_div=div)
+            result = transformer.transform(self.src_img, self.src_bbox,
+                                           self.dst_size, self.dst_bbox)
+            result.as_image().save('/tmp/transform-%d.png' % (div,))
+
+
+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)
diff --git a/mapproxy/test/unit/test_image_mask.py b/mapproxy/test/unit/test_image_mask.py
new file mode 100644
index 0000000..eb8a338
--- /dev/null
+++ b/mapproxy/test/unit/test_image_mask.py
@@ -0,0 +1,89 @@
+# 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
+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.util.coverage import load_limited_to
+from mapproxy.test.image import assert_img_colors_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*61 - 20*21 = 3240
+        assert_img_colors_eq(result.as_image().getcolors(),
+            [(10000-3240, (255, 255, 255, 0)), (3240, (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))])
diff --git a/mapproxy/test/unit/test_image_messages.py b/mapproxy/test/unit/test_image_messages.py
new file mode 100644
index 0000000..1475d7e
--- /dev/null
+++ b/mapproxy/test/unit/test_image_messages.py
@@ -0,0 +1,182 @@
+# -:- 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))
\ No newline at end of file
diff --git a/mapproxy/test/unit/test_image_options.py b/mapproxy/test/unit/test_image_options.py
new file mode 100644
index 0000000..396c3de
--- /dev/null
+++ b/mapproxy/test/unit/test_image_options.py
@@ -0,0 +1,160 @@
+# 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')
+
diff --git a/mapproxy/test/unit/test_multiapp.py b/mapproxy/test/unit/test_multiapp.py
new file mode 100644
index 0000000..e99c6e7
--- /dev/null
+++ b/mapproxy/test/unit/test_multiapp.py
@@ -0,0 +1,172 @@
+# -:- 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.
+
+from __future__ import with_statement
+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')
+
diff --git a/mapproxy/test/unit/test_ogr_reader.py b/mapproxy/test/unit/test_ogr_reader.py
new file mode 100644
index 0000000..739d910
--- /dev/null
+++ b/mapproxy/test/unit/test_ogr_reader.py
@@ -0,0 +1,41 @@
+# 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)
diff --git a/mapproxy/test/unit/test_request.py b/mapproxy/test/unit/test_request.py
new file mode 100644
index 0000000..da40da2
--- /dev/null
+++ b/mapproxy/test/unit/test_request.py
@@ -0,0 +1,536 @@
+# -:- 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.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 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
+
diff --git a/mapproxy/test/unit/test_request_wmts.py b/mapproxy/test/unit/test_request_wmts.py
new file mode 100644
index 0000000..4ab8005
--- /dev/null
+++ b/mapproxy/test/unit/test_request_wmts.py
@@ -0,0 +1,75 @@
+# 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'
+
+ at 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']
diff --git a/mapproxy/test/unit/test_response.py b/mapproxy/test/unit/test_response.py
new file mode 100644
index 0000000..2820aa2
--- /dev/null
+++ b/mapproxy/test/unit/test_response.py
@@ -0,0 +1,75 @@
+# 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
diff --git a/mapproxy/test/unit/test_seed.py b/mapproxy/test/unit/test_seed.py
new file mode 100644
index 0000000..818367a
--- /dev/null
+++ b/mapproxy/test/unit/test_seed.py
@@ -0,0 +1,265 @@
+# 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 with_statement, 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), (0, 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
+        )
diff --git a/mapproxy/test/unit/test_seed_cachelock.py b/mapproxy/test/unit/test_seed_cachelock.py
new file mode 100644
index 0000000..c8ff531
--- /dev/null
+++ b/mapproxy/test/unit/test_seed_cachelock.py
@@ -0,0 +1,89 @@
+# 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 __future__ import with_statement
+
+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()
\ No newline at end of file
diff --git a/mapproxy/test/unit/test_srs.py b/mapproxy/test/unit/test_srs.py
new file mode 100644
index 0000000..fcaa53e
--- /dev/null
+++ b/mapproxy/test/unit/test_srs.py
@@ -0,0 +1,94 @@
+# 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
+        
\ No newline at end of file
diff --git a/mapproxy/test/unit/test_tiled_source.py b/mapproxy/test/unit/test_tiled_source.py
new file mode 100644
index 0000000..3e32aef
--- /dev/null
+++ b/mapproxy/test/unit/test_tiled_source.py
@@ -0,0 +1,74 @@
+# 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 __future__ import with_statement
+
+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))])
diff --git a/mapproxy/test/unit/test_tilefilter.py b/mapproxy/test/unit/test_tilefilter.py
new file mode 100644
index 0000000..ec053ed
--- /dev/null
+++ b/mapproxy/test/unit/test_tilefilter.py
@@ -0,0 +1,30 @@
+# 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')
diff --git a/mapproxy/test/unit/test_times.py b/mapproxy/test/unit/test_times.py
new file mode 100644
index 0000000..dfdfe7e
--- /dev/null
+++ b/mapproxy/test/unit/test_times.py
@@ -0,0 +1,13 @@
+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'
diff --git a/mapproxy/test/unit/test_timeutils.py b/mapproxy/test/unit/test_timeutils.py
new file mode 100644
index 0000000..b973594
--- /dev/null
+++ b/mapproxy/test/unit/test_timeutils.py
@@ -0,0 +1,50 @@
+# 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)
diff --git a/mapproxy/test/unit/test_util_conf_utils.py b/mapproxy/test/unit/test_util_conf_utils.py
new file mode 100644
index 0000000..4779a51
--- /dev/null
+++ b/mapproxy/test/unit/test_util_conf_utils.py
@@ -0,0 +1,69 @@
+# -:- 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})
diff --git a/mapproxy/test/unit/test_utils.py b/mapproxy/test/unit/test_utils.py
new file mode 100644
index 0000000..29c4511
--- /dev/null
+++ b/mapproxy/test/unit/test_utils.py
@@ -0,0 +1,445 @@
+# 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.
+
+from __future__ import with_statement
+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 = []
+        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)
+
+        for filename in files:
+            assert os.path.exists(filename), filename
+        cleanup_directory(self.tmpdir, timestamp_before())
+        for filename in files[::2]:
+            assert not os.path.exists(filename), filename
+            assert not os.path.exists(os.path.dirname(filename)), filename
+        for filename in files[1::2]:
+            assert os.path.exists(filename), filename
+
+def write_atomic_data(xxx_todo_changeme):
+    (i, filename) = xxx_todo_changeme
+    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'
\ No newline at end of file
diff --git a/mapproxy/test/unit/test_wms_capabilities.py b/mapproxy/test/unit/test_wms_capabilities.py
new file mode 100644
index 0000000..ddcbbd6
--- /dev/null
+++ b/mapproxy/test/unit/test_wms_capabilities.py
@@ -0,0 +1,42 @@
+# 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)),}
+        )
diff --git a/mapproxy/test/unit/test_wms_layer.py b/mapproxy/test/unit/test_wms_layer.py
new file mode 100644
index 0000000..bd60be9
--- /dev/null
+++ b/mapproxy/test/unit/test_wms_layer.py
@@ -0,0 +1,78 @@
+# 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 with_statement, division
+
+from mapproxy.layer import MapQuery
+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'])
+
diff --git a/mapproxy/test/unit/test_yaml.py b/mapproxy/test/unit/test_yaml.py
new file mode 100644
index 0000000..682af79
--- /dev/null
+++ b/mapproxy/test/unit/test_yaml.py
@@ -0,0 +1,61 @@
+# 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'
diff --git a/mapproxy/tilefilter.py b/mapproxy/tilefilter.py
new file mode 100644
index 0000000..5549498
--- /dev/null
+++ b/mapproxy/tilefilter.py
@@ -0,0 +1,59 @@
+# 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
+
diff --git a/mapproxy/util/__init__.py b/mapproxy/util/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/mapproxy/util/async.py b/mapproxy/util/async.py
new file mode 100644
index 0000000..217e04c
--- /dev/null
+++ b/mapproxy/util/async.py
@@ -0,0 +1,344 @@
+# 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 __future__ import with_statement
+
+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(*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
diff --git a/mapproxy/util/collections.py b/mapproxy/util/collections.py
new file mode 100644
index 0000000..4d3cf26
--- /dev/null
+++ b/mapproxy/util/collections.py
@@ -0,0 +1,132 @@
+# 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]
diff --git a/mapproxy/util/coverage.py b/mapproxy/util/coverage.py
new file mode 100644
index 0000000..4ad2a17
--- /dev/null
+++ b/mapproxy/util/coverage.py
@@ -0,0 +1,258 @@
+# 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 with_statement
+
+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,
+)
+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):
+    if isinstance(geom, (list, tuple)):
+        return BBOXCoverage(geom, srs)
+    else:
+        return GeomCoverage(geom, srs)
+
+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):
+    clip = False
+    def __init__(self, bbox, srs):
+        self.bbox = bbox
+        self.srs = srs
+        self.geom = None
+
+    @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)
+
+    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)
+
+    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)
+
+    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)
+
+    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)
\ No newline at end of file
diff --git a/mapproxy/util/ext/__init__.py b/mapproxy/util/ext/__init__.py
new file mode 100644
index 0000000..3809c9f
--- /dev/null
+++ b/mapproxy/util/ext/__init__.py
@@ -0,0 +1,14 @@
+# 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.
diff --git a/mapproxy/util/ext/dictspec/__init__.py b/mapproxy/util/ext/dictspec/__init__.py
new file mode 100644
index 0000000..5e3048b
--- /dev/null
+++ b/mapproxy/util/ext/dictspec/__init__.py
@@ -0,0 +1 @@
+__version__ = '0.1'
\ No newline at end of file
diff --git a/mapproxy/util/ext/dictspec/spec.py b/mapproxy/util/ext/dictspec/spec.py
new file mode 100644
index 0000000..94fc693
--- /dev/null
+++ b/mapproxy/util/ext/dictspec/spec.py
@@ -0,0 +1,120 @@
+# Copyright (c) 2011, Oliver Tonnhofer <olt at 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]
diff --git a/mapproxy/util/ext/dictspec/test/__init__.py b/mapproxy/util/ext/dictspec/test/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/mapproxy/util/ext/dictspec/test/test_validator.py b/mapproxy/util/ext/dictspec/test/test_validator.py
new file mode 100644
index 0000000..54d65c6
--- /dev/null
+++ b/mapproxy/util/ext/dictspec/test/test_validator.py
@@ -0,0 +1,274 @@
+# -:- encoding: utf8 -:-
+# Copyright (c) 2011, Oliver Tonnhofer <olt at 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()
+
diff --git a/mapproxy/util/ext/dictspec/validator.py b/mapproxy/util/ext/dictspec/validator.py
new file mode 100644
index 0000000..69c9678
--- /dev/null
+++ b/mapproxy/util/ext/dictspec/validator.py
@@ -0,0 +1,190 @@
+# Copyright (c) 2011, Oliver Tonnhofer <olt at 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 with_statement
+
+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)
+
diff --git a/mapproxy/util/ext/local.py b/mapproxy/util/ext/local.py
new file mode 100644
index 0000000..9f70b08
--- /dev/null
+++ b/mapproxy/util/ext/local.py
@@ -0,0 +1,196 @@
+# -*- 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
+
diff --git a/mapproxy/util/ext/lockfile.py b/mapproxy/util/ext/lockfile.py
new file mode 100644
index 0000000..39cc2f9
--- /dev/null
+++ b/mapproxy/util/ext/lockfile.py
@@ -0,0 +1,138 @@
+##############################################################################
+#
+# 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 at 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
diff --git a/mapproxy/util/ext/odict.py b/mapproxy/util/ext/odict.py
new file mode 100644
index 0000000..c1e1f97
--- /dev/null
+++ b/mapproxy/util/ext/odict.py
@@ -0,0 +1,330 @@
+# -*- 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)
+        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()
\ No newline at end of file
diff --git a/mapproxy/util/ext/serving.py b/mapproxy/util/ext/serving.py
new file mode 100644
index 0000000..4fd73d7
--- /dev/null
+++ b/mapproxy/util/ext/serving.py
@@ -0,0 +1,773 @@
+# -*- coding: utf-8 -*-
+"""
+    werkzeug.serving
+    ~~~~~~~~~~~~~~~~
+
+    There are many ways to serve a WSGI application.  While you're developing
+    it you usually don't want a full blown webserver like Apache but a simple
+    standalone one.  From Python 2.5 onwards there is the `wsgiref`_ server in
+    the standard library.  If you're using older versions of Python you can
+    download the package from the cheeseshop.
+
+    However there are some caveats. Sourcecode won't reload itself when
+    changed and each time you kill the server using ``^C`` you get an
+    `KeyboardInterrupt` error.  While the latter is easy to solve the first
+    one can be a pain in the ass in some situations.
+
+    The easiest way is creating a small ``start-myproject.py`` that runs the
+    application::
+
+        #!/usr/bin/env python
+        # -*- coding: utf-8 -*-
+        from myproject import make_app
+        from werkzeug.serving import run_simple
+
+        app = make_app(...)
+        run_simple('localhost', 8080, app, use_reloader=True)
+
+    You can also pass it a `extra_files` keyword argument with a list of
+    additional files (like configuration files) you want to observe.
+
+    For bigger applications you should consider using `werkzeug.script`
+    instead of a simple start file.
+
+
+    :copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
+    :license: BSD, see LICENSE for more details.
+"""
+from __future__ import with_statement
+
+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, ForkingMixIn
+    from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
+except ImportError:
+    from socketserver import ThreadingMixIn, ForkingMixIn
+    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
+
+def wsgi_encoding_dance(s, charset='utf-8', errors='replace'):
+    if isinstance(s, bytes):
+        return s
+    return s.encode(charset, 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 = self.server.ssl_context is None and 'http' or 'https'
+        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:
+            if self.server.ssl_context is None or not is_ssl_error():
+                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 generate_adhoc_ssl_pair(cn=None):
+    from random import random
+    from OpenSSL import crypto
+
+    # pretty damn sure that this is not actually accepted by anyone
+    if cn is None:
+        cn = '*'
+
+    cert = crypto.X509()
+    cert.set_serial_number(int(random() * sys.maxsize))
+    cert.gmtime_adj_notBefore(0)
+    cert.gmtime_adj_notAfter(60 * 60 * 24 * 365)
+
+    subject = cert.get_subject()
+    subject.CN = cn
+    subject.O = 'Dummy Certificate'
+
+    issuer = cert.get_issuer()
+    issuer.CN = 'Untrusted Authority'
+    issuer.O = 'Self-Signed'
+
+    pkey = crypto.PKey()
+    pkey.generate_key(crypto.TYPE_RSA, 768)
+    cert.set_pubkey(pkey)
+    cert.sign(pkey, 'md5')
+
+    return cert, pkey
+
+
+def make_ssl_devcert(base_path, host=None, cn=None):
+    """Creates an SSL key for development.  This should be used instead of
+    the ``'adhoc'`` key which generates a new cert on each server start.
+    It accepts a path for where it should store the key and cert and
+    either a host or CN.  If a host is given it will use the CN
+    ``*.host/CN=host``.
+
+    For more information see :func:`run_simple`.
+
+    .. versionadded:: 0.9
+
+    :param base_path: the path to the certificate and key.  The extension
+                      ``.crt`` is added for the certificate, ``.key`` is
+                      added for the key.
+    :param host: the name of the host.  This can be used as an alternative
+                 for the `cn`.
+    :param cn: the `CN` to use.
+    """
+    from OpenSSL import crypto
+    if host is not None:
+        cn = '*.%s/CN=%s' % (host, host)
+    cert, pkey = generate_adhoc_ssl_pair(cn=cn)
+
+    cert_file = base_path + '.crt'
+    pkey_file = base_path + '.key'
+
+    with open(cert_file, 'wb') as f:
+        f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
+    with open(pkey_file, 'wb') as f:
+        f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
+
+    return cert_file, pkey_file
+
+
+def generate_adhoc_ssl_context():
+    """Generates an adhoc SSL context for the development server."""
+    from OpenSSL import SSL
+    cert, pkey = generate_adhoc_ssl_pair()
+    ctx = SSL.Context(SSL.SSLv23_METHOD)
+    ctx.use_privatekey(pkey)
+    ctx.use_certificate(cert)
+    return ctx
+
+
+def load_ssl_context(cert_file, pkey_file):
+    """Loads an SSL context from a certificate and private key file."""
+    from OpenSSL import SSL
+    ctx = SSL.Context(SSL.SSLv23_METHOD)
+    ctx.use_certificate_file(cert_file)
+    ctx.use_privatekey_file(pkey_file)
+    return ctx
+
+
+def is_ssl_error(error=None):
+    """Checks if the given error (or the current one) is an SSL error."""
+    if error is None:
+        error = sys.exc_info()[1]
+    from OpenSSL import SSL
+    return isinstance(error, SSL.Error)
+
+
+class _SSLConnectionFix(object):
+    """Wrapper around SSL connection to provide a working makefile()."""
+
+    def __init__(self, con):
+        self._con = con
+
+    def makefile(self, mode, bufsize):
+        return socket._fileobject(self._con, mode, bufsize)
+
+    def __getattr__(self, attrib):
+        return getattr(self._con, attrib)
+
+    def shutdown(self, arg=None):
+        try:
+            self._con.shutdown()
+        except Exception:
+            pass
+
+
+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, ssl_context=None):
+        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
+
+        if ssl_context is not None:
+            try:
+                from OpenSSL import tsafe
+            except ImportError:
+                raise TypeError('SSL is not available if the OpenSSL '
+                                'library is not installed.')
+            if isinstance(ssl_context, tuple):
+                ssl_context = load_ssl_context(*ssl_context)
+            if ssl_context == 'adhoc':
+                ssl_context = generate_adhoc_ssl_context()
+            self.socket = tsafe.Connection(ssl_context, self.socket)
+            self.ssl_context = ssl_context
+        else:
+            self.ssl_context = None
+
+    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()
+        if self.ssl_context is not None:
+            con = _SSLConnectionFix(con)
+        return con, info
+
+
+class ThreadedWSGIServer(ThreadingMixIn, BaseWSGIServer):
+    """A WSGI server that does threading."""
+    multithread = True
+
+
+class ForkingWSGIServer(ForkingMixIn, BaseWSGIServer):
+    """A WSGI server that does forking."""
+    multiprocess = True
+
+    def __init__(self, host, port, app, processes=40, handler=None,
+                 passthrough_errors=False, ssl_context=None):
+        BaseWSGIServer.__init__(self, host, port, app, handler,
+                                passthrough_errors, ssl_context)
+        self.max_children = processes
+
+
+def make_server(host, port, app=None, threaded=False, processes=1,
+                request_handler=None, passthrough_errors=False,
+                ssl_context=None):
+    """Create a new server instance that is either threaded, or forks
+    or just processes one request after another.
+    """
+    if threaded and processes > 1:
+        raise ValueError("cannot have a multithreaded and "
+                         "multi process server.")
+    elif threaded:
+        return ThreadedWSGIServer(host, port, app, request_handler,
+                                  passthrough_errors, ssl_context)
+    elif processes > 1:
+        return ForkingWSGIServer(host, port, app, processes, request_handler,
+                                 passthrough_errors, ssl_context)
+    else:
+        return BaseWSGIServer(host, port, app, request_handler,
+                              passthrough_errors, ssl_context)
+
+
+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)
+
+
+def _reloader_inotify(extra_files=None, interval=None):
+    # Mutated by inotify loop when changes occur.
+    changed = [False]
+
+    # Setup inotify watches
+    from pyinotify import WatchManager, Notifier
+
+    # this API changed at one point, support both
+    try:
+        from pyinotify import EventsCodes as ec
+        ec.IN_ATTRIB
+    except (ImportError, AttributeError):
+        import pyinotify as ec
+
+    wm = WatchManager()
+    mask = ec.IN_DELETE_SELF | ec.IN_MOVE_SELF | ec.IN_MODIFY | ec.IN_ATTRIB
+
+    def signal_changed(event):
+        if changed[0]:
+            return
+        _log('info', ' * Detected change in %r, reloading' % event.path)
+        changed[:] = [True]
+
+    for fname in extra_files or ():
+        wm.add_watch(fname, mask, signal_changed)
+
+    # ... And now we wait...
+    notif = Notifier(wm)
+    try:
+        while not changed[0]:
+            # always reiterate through sys.modules, adding them
+            for fname in _iter_module_files():
+                wm.add_watch(fname, mask, signal_changed)
+            notif.process_events()
+            if notif.check_events(timeout=interval):
+                notif.read_events()
+            # TODO Set timeout to something small and check parent liveliness
+    finally:
+        notif.stop()
+    sys.exit(3)
+
+
+# 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
+        # pip installs commands as .exe, but sys.argv[0]
+        # can miss the prefix. add .exe to avoid file-not-found
+        # in subprocess call
+        if os.name == 'nt' and '.' not in args[1]:
+            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, ssl_context=None):
+    """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.)
+    :param ssl_context: an SSL context for the connection. Either an OpenSSL
+                        context, a tuple in the form ``(cert_file, pkey_file)``,
+                        the string ``'adhoc'`` if the server should
+                        automatically create one, or `None` to disable SSL
+                        (which is the default).
+    """
+    if use_debugger:
+        from werkzeug.debug import DebuggedApplication
+        application = DebuggedApplication(application, use_evalex)
+    if static_files:
+        from werkzeug.wsgi import SharedDataMiddleware
+        application = SharedDataMiddleware(application, static_files)
+
+    def inner():
+        make_server(hostname, port, application, threaded,
+                    processes, request_handler,
+                    passthrough_errors, ssl_context).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 %s://%s:%d/ %s', ssl_context is None
+             and 'http' or 'https', 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()
+
+def main():
+    '''A simple command-line interface for :py:func:`run_simple`.'''
+
+    # in contrast to argparse, this works at least under Python < 2.7
+    import optparse
+    from werkzeug.utils import import_string
+
+    parser = optparse.OptionParser(usage='Usage: %prog [options] app_module:app_object')
+    parser.add_option('-b', '--bind', dest='address',
+                      help='The hostname:port the app should listen on.')
+    parser.add_option('-d', '--debug', dest='use_debugger',
+                      action='store_true', default=False,
+                      help='Use Werkzeug\'s debugger.')
+    parser.add_option('-r', '--reload', dest='use_reloader',
+                      action='store_true', default=False,
+                      help='Reload Python process if modules change.')
+    options, args = parser.parse_args()
+
+    hostname, port = None, None
+    if options.address:
+        address = options.address.split(':')
+        hostname = address[0]
+        if len(address) > 1:
+            port = address[1]
+
+    if len(args) != 1:
+        sys.stdout.write('No application supplied, or too much. See --help\n')
+        sys.exit(1)
+    app = import_string(args[0])
+
+    run_simple(
+        hostname=(hostname or '127.0.0.1'), port=int(port or 5000),
+        application=app, use_reloader=options.use_reloader,
+        use_debugger=options.use_debugger
+    )
+
+if __name__ == '__main__':
+    main()
diff --git a/mapproxy/util/ext/tempita/__init__.py b/mapproxy/util/ext/tempita/__init__.py
new file mode 100644
index 0000000..24b008d
--- /dev/null
+++ b/mapproxy/util/ext/tempita/__init__.py
@@ -0,0 +1,1172 @@
+"""
+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()
diff --git a/mapproxy/util/ext/tempita/_looper.py b/mapproxy/util/ext/tempita/_looper.py
new file mode 100644
index 0000000..d484ce1
--- /dev/null
+++ b/mapproxy/util/ext/tempita/_looper.py
@@ -0,0 +1,163 @@
+"""
+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]
diff --git a/mapproxy/util/ext/tempita/compat3.py b/mapproxy/util/ext/tempita/compat3.py
new file mode 100644
index 0000000..5e18fa0
--- /dev/null
+++ b/mapproxy/util/ext/tempita/compat3.py
@@ -0,0 +1,45 @@
+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
diff --git a/mapproxy/util/ext/wmsparse/__init__.py b/mapproxy/util/ext/wmsparse/__init__.py
new file mode 100644
index 0000000..d16294b
--- /dev/null
+++ b/mapproxy/util/ext/wmsparse/__init__.py
@@ -0,0 +1,3 @@
+from .parse import parse_capabilities
+
+__all__ = ['parse_capabilities']
\ No newline at end of file
diff --git a/mapproxy/util/ext/wmsparse/parse.py b/mapproxy/util/ext/wmsparse/parse.py
new file mode 100644
index 0000000..2b07666
--- /dev/null
+++ b/mapproxy/util/ext/wmsparse/parse.py
@@ -0,0 +1,305 @@
+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 not elem:
+            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)
\ No newline at end of file
diff --git a/mapproxy/util/ext/wmsparse/test/__init__.py b/mapproxy/util/ext/wmsparse/test/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/mapproxy/util/ext/wmsparse/test/test_parse.py b/mapproxy/util/ext/wmsparse/test/test_parse.py
new file mode 100644
index 0000000..66549e0
--- /dev/null
+++ b/mapproxy/util/ext/wmsparse/test/test_parse.py
@@ -0,0 +1,107 @@
+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'], '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/)')
+        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 at 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)
diff --git a/mapproxy/util/ext/wmsparse/test/test_util.py b/mapproxy/util/ext/wmsparse/test/test_util.py
new file mode 100644
index 0000000..e102d89
--- /dev/null
+++ b/mapproxy/util/ext/wmsparse/test/test_util.py
@@ -0,0 +1,16 @@
+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')
diff --git a/mapproxy/util/ext/wmsparse/test/wms-large-111.xml b/mapproxy/util/ext/wmsparse/test/wms-large-111.xml
new file mode 100644
index 0000000..b7e454d
--- /dev/null
+++ b/mapproxy/util/ext/wmsparse/test/wms-large-111.xml
@@ -0,0 +1,2114 @@
+<?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</Title>
+  <Abstract>Open Street Map</Abstract>
+        <KeywordList>
+          <Keyword>osm</Keyword>
+          <Keyword> OpenStreetMap</Keyword>
+        </KeywordList>
+  <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://example.org/service?SERVICE=WMS&"/>
+  <ContactInformation>
+    <ContactPersonPrimary>
+      <ContactPerson>Name</ContactPerson>
+      <ContactOrganization>Organization</ContactOrganization>
+    </ContactPersonPrimary>
+      <ContactPosition>Development</ContactPosition>
+    <ContactAddress>
+        <AddressType>postal</AddressType>
+        <Address>Fakestreet 23</Address>
+        <City>Somewhere</City>
+        <StateOrProvince></StateOrProvince>
+        <PostCode>12345</PostCode>
+        <Country>Germany</Country>
+    </ContactAddress>
+      <ContactVoiceTelephone></ContactVoiceTelephone>
+      <ContactFacsimileTelephone>0</ContactFacsimileTelephone>
+  <ContactElectronicMailAddress>info at example.org</ContactElectronicMailAddress>
+  </ContactInformation>
+  <Fees>none</Fees>
+  <AccessConstraints>none</AccessConstraints>
+</Service>
+
+<Capability>
+  <Request>
+    <GetCapabilities>
+      <Format>application/vnd.ogc.wms_xml</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://example.org/service?SERVICE=WMS&"/></Get>
+          <Post><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://example.org/service?SERVICE=WMS&"/></Post>
+        </HTTP>
+      </DCPType>
+    </GetCapabilities>
+    <GetMap>
+      <Format>image/gif</Format>
+      <Format>image/png</Format>
+      <Format>image/png; mode=24bit</Format>
+      <Format>image/jpeg</Format>
+      <Format>image/vnd.wap.wbmp</Format>
+      <Format>image/tiff</Format>
+      <Format>image/svg+xml</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://example.org/service?SERVICE=WMS&"/></Get>
+          <Post><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://example.org/service?SERVICE=WMS&"/></Post>
+        </HTTP>
+      </DCPType>
+    </GetMap>
+    <GetFeatureInfo>
+      <Format>text/plain</Format>
+      <Format>text/html</Format>
+      <Format>application/vnd.ogc.gml</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://example.org/service?SERVICE=WMS&"/></Get>
+          <Post><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://example.org/service?SERVICE=WMS&"/></Post>
+        </HTTP>
+      </DCPType>
+    </GetFeatureInfo>
+    <DescribeLayer>
+      <Format>text/xml</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://example.org/service?SERVICE=WMS&"/></Get>
+          <Post><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://example.org/service?SERVICE=WMS&"/></Post>
+        </HTTP>
+      </DCPType>
+    </DescribeLayer>
+    <GetLegendGraphic>
+      <Format>image/gif</Format>
+      <Format>image/png</Format>
+      <Format>image/png; mode=24bit</Format>
+      <Format>image/jpeg</Format>
+      <Format>image/vnd.wap.wbmp</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://example.org/service?SERVICE=WMS&"/></Get>
+          <Post><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://example.org/service?SERVICE=WMS&"/></Post>
+        </HTTP>
+      </DCPType>
+    </GetLegendGraphic>
+    <GetStyles>
+      <Format>text/xml</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://example.org/service?SERVICE=WMS&"/></Get>
+          <Post><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://example.org/service?SERVICE=WMS&"/></Post>
+        </HTTP>
+      </DCPType>
+    </GetStyles>
+  </Request>
+  <Exception>
+    <Format>application/vnd.ogc.se_xml</Format>
+    <Format>application/vnd.ogc.se_inimage</Format>
+    <Format>application/vnd.ogc.se_blank</Format>
+  </Exception>
+  <VendorSpecificCapabilities />
+  <UserDefinedSymbolization SupportSLD="1" UserLayer="0" UserStyle="1" RemoteWFS="0"/>
+  <Layer>
+    <Name>OSM</Name>
+    <Title>OSM</Title>
+    <SRS>EPSG:31467</SRS>
+    <SRS>EPSG:31466</SRS>
+    <SRS>EPSG:31468</SRS>
+    <SRS>EPSG:31469</SRS>
+    <SRS>EPSG:31492</SRS>
+    <SRS>EPSG:31493</SRS>
+    <SRS>EPSG:31494</SRS>
+    <SRS>EPSG:31495</SRS>
+    <SRS>EPSG:31462</SRS>
+    <SRS>EPSG:31463</SRS>
+    <SRS>EPSG:31464</SRS>
+    <SRS>EPSG:31465</SRS>
+    <SRS>EPSG:4326</SRS>
+    <SRS>EPSG:25832</SRS>
+    <SRS>EPSG:25833</SRS>
+    <SRS>EPSG:31257</SRS>
+    <SRS>EPSG:31258</SRS>
+    <SRS>EPSG:31259</SRS>
+    <SRS>EPSG:31284</SRS>
+    <SRS>EPSG:31285</SRS>
+    <SRS>EPSG:31286</SRS>
+    <SRS>EPSG:31287</SRS>
+    <SRS>EPSG:28992</SRS>
+    <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+    <BoundingBox SRS="EPSG:4326"
+                minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Grenzen</Name>
+        <Title>Europa</Title>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="66" height="32">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Grenzen&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Landwirtschaft</Name>
+        <Title>Landwirtschaft</Title>
+        <KeywordList>
+          <Keyword>Landwirtschaft</Keyword>
+          <Keyword> Bauernhof</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="110" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Landwirtschaft&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="99.7805696859274" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Industriegebiet</Name>
+        <Title>Industriegebiet</Title>
+        <KeywordList>
+          <Keyword>Industrie</Keyword>
+          <Keyword> Industriegebiet</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="110" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Industriegebiet&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="49.8902848429637" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Bauland</Name>
+        <Title>Bauland</Title>
+        <KeywordList>
+          <Keyword>Bauland</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="80" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Bauland&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="49.8902848429637" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Gruenflaeche</Name>
+        <Title>Gruenflaeche</Title>
+        <KeywordList>
+          <Keyword>Grünfläche</Keyword>
+          <Keyword> Land</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="105" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Gruenflaeche&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="49.8902848429637" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>unkultiviertes_Land</Name>
+        <Title>unkultiviertes_Land</Title>
+        <KeywordList>
+          <Keyword>unkultiviertes Land</Keyword>
+          <Keyword> Unterholz</Keyword>
+          <Keyword> Busch</Keyword>
+          <Keyword> Land</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="128" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=unkultiviertes_Land&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="99.7805696859274" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Park</Name>
+        <Title>Park</Title>
+        <KeywordList>
+          <Keyword>Park</Keyword>
+          <Keyword> Land</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="64" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Park&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="24.9451424214819" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Naherholungsgebiet</Name>
+        <Title>Naherholungsgebiet</Title>
+        <KeywordList>
+          <Keyword>Naherholungsgebiet</Keyword>
+          <Keyword> Land</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="135" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Naherholungsgebiet&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="24.9451424214819" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Wald</Name>
+        <Title>Wald/Forst</Title>
+        <KeywordList>
+          <Keyword>Wald/Forst</Keyword>
+          <Keyword> Land</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="95" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Wald&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="149.670854528891" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Wiese</Name>
+        <Title>Wiese</Title>
+        <KeywordList>
+          <Keyword>Wiese</Keyword>
+          <Keyword> Land</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="73" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Wiese&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="149.670854528891" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Fussgaengerzone</Name>
+        <Title>Fußgängerzone</Title>
+        <KeywordList>
+          <Keyword>Fußgängerzone</Keyword>
+          <Keyword> Strassen</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="115" height="32">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Fussgaengerzone&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="2.49451424214819" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Gebaeude</Name>
+        <Title>Gebäude</Title>
+        <KeywordList>
+          <Keyword>Gebäude</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="85" height="32">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Gebaeude&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="2.49451424214819" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Wasser</Name>
+        <Title>Gewaesser</Title>
+        <KeywordList>
+          <Keyword>Gewaesser</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="95" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Wasser&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="498.902848429637" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Fluesse</Name>
+        <Title>Fluesse</Title>
+        <KeywordList>
+          <Keyword>Flüße</Keyword>
+          <Keyword> Wasser</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="79" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Fluesse&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="997.805696859274" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Baeche</Name>
+        <Title>Baeche</Title>
+        <KeywordList>
+          <Keyword>Bach</Keyword>
+          <Keyword> Baeche</Keyword>
+          <Keyword> Wasser</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="67" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Baeche&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="24.9451424214819" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Kanal</Name>
+        <Title>Kanal</Title>
+        <KeywordList>
+          <Keyword>Kanal</Keyword>
+          <Keyword> Wasser</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="69" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Kanal&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="24.9451424214819" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Wasserbecken</Name>
+        <Title>Wasser- und Speicherbecken</Title>
+        <KeywordList>
+          <Keyword>Wasserbecken</Keyword>
+          <Keyword> Wasser</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="113" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Wasserbecken&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="24.9451424214819" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Insel</Name>
+        <Title>Insel</Title>
+        <KeywordList>
+          <Keyword>Insel</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="65" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Insel&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="997.805696859274" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Kueste</Name>
+        <Title>Kueste</Title>
+        <KeywordList>
+          <Keyword>Insel</Keyword>
+          <Keyword>Küste</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="70" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Kueste&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="997.805696859274" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Inselpunkte</Name>
+        <Title>Inselpunkte</Title>
+        <KeywordList>
+          <Keyword>Insel</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="95" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Inselpunkte&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="99.7805696859274" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Strand</Name>
+        <Title>Strand</Title>
+        <KeywordList>
+          <Keyword>Strand</Keyword>
+          <Keyword> Land</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="73" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Strand&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="49.8902848429637" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Fussgaengerweg</Name>
+        <Title>Fußgängerwege</Title>
+        <KeywordList>
+          <Keyword>Fußgängerwege</Keyword>
+          <Keyword> Strassen</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="112" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Fussgaengerweg&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="2.49451424214819" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Radweg</Name>
+        <Title>Radweg</Title>
+        <KeywordList>
+          <Keyword>Radweg</Keyword>
+          <Keyword> Strassen</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="80" height="32">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Radweg&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="0.997805696859274" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Wege</Name>
+        <Title>Wege</Title>
+        <KeywordList>
+          <Keyword>Wege</Keyword>
+          <Keyword> Strassen</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="127" height="221">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Wege&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="4.98902848429637" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Wohnstrasse</Name>
+        <Title>Wohnstrasse</Title>
+        <KeywordList>
+          <Keyword>Wohnstrasse</Keyword>
+          <Keyword> Strassen</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="105" height="32">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Wohnstrasse&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="19.9561139371855" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Zufahrtswege</Name>
+        <Title>Zufahrtswege</Title>
+        <KeywordList>
+          <Keyword>Zufahrtswege</Keyword>
+          <Keyword> Strassen</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="106" height="32">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Zufahrtswege&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="49.8902848429637" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>einfache_Strasse</Name>
+        <Title>einfache Strasse</Title>
+        <KeywordList>
+          <Keyword>einfache Strasse</Keyword>
+          <Keyword> Strassen</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="149" height="32">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=einfache_Strasse&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="49.8902848429637" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Landstrasse</Name>
+        <Title>Landstrasse</Title>
+        <KeywordList>
+          <Keyword>Landstrasse</Keyword>
+          <Keyword> Strassen</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="99" height="32">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Landstrasse&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="49.8902848429637" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Bundesstrasse</Name>
+        <Title>Bundesstrasse</Title>
+        <KeywordList>
+          <Keyword>Bundesstrasse</Keyword>
+          <Keyword> Strassen</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="112" height="32">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Bundesstrasse&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="199.561139371855" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Kraftfahrstrasse</Name>
+        <Title>Kraftfahrstrasse</Title>
+        <KeywordList>
+          <Keyword>Kraftfahrstrasse</Keyword>
+          <Keyword> Strassen</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="117" height="32">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Kraftfahrstrasse&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="498.902848429637" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Autobahn</Name>
+        <Title>Autobahn</Title>
+        <KeywordList>
+          <Keyword>Autobahn</Keyword>
+          <Keyword> Strassen</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="88" height="32">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Autobahn&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="9978.05696859274" />
+    </Layer>
+    <Layer queryable="1" opaque="0" cascaded="0">
+        <Name>Ortschaft</Name>
+        <Title>Ortschaft</Title>
+        <KeywordList>
+          <Keyword>Ortschaft</Keyword>
+          <Keyword> Orte</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="86" height="32">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Ortschaft&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="39.912227874371" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Weiler</Name>
+        <Title>Weiler</Title>
+        <KeywordList>
+          <Keyword>Weiler</Keyword>
+          <Keyword> Orte</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="73" height="32">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Weiler&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="9.97805696859274" />
+    </Layer>
+    <Layer queryable="1" opaque="0" cascaded="0">
+        <Name>Stadtteil</Name>
+        <Title>Stadtteil</Title>
+        <KeywordList>
+          <Keyword>Stadtteil</Keyword>
+          <Keyword> Orte</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="80" height="32">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Stadtteil&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="34.9231993900746" />
+    </Layer>
+    <Layer queryable="1" opaque="0" cascaded="0">
+        <Name>Dorf</Name>
+        <Title>Dorf</Title>
+        <KeywordList>
+          <Keyword>Dorf</Keyword>
+          <Keyword> Orte</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="63" height="32">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Dorf&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="34.9231993900746" />
+    </Layer>
+    <Layer queryable="1" opaque="0" cascaded="0">
+        <Name>Stadt</Name>
+        <Title>Stadt</Title>
+        <KeywordList>
+          <Keyword>Stadt</Keyword>
+          <Keyword> Orte</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="67" height="32">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Stadt&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="399.12227874371" />
+    </Layer>
+    <Layer queryable="1" opaque="0" cascaded="0">
+        <Name>Grossstadt</Name>
+        <Title>Grossstadt</Title>
+        <KeywordList>
+          <Keyword>Stadt</Keyword>
+          <Keyword> Orte</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="94" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Grossstadt&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="7483.54272644456" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Bahn</Name>
+        <Title>Bahn</Title>
+        <KeywordList>
+          <Keyword>Bahn</Keyword>
+          <Keyword> Zug</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="67" height="32">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Bahn&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="446.518049344525" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Bahnhof</Name>
+        <Title>Bahnhof</Title>
+        <KeywordList>
+          <Keyword>POI</Keyword>
+          <Keyword> Bahnhof</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="81" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Bahnhof&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="49.8902848429637" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Airport</Name>
+        <Title>Flughafen</Title>
+        <KeywordList>
+          <Keyword>POI</Keyword>
+          <Keyword> Flughafen</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="89" height="32">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Airport&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="49.8902848429637" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Kirchengelaende</Name>
+        <Title>Kirchengelände</Title>
+        <KeywordList>
+          <Keyword>Kirche</Keyword>
+          <Keyword> POI</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="119" height="32">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Kirchengelaende&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="9.97805696859274" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Friedhof</Name>
+        <Title>Friedhof</Title>
+        <KeywordList>
+          <Keyword>Friedhof</Keyword>
+          <Keyword> Land</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="81" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Friedhof&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="19.9561139371855" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Kirche</Name>
+        <Title>Kirche</Title>
+        <KeywordList>
+          <Keyword>Kirche</Keyword>
+          <Keyword> POI</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="72" height="32">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Kirche&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="9.97805696859274" />
+    </Layer>
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>Graeber</Name>
+        <Title>Gräber</Title>
+        <KeywordList>
+          <Keyword>Friedhof</Keyword>
+          <Keyword>Gräber</Keyword>
+          <Keyword> Land</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <Style>
+          <Name>default</Name>
+          <Title>default</Title>
+          <LegendURL width="75" height="59">
+             <Format>image/png</Format>
+             <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://example.org/service?SERVICE=WMS&version=1.1.1&service=WMS&request=GetLegendGraphic&layer=Graeber&format=image/png&STYLE=default"/>
+          </LegendURL>
+        </Style>
+        <ScaleHint min="0" max="9.97805696859274" />
+    </Layer>
+<!-- WARNING: This layer has its status set to DEFAULT and will always be displayed when doing a GetMap request even if it is not requested by the client. This is not in line with the expected behavior of a WMS server. Using status ON or OFF is recommended. -->
+    <Layer queryable="0" opaque="0" cascaded="0">
+        <Name>copyright</Name>
+        <Title>Copyright</Title>
+        <KeywordList>
+          <Keyword>Copyright</Keyword>
+          <Keyword> Lizenz</Keyword>
+          <Keyword> OSM</Keyword>
+        </KeywordList>
+        <SRS>EPSG:4326</SRS>
+        <SRS>EPSG:31467</SRS>
+        <SRS>EPSG:31466</SRS>
+        <SRS>EPSG:31468</SRS>
+        <SRS>EPSG:31469</SRS>
+        <SRS>EPSG:31492</SRS>
+        <SRS>EPSG:31493</SRS>
+        <SRS>EPSG:31494</SRS>
+        <SRS>EPSG:31495</SRS>
+        <SRS>EPSG:31462</SRS>
+        <SRS>EPSG:31463</SRS>
+        <SRS>EPSG:31464</SRS>
+        <SRS>EPSG:31465</SRS>
+        <SRS>EPSG:25832</SRS>
+        <SRS>EPSG:25833</SRS>
+        <SRS>EPSG:31257</SRS>
+        <SRS>EPSG:31258</SRS>
+        <SRS>EPSG:31259</SRS>
+        <SRS>EPSG:31284</SRS>
+        <SRS>EPSG:31285</SRS>
+        <SRS>EPSG:31286</SRS>
+        <SRS>EPSG:31287</SRS>
+        <SRS>EPSG:28992</SRS>
+        <LatLonBoundingBox minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+        <BoundingBox SRS="EPSG:4326"
+                    minx="-10.4" miny="35.7" maxx="43" maxy="74.1" />
+    </Layer>
+  </Layer>
+</Capability>
+</WMT_MS_Capabilities>
diff --git a/mapproxy/util/ext/wmsparse/test/wms-omniscale-111.xml b/mapproxy/util/ext/wmsparse/test/wms-omniscale-111.xml
new file mode 100644
index 0000000..0b1c4a3
--- /dev/null
+++ b/mapproxy/util/ext/wmsparse/test/wms-omniscale-111.xml
@@ -0,0 +1,90 @@
+<?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">
+<Service>
+  <Name>OGC:WMS</Name>
+  <Title>Omniscale OpenStreetMap WMS</Title>
+  <Abstract>Omniscale OpenStreetMap WMS (powered by MapProxy)</Abstract>
+  <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://omniscale.de/"/>
+  <ContactInformation>
+      <ContactPersonPrimary>
+        <ContactPerson>Oliver Tonnhofer</ContactPerson>
+        <ContactOrganization>Omniscale</ContactOrganization>
+      </ContactPersonPrimary>
+      <ContactPosition>Technical Director</ContactPosition>
+      <ContactAddress>
+        <AddressType>postal</AddressType>
+        <Address>Nadorster Str. 60</Address>
+        <City>Oldenburg</City>
+        <StateOrProvince></StateOrProvince>
+        <PostCode>26123</PostCode>
+        <Country>Germany</Country>
+      </ContactAddress>
+      <ContactVoiceTelephone>+49(0)441-9392774-0</ContactVoiceTelephone>
+      <ContactFacsimileTelephone>+49(0)441-9392774-9</ContactFacsimileTelephone>
+      <ContactElectronicMailAddress>osm at omniscale.de</ContactElectronicMailAddress>
+  </ContactInformation>
+  <Fees>none</Fees>
+  <AccessConstraints>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/)</AccessConstraints>
+</Service>
+<Capability>
+  <Request>
+    <GetCapabilities>
+      <Format>application/vnd.ogc.wms_xml</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://osm.omniscale.net/proxy/service?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetCapabilities>
+    <GetMap>
+        <Format>image/jpeg</Format>
+        <Format>image/png</Format>
+        <Format>image/gif</Format>
+        <Format>image/GeoTIFF</Format>
+        <Format>image/tiff</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://osm.omniscale.net/proxy/service?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetMap>
+    <GetFeatureInfo>
+      <Format>text/plain</Format>
+      <Format>text/html</Format>
+      <Format>application/vnd.ogc.gml</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://osm.omniscale.net/proxy/service?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetFeatureInfo>
+  </Request>
+  <Exception>
+    <Format>application/vnd.ogc.se_xml</Format>
+    <Format>application/vnd.ogc.se_inimage</Format>
+    <Format>application/vnd.ogc.se_blank</Format>
+  </Exception>
+  <Layer>
+    <Title>Omniscale OpenStreetMap WMS</Title>
+    <SRS>EPSG:4326 EPSG:4258 CRS:84 EPSG:900913 EPSG:31466 EPSG:31467 EPSG:31468 EPSG:25831 EPSG:25832 EPSG:25833 EPSG:3857</SRS>
+    <LatLonBoundingBox minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+    <BoundingBox SRS="EPSG:4326" minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+    <Layer>
+      <Name>osm</Name>
+      <Title>OpenStreetMap (complete map)</Title>
+      <LatLonBoundingBox minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+      <BoundingBox SRS="EPSG:4326" minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+    </Layer>
+    <Layer>
+      <Name>osm_roads</Name>
+      <Title>OpenStreetMap (streets only)</Title>
+      <LatLonBoundingBox minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+      <BoundingBox SRS="EPSG:4326" minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+    </Layer>
+  </Layer>
+</Capability>
+</WMT_MS_Capabilities>
\ No newline at end of file
diff --git a/mapproxy/util/ext/wmsparse/test/wms-omniscale-130.xml b/mapproxy/util/ext/wmsparse/test/wms-omniscale-130.xml
new file mode 100644
index 0000000..921f5dd
--- /dev/null
+++ b/mapproxy/util/ext/wmsparse/test/wms-omniscale-130.xml
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<WMS_Capabilities xmlns="http://www.opengis.net/wms" xmlns:sld="http://www.opengis.net/sld" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.3.0" xsi:schemaLocation="http://www.opengis.net/wms http://schemas.opengis.net/wms/1.3.0/capabilities_1_3_0.xsd">
+<Service>
+  <Name>WMS</Name>
+  <Title>Omniscale OpenStreetMap WMS</Title>
+  <Abstract>Omniscale OpenStreetMap WMS (powered by MapProxy)</Abstract>
+  <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://omniscale.de/"/>
+  <ContactInformation>
+      <ContactPersonPrimary>
+        <ContactPerson>Oliver Tonnhofer</ContactPerson>
+        <ContactOrganization>Omniscale</ContactOrganization>
+      </ContactPersonPrimary>
+      <ContactPosition>Technical Director</ContactPosition>
+      <ContactAddress>
+        <AddressType>postal</AddressType>
+        <Address>Nadorster Str. 60</Address>
+        <City>Oldenburg</City>
+        <StateOrProvince></StateOrProvince>
+        <PostCode>26123</PostCode>
+        <Country>Germany</Country>
+      </ContactAddress>
+      <ContactVoiceTelephone>+49(0)441-9392774-0</ContactVoiceTelephone>
+      <ContactFacsimileTelephone>+49(0)441-9392774-9</ContactFacsimileTelephone>
+      <ContactElectronicMailAddress>osm at omniscale.de</ContactElectronicMailAddress>
+  </ContactInformation>
+    <Fees>none</Fees>
+    <AccessConstraints>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/)</AccessConstraints>
+</Service>
+<Capability>
+  <Request>
+    <GetCapabilities>
+      <Format>text/xml</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xlink:href="http://osm.omniscale.net/proxy/service?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetCapabilities>
+    <GetMap>
+      <Format>image/gif</Format>
+      <Format>image/png</Format>
+      <Format>image/tiff</Format>
+      <Format>image/jpeg</Format>
+      <Format>image/GeoTIFF</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xlink:href="http://osm.omniscale.net/proxy/service?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetMap>
+    <GetFeatureInfo>
+      <Format>text/plain</Format>
+      <Format>text/html</Format>
+      <Format>text/xml</Format>
+      <DCPType>
+        <HTTP>
+          <Get><OnlineResource xlink:href="http://osm.omniscale.net/proxy/service?"/></Get>
+        </HTTP>
+      </DCPType>
+    </GetFeatureInfo>
+  </Request>
+  <Exception>
+    <Format>XML</Format>
+    <Format>INIMAGE</Format>
+    <Format>BLANK</Format>
+  </Exception>
+  <Layer>
+    <Title>Omniscale OpenStreetMap WMS</Title>
+    <CRS>EPSG:4326</CRS>
+    <CRS>EPSG:4258</CRS>
+    <CRS>CRS:84</CRS>
+    <CRS>EPSG:900913</CRS>
+    <CRS>EPSG:31466</CRS>
+    <CRS>EPSG:31467</CRS>
+    <CRS>EPSG:31468</CRS>
+    <CRS>EPSG:25831</CRS>
+    <CRS>EPSG:25832</CRS>
+    <CRS>EPSG:25833</CRS>
+    <CRS>EPSG:3857</CRS>
+    <EX_GeographicBoundingBox>
+      <westBoundLongitude>-180</westBoundLongitude>
+      <eastBoundLongitude>180</eastBoundLongitude>
+      <southBoundLatitude>-85.0511287798</southBoundLatitude>
+      <northBoundLatitude>85.0511287798</northBoundLatitude>
+    </EX_GeographicBoundingBox>
+    <BoundingBox CRS="CRS:84" minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+    <BoundingBox CRS="EPSG:900913" minx="-20037508.3428" miny="-20037508.3428" maxx="20037508.3428" maxy="20037508.3428" />
+    <BoundingBox CRS="EPSG:4326" minx="-85.0511287798" miny="-180.0" maxx="85.0511287798" maxy="180.0" />
+    <BoundingBox CRS="EPSG:3857" minx="-20037508.3428" miny="-20037508.3428" maxx="20037508.3428" maxy="20037508.3428" />
+    <Layer>
+      <Name>osm</Name>
+      <Title>OpenStreetMap (complete map)</Title>
+      <EX_GeographicBoundingBox>
+        <westBoundLongitude>-180</westBoundLongitude>
+        <eastBoundLongitude>180</eastBoundLongitude>
+        <southBoundLatitude>-85.0511287798</southBoundLatitude>
+        <northBoundLatitude>85.0511287798</northBoundLatitude>
+      </EX_GeographicBoundingBox>
+      <BoundingBox CRS="CRS:84" minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+      <BoundingBox CRS="EPSG:900913" minx="-20037508.3428" miny="-20037508.3428" maxx="20037508.3428" maxy="20037508.3428" />
+      <BoundingBox CRS="EPSG:4326" minx="-85.0511287798" miny="-180.0" maxx="85.0511287798" maxy="180.0" />
+      <BoundingBox CRS="EPSG:3857" minx="-20037508.3428" miny="-20037508.3428" maxx="20037508.3428" maxy="20037508.3428" />
+    </Layer>
+    <Layer>
+      <Name>osm_roads</Name>
+      <Title>OpenStreetMap (streets only)</Title>
+      <EX_GeographicBoundingBox>
+        <westBoundLongitude>-180</westBoundLongitude>
+        <eastBoundLongitude>180</eastBoundLongitude>
+        <southBoundLatitude>-85.0511287798</southBoundLatitude>
+        <northBoundLatitude>85.0511287798</northBoundLatitude>
+      </EX_GeographicBoundingBox>
+      <BoundingBox CRS="CRS:84" minx="-180" miny="-85.0511287798" maxx="180" maxy="85.0511287798" />
+      <BoundingBox CRS="EPSG:900913" minx="-20037508.3428" miny="-20037508.3428" maxx="20037508.3428" maxy="20037508.3428" />
+      <BoundingBox CRS="EPSG:4326" minx="-85.0511287798" miny="-180.0" maxx="85.0511287798" maxy="180.0" />
+      <BoundingBox CRS="EPSG:3857" minx="-20037508.3428" miny="-20037508.3428" maxx="20037508.3428" maxy="20037508.3428" />
+    </Layer>
+  </Layer>
+</Capability>
+</WMS_Capabilities>
\ No newline at end of file
diff --git a/mapproxy/util/ext/wmsparse/test/wms_nasa_cap.xml b/mapproxy/util/ext/wmsparse/test/wms_nasa_cap.xml
new file mode 100644
index 0000000..4ceaf7a
--- /dev/null
+++ b/mapproxy/util/ext/wmsparse/test/wms_nasa_cap.xml
@@ -0,0 +1,386 @@
+<?xml version='1.0' encoding="UTF-8" standalone="no" ?>
+<!DOCTYPE WMT_MS_Capabilities SYSTEM "http://wms.jpl.nasa.gov/WMS_MS_Capabilities.dtd" [ <!ELEMENT VendorSpecificCapabilities EMPTY> ]>
+<WMT_MS_Capabilities version="1.1.1">
+<Service>
+  <Name>OGC:WMS</Name>
+  <Title>JPL Global Imagery Service</Title>
+  <Abstract>WMS Server maintained by JPL, worldwide satellite imagery.</Abstract>
+  <KeywordList>
+    <Keyword>ImageryBaseMapsEarthCover</Keyword> <Keyword>Imagery</Keyword>
+    <Keyword>BaseMaps</Keyword> <Keyword>EarthCover</Keyword>
+    <Keyword>JPL</Keyword> <Keyword>Jet Propulsion Laboratory</Keyword> <Keyword>Landsat</Keyword>
+    <Keyword>WMS</Keyword> <Keyword>SLD</Keyword> <Keyword>Global</Keyword>
+  </KeywordList>
+  <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://OnEarth.jpl.nasa.gov/index.html" />
+  <ContactInformation>
+    <ContactPersonPrimary>
+      <ContactPerson>Lucian Plesea</ContactPerson>
+      <ContactOrganization>JPL</ContactOrganization>
+    </ContactPersonPrimary>
+    <ContactElectronicMailAddress>lucian.plesea at jpl.nasa.gov</ContactElectronicMailAddress>
+  </ContactInformation>
+  <Fees>none</Fees>
+  <AccessConstraints>Server is load limited</AccessConstraints>
+</Service>
+<Capability>
+  <Request>
+    <GetTileService>
+      <Format>text/xml</Format>
+      <DCPType>
+	<HTTP> <Get>
+	  <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://wms.jpl.nasa.gov/wms.cgi?" />
+	</Get> </HTTP>
+      </DCPType>
+    </GetTileService>
+    <GetCapabilities>
+      <Format>application/vnd.ogc.wms_xml</Format>
+      <DCPType>
+	<HTTP> <Get>
+	  <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://wms.jpl.nasa.gov/wms.cgi?" />
+	</Get> </HTTP>
+      </DCPType>
+    </GetCapabilities>
+    <GetMap>
+      <Format>image/jpeg</Format>
+      <Format>image/png</Format>
+      <Format>image/geotiff</Format>
+      <Format>image/tiff</Format>
+      <Format>application/vnd.google-earth.kml+xml</Format>
+      <DCPType> <HTTP>
+	<Get>
+	  <OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="http://wms.jpl.nasa.gov/wms.cgi?" />
+	</Get>
+      </HTTP> </DCPType>
+    </GetMap>
+  </Request>
+  <Exception>
+    <Format>application/vnd.ogc.se_xml</Format>
+  </Exception>
+  <VendorSpecificCapabilities />
+  <UserDefinedSymbolization SupportSLD="1" UserLayer="0" UserStyle="1" RemoteWFS="0" />
+  <Layer queryable="0">
+    <Title>OnEarth Web Map Server</Title>
+    <SRS>EPSG:4326</SRS>
+    <SRS>AUTO:42003</SRS>
+    <CRS>EPSG:4326</CRS>
+    <CRS>AUTO:42003</CRS>
+
+    <Layer queryable="0">
+      <Name>global_mosaic</Name> 
+      <Title>WMS Global Mosaic, pan sharpened</Title>
+      <Abstract>
+	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.
+      </Abstract>
+      <LatLonBoundingBox minx="-180" miny="-60" maxx="180" maxy="84"/>
+      <MetadataURL type="FGDC">
+	<Format>text/xml</Format>
+	<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink"
+	  xlink:type="simple"
+	  xlink:href="http://onearth.jpl.nasa.gov/WAF/WMS_GM.xml"
+	/>
+      </MetadataURL>
+      <Style> <Name>pseudo</Name> <Title>(default) Pseudo-color image, pan sharpened (Uses IR and Visual bands, 542 mapping), gamma 1.5</Title> </Style>
+      <Style> <Name>pseudo_low</Name> <Title>Pseudo-color image, pan sharpened (Uses IR and Visual bands, 542 mapping)</Title> </Style>
+      <Style> <Name>pseudo_bright</Name> <Title>Pseudo-color image (Uses IR and Visual bands, 542 mapping), gamma 1.5</Title> </Style>
+      <Style> <Name>visual</Name> <Title>Real-color image, pan sharpened (Uses the visual bands, 321 mapping), gamma 1.5</Title> </Style>
+      <Style> <Name>visual_low</Name> <Title>Real-color image, pan sharpened (Uses the visual bands, 321 mapping)</Title> </Style>
+      <Style> <Name>visual_bright</Name> <Title>Real-color image (Uses the visual bands, 321 mapping), gamma 1.5</Title> </Style>
+      <ScaleHint min="10" max="10000"/>
+      <MinScaleDenominator>20000</MinScaleDenominator>
+    </Layer>
+
+    <Layer queryable="0">
+      <Name>global_mosaic_base</Name>
+      <Title>WMS Global Mosaic, not pan sharpened</Title>
+      <Abstract>
+	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.
+      </Abstract>
+      <LatLonBoundingBox minx="-180" miny="-60" maxx="180" maxy="84"/>
+      <Style> <Name>pseudo</Name> <Title>Pseudo-color image (Uses IR and Visual bands, 542 mapping) (default)</Title> </Style>
+      <Style> <Name>visual</Name> <Title>Real-color image (Uses the visual bands, 321 mapping)</Title> </Style>
+      <Style> <Name>Pan</Name> <Title>Pan-chromatic band, grayscale</Title> </Style>
+      <Style> <Name>Red</Name> <Title>Visual Red band, grayscale</Title> </Style>
+      <Style> <Name>Green</Name> <Title>Visual Green band, grayscale</Title> </Style>
+      <Style> <Name>Blue</Name> <Title>Visual Blue band, grayscale</Title> </Style>
+      <Style> <Name>IR1</Name> <Title> Near IR band 1, (Landsat band 4), grayscale</Title> </Style>
+      <Style> <Name>IR2</Name> <Title> Near IR band 2, (Landsat band 5), grayscale</Title> </Style>
+      <Style> <Name>IR3</Name> <Title> Near IR band 2, (Landsat band 7), grayscale</Title> </Style>
+      <Style> <Name>ThL</Name> <Title> Thermal band, low gain, grayscale</Title> </Style>
+      <Style> <Name>ThH</Name> <Title> Thermal band, high gain, grayscale</Title> </Style>
+      <ScaleHint min="10" max="10000"/>
+      <MinScaleDenominator>20000</MinScaleDenominator>
+    </Layer>
+
+    <Layer queryable="0">
+      <Name>us_landsat_wgs84</Name>
+      <Title>CONUS mosaic of 1990 MRLC dataset</Title>
+      <Abstract>
+	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!
+      </Abstract>
+      <LatLonBoundingBox minx="-127" miny="23" maxx="-66" maxy="50"/>
+
+      <Style> <Name>pseudo</Name> <Title>Pseudo-color image (Uses IR and Visual bands, 542 mapping)</Title> </Style>
+      <Style> <Name>visual</Name> <Title>Real-color image (Uses the visual bands, 321 mapping)</Title> </Style>
+      <Style> <Name>Red</Name> <Title>Visual Red band, grayscale</Title> </Style>
+      <Style> <Name>Green</Name> <Title>Visual Green band, grayscale</Title> </Style>
+      <Style> <Name>Blue</Name> <Title>Visual Blue band, grayscale</Title> </Style>
+      <Style> <Name>IR1</Name> <Title> Near IR band 1, (Landsat band 4), grayscale</Title> </Style>
+      <Style> <Name>IR2</Name> <Title> Near IR band 2, (Landsat band 5), grayscale</Title> </Style>
+      <Style> <Name>IR3</Name> <Title> Near IR band 2, (Landsat band 7), grayscale</Title> </Style>
+      <ScaleHint min="20" max="10000"/>
+    </Layer>
+
+    <Layer queryable="0">
+      <Name>srtm_mag</Name>
+      <Title>SRTM reflectance magnitude, 30m</Title>
+      <Abstract>
+  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 mult [...]
+      </Abstract>
+      <LatLonBoundingBox minx="-180" miny="-55" maxx="180" maxy="60"/>
+      <Style> <Name>default</Name>
+	<Title>Arbitrary color image of the SRTM averaged reflectance</Title>
+      </Style>
+      <Style> <Name>all</Name> <Title>SRTM average reflectance, grayscale</Title> </Style>
+      <Style> <Name>ss1</Name> <Title>SRTM average reflectance of subswath 1 data</Title> </Style>
+      <Style> <Name>ss2</Name> <Title>SRTM average reflectance of subswath 2 data</Title> </Style>
+      <Style> <Name>ss3</Name> <Title>SRTM average reflectance of subswath 3 data</Title> </Style>
+      <Style> <Name>ss4</Name> <Title>SRTM average reflectance of subswath 4 data</Title> </Style>
+
+      <ScaleHint min="10" max="10000"/>
+      <MinScaleDenominator>20000</MinScaleDenominator>
+    </Layer>
+
+    <Layer queryable="0">
+      <Name>daily_planet</Name>
+      <Title>Current global view of the earth, morning</Title>
+      <Abstract>
+        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.
+      </Abstract>
+      <LatLonBoundingBox minx="-180" miny="-72" maxx="180" maxy="72" />
+      <Dimension name="time" units="ISO8601"/>
+      <Extent name="time">2007-12-01/2010-03-20/P1D</Extent>
+      <Style> <Name>default</Name> <Title>visual</Title> </Style>
+      <ScaleHint min="125" max="10000" />
+    </Layer>
+
+    <Layer queryable="0">
+      <Name>daily_afternoon</Name>
+      <Title>Current global view of the earth in the afternoon</Title>
+      <Abstract>
+        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.
+      </Abstract>
+      <LatLonBoundingBox minx="-180" miny="-72" maxx="180" maxy="72" />
+      <Dimension name="time" units="ISO8601"/>
+      <Extent name="time">2008-12-01/2010-03-20/P1D</Extent>
+      <Style> <Name>default</Name> <Title>visual</Title> </Style>
+      <ScaleHint min="125" max="10000" />
+    </Layer>
+    <Layer queryable="0">
+      <Name>BMNG</Name>
+      <Title>Blue Marble Next Generation, Global MODIS derived image</Title>
+      <Abstract>
+        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.
+      </Abstract>
+	<LatLonBoundingBox minx="-180" miny="-90" maxx="180" maxy="90" />
+	<Style><Name>default</Name><Title>October</Title></Style>
+	<Style><Name>Jan</Name><Title>January</Title></Style>
+	<Style><Name>Feb</Name><Title>February</Title></Style>
+	<Style><Name>Mar</Name><Title>March</Title></Style>
+	<Style><Name>Apr</Name><Title>April</Title></Style>
+	<Style><Name>May</Name><Title>May</Title></Style>
+	<Style><Name>Jun</Name><Title>June</Title></Style>
+	<Style><Name>Jul</Name><Title>July</Title></Style> 
+	<Style><Name>Aug</Name><Title>August</Title></Style>
+	<Style><Name>Sep</Name><Title>September</Title></Style>
+	<Style><Name>Oct</Name><Title>October</Title></Style>
+	<Style><Name>Nov</Name><Title>November</Title></Style>
+	<Style><Name>Dec</Name><Title>December</Title></Style>
+	<Style><Name>Jan_nb</Name><Title>January, no bathymetry shading</Title></Style>
+	<Style><Name>Feb_nb</Name><Title>February, no bathymetry shading</Title></Style>
+	<Style><Name>Mar_nb</Name><Title>March, no bathymetry shading</Title></Style>
+	<Style><Name>Apr_nb</Name><Title>April, no bathymetry shading</Title></Style>
+	<Style><Name>May_nb</Name><Title>May, no bathymetry shading</Title></Style>
+	<Style><Name>Jun_nb</Name><Title>June, no bathymetry shading</Title></Style>
+	<Style><Name>Jul_nb</Name><Title>July, no bathymetry shading</Title></Style> 
+	<Style><Name>Aug_nb</Name><Title>August, no bathymetry shading</Title></Style>
+	<Style><Name>Sep_nb</Name><Title>September, no bathymetry shading</Title></Style>
+	<Style><Name>Oct_nb</Name><Title>October, no bathymetry shading</Title></Style>
+	<Style><Name>Nov_nb</Name><Title>November, no bathymetry shading</Title></Style>
+	<Style><Name>Dec_nb</Name><Title>December, no bathymetry shading</Title></Style>
+	<Style><Name>Jan_ns</Name><Title>January, no shading</Title></Style>
+	<Style><Name>Feb_ns</Name><Title>February, no shading</Title></Style>
+	<Style><Name>Mar_ns</Name><Title>March, no shading</Title></Style>
+	<Style><Name>Apr_ns</Name><Title>April, no shading</Title></Style>
+	<Style><Name>May_ns</Name><Title>May, no shading</Title></Style>
+	<Style><Name>Jun_ns</Name><Title>June, no shading</Title></Style>
+	<Style><Name>Jul_ns</Name><Title>July, no shading</Title></Style> 
+	<Style><Name>Aug_ns</Name><Title>August, no shading</Title></Style>
+	<Style><Name>Sep_ns</Name><Title>September, no shading</Title></Style>
+	<Style><Name>Oct_ns</Name><Title>October, no shading</Title></Style>
+	<Style><Name>Nov_ns</Name><Title>November, no shading</Title></Style>
+	<Style><Name>Dec_ns</Name><Title>December, no shading</Title></Style>
+	<ScaleHint min="250" max="10000" />
+    </Layer>
+
+    <Layer queryable="0">
+      <Name>modis</Name>
+      <Title>Blue Marble, Global MODIS derived image</Title>
+      <LatLonBoundingBox minx="-180" miny="-90" maxx="180" maxy="90" />
+      <Style> <Name>default</Name><Title>visual</Title></Style>
+      <ScaleHint min="500" max="10000" />
+    </Layer>
+
+    <Layer queryable="0">
+      <Name>huemapped_srtm</Name>
+      <Title>SRTM derived global elevation, 3 arc-second, hue mapped</Title>
+      <Abstract> An SRTM derived elevation dataset, where elevation is mapped to hue, resulting a color image</Abstract>
+      <LatLonBoundingBox minx="-180" miny="-80" maxx="180" maxy="80"/>
+      <Style> <Name>default</Name> <Title>Default Elevation Style</Title> </Style>
+      <ScaleHint min="45" max="10000" />
+      <MinScaleDenominator>12000</MinScaleDenominator>
+    </Layer>
+
+    <Layer queryable="0">
+      <Name>srtmplus</Name> <Title>Global 1km elevation, seamless SRTM land elevation and ocean depth</Title>
+      <Abstract>
+	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.
+      </Abstract>
+      <LatLonBoundingBox minx="-180" miny="-80" maxx="180" maxy="80"/>
+      <Style> <Name>default</Name> <Title>Default Elevation Style, scaled to 8 bit using a non-linear function</Title> </Style>
+      <Style>
+	<Name>short_int</Name>
+	<Title>short int elevation values when format is image/png, identical to default for jpeg</Title>
+      </Style>
+      <Style>
+	<Name>feet_short_int</Name>
+	<Title>short int elevation values in feet when format is image/png </Title>
+      </Style>
+      <ScaleHint min="500" max="10000" />
+      <MinScaleDenominator>120000</MinScaleDenominator>
+    </Layer>
+
+    <Layer queryable="0">
+      <Name>worldwind_dem</Name>
+      <Title>SRTM derived global elevation, 3 arc-second</Title>
+      <Abstract>
+	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.
+      </Abstract>
+      <LatLonBoundingBox minx="-180" miny="-80" maxx="180" maxy="80"/>
+      <Style>
+	<Name>default</Name>
+	<Title>Default Elevation Style, scaled to 8 bit using a non-linear function</Title>
+      </Style>
+      <Style>
+	<Name>short_int</Name>
+	<Title>short int elevation values when format is image/png</Title>
+      </Style>
+      <Style>
+	<Name>feet_short_int</Name>
+	<Title>short int elevation values in feet when format is image/png</Title>
+      </Style>
+      <ScaleHint min="45" max="10000" />
+      <MinScaleDenominator>120000</MinScaleDenominator>
+    </Layer>
+
+    <Layer queryable="0">
+      <Name>us_ned</Name>
+      <Title>United States elevation, 30m</Title>
+      <Abstract>
+        Continental United States elevation, produced from the USGS National Elevation.
+        The default style is scaled to 8 bit from the orginal floating point data.
+      </Abstract>
+      <LatLonBoundingBox minx="-125" miny="24" maxx="-66" maxy="50"/>
+      <Style><Name>default</Name> <Title>Default Elevation</Title> </Style>
+      <Style><Name>short_int</Name> <Title>short int signed elevation values when format is image/png or tiff</Title> </Style>
+      <Style><Name>feet_short_int</Name> <Title>short int elevation values in feet when format is image/png or image/tiff</Title> </Style>
+      <Style><Name>real</Name> <Title>DEM real numbers, in floating point format, meters, when used with image/tiff</Title> </Style>
+      <Style><Name>feet_real</Name> <Title>DEM in real numbers, in floating point format, feet, when used with image/tiff</Title> </Style>
+      <ScaleHint min="20" max="10000" /> <MinScaleDenominator>24000</MinScaleDenominator>
+    </Layer>
+
+    <Layer queryable="0">
+      <Name>us_elevation</Name>
+      <Title>Digital Elevation Map of the United States, DTED dataset, 3 second resolution, grayscale</Title>
+      <Abstract>
+	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).
+      </Abstract>
+      <LatLonBoundingBox minx="-127" miny="23" maxx="-66" maxy="50"/>
+      <Style>
+	<Name>default</Name>
+	<Title>Default Elevation</Title>
+      </Style>
+      <Style>
+	<Name>short_int</Name>
+	<Title>short int elevation values when format is image/png</Title>
+      </Style>
+      <Style>
+	<Name>feet_short_int</Name>
+	<Title>short int elevation values in feet when format is image/png</Title>
+      </Style>
+      <ScaleHint min="45" max="10000" />
+    </Layer>
+
+    <Layer queryable="0">
+      <Name>us_colordem</Name>
+      <Title>Digital Elevation Map of the United States, DTED dataset, 3 second resolution, hue mapped</Title>
+      <Abstract>
+	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.
+      </Abstract>
+      <LatLonBoundingBox minx="-127" miny="23" maxx="-66" maxy="50"/>
+      <Style>
+	<Name>default</Name>
+	<Title>Default Color Elevation</Title>
+      </Style>
+      <ScaleHint min="45" max="10000" />
+      <MinScaleDenominator>20000</MinScaleDenominator>
+    </Layer>
+
+    <Layer queryable="0">
+      <Name>gdem</Name>
+      <Title>ASTER DEM, tiled only, 1.5 arc-second per pixel</Title>
+      <Abstract> 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.
+      </Abstract>
+      <LatLonBoundingBox minx="-180" miny="-90" maxx="180" maxy="90"/>
+      <Style>
+	<Name>short_int</Name>
+	<Title>short int elevation values when format is image/png</Title>
+      </Style>
+      <ScaleHint min="45" max="10000" />
+      <MinScaleDenominator>20000</MinScaleDenominator>
+    </Layer>
+
+  </Layer>
+
+</Capability>
+</WMT_MS_Capabilities>
\ No newline at end of file
diff --git a/mapproxy/util/ext/wmsparse/util.py b/mapproxy/util/ext/wmsparse/util.py
new file mode 100644
index 0000000..67c94ff
--- /dev/null
+++ b/mapproxy/util/ext/wmsparse/util.py
@@ -0,0 +1,22 @@
+import 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)
diff --git a/mapproxy/util/fs.py b/mapproxy/util/fs.py
new file mode 100644
index 0000000..675a543
--- /dev/null
+++ b/mapproxy/util/fs.py
@@ -0,0 +1,139 @@
+# 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.
+
+"""
+File system related utility functions.
+"""
+from __future__ import with_statement, 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)
+            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)
diff --git a/mapproxy/util/geom.py b/mapproxy/util/geom.py
new file mode 100644
index 0000000..b767652
--- /dev/null
+++ b/mapproxy/util/geom.py
@@ -0,0 +1,219 @@
+# 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, with_statement
+
+import os
+import codecs
+from functools import partial
+from contextlib import closing
+
+from mapproxy.compat import string_type
+
+import logging
+log_config = logging.getLogger('mapproxy.config.coverage')
+
+try:
+    import shapely.wkt
+    import shapely.geometry
+    import shapely.ops
+    import shapely.prepared
+    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 file
+    if os.path.exists(os.path.abspath(datasource)):
+        with open(os.path.abspath(datasource), 'r') as fp:
+            data = fp.read(50)
+        if data.lower().lstrip().startswith(('polygon', 'multipolygon')):
+            return load_polygons(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):
+                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_polygon_lines(line_iter, source='<string>'):
+    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:
+        p = shapely.geometry.Polygon()
+        return p.bounds, p
+
+    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':
+        return transform_polygon(transf, geometry)
+
+    if geometry.type == 'MultiPolygon':
+        return transform_multipolygon(transf, geometry)
+
+    raise ValueError('cannot transform %s' % geometry.type)
+
+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 []
+
+
diff --git a/mapproxy/util/lib.py b/mapproxy/util/lib.py
new file mode 100644
index 0000000..6eeed6c
--- /dev/null
+++ b/mapproxy/util/lib.py
@@ -0,0 +1,108 @@
+# 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.
+
+"""
+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]))
diff --git a/mapproxy/util/lock.py b/mapproxy/util/lock.py
new file mode 100644
index 0000000..963145b
--- /dev/null
+++ b/mapproxy/util/lock.py
@@ -0,0 +1,164 @@
+# This file is part of the MapProxy project.
+# Copyright (C) 2010-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.
+
+"""
+Utility methods and classes (file locking, asynchronous execution pools, etc.).
+"""
+from __future__ import with_statement
+
+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
diff --git a/mapproxy/util/ogr.py b/mapproxy/util/ogr.py
new file mode 100644
index 0000000..a18a930
--- /dev/null
+++ b/mapproxy/util/ogr.py
@@ -0,0 +1,228 @@
+# 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
+
+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 at 0
+    # mangling, otherwise print no detailed errors
+    if not hasattr(libgdal, 'CPLGetLastErrorMsg') and hasattr(libgdal, '_CPLGetLastErrorMsg at 0'):
+        libgdal.CPLGetLastErrorMsg = getattr(libgdal, '_CPLGetLastErrorMsg at 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)
+            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)
diff --git a/mapproxy/util/py.py b/mapproxy/util/py.py
new file mode 100644
index 0000000..b76b3ab
--- /dev/null
+++ b/mapproxy/util/py.py
@@ -0,0 +1,81 @@
+# 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.
+
+"""
+Python related helper functions.
+"""
+from __future__ import with_statement
+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):
+        if not hasattr(self, '__memoize_cache'):
+            self.__memoize_cache = {}
+        cache = self.__memoize_cache.setdefault(func, {})
+        if args not in cache:
+            cache[args] = func(self, *args)
+        return cache[args]
+    return wrapper
+
diff --git a/mapproxy/util/times.py b/mapproxy/util/times.py
new file mode 100644
index 0000000..aedacba
--- /dev/null
+++ b/mapproxy/util/times.py
@@ -0,0 +1,75 @@
+# This file is part of the MapProxy project.
+# Copyright (C) 2010-213 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
+
+"""
+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())
\ No newline at end of file
diff --git a/mapproxy/util/wsgi.py b/mapproxy/util/wsgi.py
new file mode 100644
index 0000000..e76b808
--- /dev/null
+++ b/mapproxy/util/wsgi.py
@@ -0,0 +1,41 @@
+# 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.
+
+"""
+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)
diff --git a/mapproxy/util/yaml.py b/mapproxy/util/yaml.py
new file mode 100644
index 0000000..3fde31a
--- /dev/null
+++ b/mapproxy/util/yaml.py
@@ -0,0 +1,48 @@
+# 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 __future__ import with_statement, 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))
+
diff --git a/mapproxy/version.py b/mapproxy/version.py
new file mode 100644
index 0000000..2e07158
--- /dev/null
+++ b/mapproxy/version.py
@@ -0,0 +1,31 @@
+# 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
+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__)
\ No newline at end of file
diff --git a/mapproxy/wsgiapp.py b/mapproxy/wsgiapp.py
new file mode 100644
index 0000000..7d08b12
--- /dev/null
+++ b/mapproxy/wsgiapp.py
@@ -0,0 +1,211 @@
+# 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.
+
+"""
+The WSGI application.
+"""
+from __future__ import print_function
+import re
+import os
+import sys
+import time
+import threading
+import warnings
+
+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 sys.version_info[0] == 2 and sys.version_info[1] == 5:
+        warnings.warn('Support for Python 2.5 is deprecated since 1.7.0 and will be dropped with 1.8.0', FutureWarning)
+
+    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 = "<html><body><h1>Welcome to MapProxy %s</h1>" % mapproxy.version.version
+        if 'demo' in self.handlers:
+            html += '<p>See all configured layers and services at: <a href="%s/demo/">demo</a>' % (script_url, )
+        return Response(html, mimetype='text/html')
diff --git a/requirements-tests.txt b/requirements-tests.txt
new file mode 100644
index 0000000..2152b3a
--- /dev/null
+++ b/requirements-tests.txt
@@ -0,0 +1,12 @@
+WebTest==2.0.10
+lxml==3.2.4
+nose==1.3.0
+Shapely==1.5.8
+PyYAML==3.10
+Pillow==2.8.1
+WebOb==1.2.3
+beautifulsoup4==4.4.0
+coverage==3.7
+requests==2.0.1
+six==1.4.1
+waitress==0.8.7
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..ee4fd5f
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,14 @@
+[nosetests]
+cover-erase = 1
+verbosity = 2
+doctest-tests = 1
+with-doctest = 1
+
+[egg_info]
+tag_date = 0
+tag_build = 
+tag_svn_revision = 0
+
+[bdist_wheel]
+universal = 1
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..434d606
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,100 @@
+import 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.8.2",
+    description='An accelerating proxy for web map services',
+    long_description=long_description(7),
+    author='Oliver Tonnhofer',
+    author_email='olt at omniscale.de',
+    url='http://mapproxy.org',
+    license='Apache Software License 2.0',
+    namespace_packages = ['mapproxy'],
+    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.6",
+        "Programming Language :: Python :: 2.7",
+        "Programming Language :: Python :: 3.3",
+        "Programming Language :: Python :: 3.4",
+        "Programming Language :: Python :: 3.5",
+        "Topic :: Internet :: Proxy Servers",
+        "Topic :: Internet :: WWW/HTTP :: WSGI",
+        "Topic :: Scientific/Engineering :: GIS",
+    ],
+    zip_safe=False,
+    test_suite='nose.collector',
+)

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/mapproxy.git



More information about the Pkg-grass-devel mailing list