[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:
+