diff -Nru mapproxy-1.14.0/CHANGES.txt mapproxy-1.15.1/CHANGES.txt --- mapproxy-1.14.0/CHANGES.txt 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/CHANGES.txt 2022-06-14 12:39:29.000000000 +0000 @@ -1,5 +1,25 @@ Nightly ~~~~~~~~~~~~~~~~~ +1.15.1 2022-06-14 +~~~~~~~~~~~~~~~~~ + +Fixes: + +- Fixup release on PyPI + +1.15.0 2022-06-14 +~~~~~~~~~~~~~~~~~ + +Improvements: + +- WMS Dimension caching (#449) +- Add a mechanism to define plugins (#578) +- Support of non-EPSG SRS authorities (#572) +- Support for python 3.10 (#582) + +Fixes: + +- Several minor bugfixes 1.14.0 2021-11-24 ~~~~~~~~~~~~~~~~~ diff -Nru mapproxy-1.14.0/debian/changelog mapproxy-1.15.1/debian/changelog --- mapproxy-1.14.0/debian/changelog 2021-11-24 14:06:43.000000000 +0000 +++ mapproxy-1.15.1/debian/changelog 2022-12-05 08:00:00.000000000 +0000 @@ -1,3 +1,37 @@ +mapproxy (1.15.1-2~jammy0) jammy; urgency=medium + + * No change rebuild for Jammy. + + -- Angelos Tzotsos Mon, 05 Dec 2022 10:00:00 +0200 + +mapproxy (1.15.1-2) unstable; urgency=medium + + [ Bas Couwenberg ] + * Add patch by Chris Lamb to make the build reproducible. + (closes: #1012836) + * Bump Standards-Version to 4.6.1, no changes. + * Add Rules-Requires-Root to control file. + * Update lintian overrides. + + [ Angelos Tzotsos ] + * Added patch to work with mapnik supporting latest proj. + + -- Bas Couwenberg Thu, 01 Dec 2022 12:46:51 +0100 + +mapproxy (1.15.1-1) unstable; urgency=medium + + * New upstream release. + + -- Bas Couwenberg Tue, 14 Jun 2022 15:18:31 +0200 + +mapproxy (1.15.0-1) unstable; urgency=medium + + * New upstream release. + * Update copyright file. + * Add python3-pyproj to build dependencies. + + -- Bas Couwenberg Tue, 14 Jun 2022 12:38:12 +0200 + mapproxy (1.14.0-1) unstable; urgency=medium * New upstream release. diff -Nru mapproxy-1.14.0/debian/control mapproxy-1.15.1/debian/control --- mapproxy-1.14.0/debian/control 2021-09-12 15:30:31.000000000 +0000 +++ mapproxy-1.15.1/debian/control 2022-11-28 12:17:11.000000000 +0000 @@ -10,6 +10,7 @@ python3-lxml, python3-pil, python3-pkg-resources, + python3-pyproj, python3-pytest, python3-redis, python3-requests, @@ -23,10 +24,11 @@ docbook-xsl, docbook-xml, xsltproc -Standards-Version: 4.6.0 +Standards-Version: 4.6.1 Vcs-Browser: https://salsa.debian.org/debian-gis-team/mapproxy Vcs-Git: https://salsa.debian.org/debian-gis-team/mapproxy.git Homepage: http://mapproxy.org/ +Rules-Requires-Root: no Package: mapproxy Architecture: all diff -Nru mapproxy-1.14.0/debian/copyright mapproxy-1.15.1/debian/copyright --- mapproxy-1.14.0/debian/copyright 2020-05-24 12:59:44.000000000 +0000 +++ mapproxy-1.15.1/debian/copyright 2022-06-15 12:07:05.000000000 +0000 @@ -5,6 +5,7 @@ Files: * Copyright: 2010-2017, Omniscale + 2022, Even Rouault License: Apache-2.0 Files: mapproxy/image/fonts/*.ttf diff -Nru mapproxy-1.14.0/debian/mapproxy-doc.lintian-overrides mapproxy-1.15.1/debian/mapproxy-doc.lintian-overrides --- mapproxy-1.14.0/debian/mapproxy-doc.lintian-overrides 2021-03-12 12:21:48.000000000 +0000 +++ mapproxy-1.15.1/debian/mapproxy-doc.lintian-overrides 2022-11-28 12:17:11.000000000 +0000 @@ -1,8 +1,8 @@ # libjs-twitter-bootstrap is not compatible -embedded-javascript-library usr/share/doc/mapproxy/html/_static/bootstrap-*/js/bootstrap.js please use libjs-bootstrap -font-in-non-font-package usr/share/doc/mapproxy/html/_static/boot*/fonts/* -font-outside-font-dir usr/share/doc/mapproxy/html/_static/boots*/fonts/* +embedded-javascript-library please use libjs-bootstrap [usr/share/doc/mapproxy/html/_static/bootstrap-*/js/bootstrap.js] +font-in-non-font-package [usr/share/doc/mapproxy/html/_static/boot*/fonts/*] +font-outside-font-dir [usr/share/doc/mapproxy/html/_static/boots*/fonts/*] # libjs-jquery is not compatible -embedded-javascript-library usr/share/doc/mapproxy/html/_static/js/jquery* please use libjs-jquery +embedded-javascript-library please use libjs-jquery [usr/share/doc/mapproxy/html/_static/js/jquery*] diff -Nru mapproxy-1.14.0/debian/patches/mapnik.patch mapproxy-1.15.1/debian/patches/mapnik.patch --- mapproxy-1.14.0/debian/patches/mapnik.patch 1970-01-01 00:00:00.000000000 +0000 +++ mapproxy-1.15.1/debian/patches/mapnik.patch 2022-11-28 12:19:31.000000000 +0000 @@ -0,0 +1,15 @@ +Description: Patch to work with latest mapnik proj support +Author: Angelos Tzotsos +Forwarded: https://github.com/mapproxy/mapproxy/issues/538#issuecomment-1205046859 + +--- mapproxy-1.15.1.orig/mapproxy/source/mapnik.py ++++ mapproxy-1.15.1/mapproxy/source/mapnik.py +@@ -120,7 +120,7 @@ class MapnikSource(MapLayer): + + m = self.map_obj(mapfile) + m.resize(query.size[0], query.size[1]) +- m.srs = '+init=%s' % str(query.srs.srs_code.lower()) ++ m.srs = str(query.srs.srs_code.lower()) + envelope = mapnik.Box2d(*query.bbox) + m.zoom_to_box(envelope) + data = None diff -Nru mapproxy-1.14.0/debian/patches/reproducible-build.patch mapproxy-1.15.1/debian/patches/reproducible-build.patch --- mapproxy-1.14.0/debian/patches/reproducible-build.patch 1970-01-01 00:00:00.000000000 +0000 +++ mapproxy-1.15.1/debian/patches/reproducible-build.patch 2022-06-15 12:07:40.000000000 +0000 @@ -0,0 +1,53 @@ +Description: Make the build reproducible +Author: Chris Lamb +Bug-Debian: https://bugs.debian.org/1012836 +Forwarded: https://github.com/mapproxy/mapproxy/pull/585 + +--- a/mapproxy/cache/geopackage.py ++++ b/mapproxy/cache/geopackage.py +@@ -14,12 +14,14 @@ + # limitations under the License. + + ++import datetime + import hashlib + import logging + import os + import re + import sqlite3 + import threading ++import time + + from mapproxy.cache.base import TileCacheBase, tile_buffer, REMOVE_ON_UNLOCK + from mapproxy.compat import BytesIO, PY2, itertools +@@ -305,6 +307,10 @@ AUTHORITY["EPSG","9122"]],AUTHORITY["EPS + log.info("srs_id already exists.".format(wkt_entry[0])) + db.commit() + ++ last_change = datetime.datetime.utcfromtimestamp( ++ int(os.environ.get('SOURCE_DATE_EPOCH', time.time())) ++ ) ++ + # Ensure that tile table exists here, don't overwrite a valid entry. + try: + db.execute(""" +@@ -313,16 +319,18 @@ AUTHORITY["EPSG","9122"]],AUTHORITY["EPS + data_type, + identifier, + description, ++ last_change, + min_x, + max_x, + min_y, + max_y, + srs_id) +- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); ++ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + """, (self.table_name, + "tiles", + self.table_name, + "Created with Mapproxy.", ++ last_change, + self.tile_grid.bbox[0], + self.tile_grid.bbox[2], + self.tile_grid.bbox[1], diff -Nru mapproxy-1.14.0/debian/patches/series mapproxy-1.15.1/debian/patches/series --- mapproxy-1.14.0/debian/patches/series 2021-03-12 12:21:48.000000000 +0000 +++ mapproxy-1.15.1/debian/patches/series 2022-11-28 12:17:11.000000000 +0000 @@ -1 +1,3 @@ disable-tag_date.patch +reproducible-build.patch +mapnik.patch diff -Nru mapproxy-1.14.0/doc/caching_layer_dimensions.rst mapproxy-1.15.1/doc/caching_layer_dimensions.rst --- mapproxy-1.14.0/doc/caching_layer_dimensions.rst 1970-01-01 00:00:00.000000000 +0000 +++ mapproxy-1.15.1/doc/caching_layer_dimensions.rst 2022-06-14 12:39:29.000000000 +0000 @@ -0,0 +1,193 @@ +Caching Dimensions +################## + +WMS servers have the capability to offer layers with 1..n dimensions. A WMS +layer may provide dimensions such as time, elevation or other axes to be able +to visualize maps of multidimensional data. The `OGC OGC Best Practice for +using Web Map Services (WMS) with Time-Dependent or Elevation-Dependent Data`_ +is an example typically implemented by WMS providers of multidimensional +data. + +MapProxy supports caching layers with dimensions and making them available +through standard WMS mechanisms. + +Configuration +============= + +To enable dimension caching, the underlying WMS layer must have dimensions +defined and available. The following properties are required in configuration +to successfully cache any layer with dimensions: + +- ``forward_req_params``: list of 1..n query parameter names to send when + caching the layer, if request by the MapProxy client. This property is + specified in a source +- ``dimensions``: an object of 1..n keys of dimension definitions. The + dimension names must match with those specified in ``forward_req_params`` + for a given layer/source combination. Each dimension object requires + a default dimension (in cases where not specified by the client) as well + as a list of dimension values. Dimension lists can be intervals, or a + a compound value of ``start-date-time/end-date-time/duration`` + +An example is shown below: + +.. code-block:: yaml + + sources: + test: + type: wms + req: + url: https://example.org/wms + layers: global-air-temperature-15km + forward_req_params: + - time + - dim_reference_time + +.. code-block:: yaml + + layers: + - name: global-air-temperature-15km + title: Global Air Temperature (°C) - 15km + sources: [test_cache] + dimensions: + time: + values: + - "2020-09-22T11:20:00Z/2020-09-22T14:20:00Z/PT2H" + default: "2020-09-22T14:20:00Z" + dim_reference_time: + values: + - "2020-09-22T11:20:00Z/2020-09-22T14:20:00Z/PT2H" + default: "2020-09-22T14:20:00Z" + + +ISO 8601 Interval +================= + +For example: + ``2020-03-25T12:00:00Z/2020-03-27T00:00:00Z/PT12H30M`` + +A time interval is the intervening time between two time points. The amount of intervening time is expressed by a duration (as described in the previous section). The two time points (start and end) are expressed by either a combined date and time representation or just a date representation. + +There are four ways to express a time interval: + 1. Start and end, such as "2007-03-01T13:00:00Z/2008-05-11T15:30:00Z" + 2. Start and duration, such as "2007-03-01T13:00:00Z/P1Y2M10DT2H30M" + 3. Duration and end, such as "P1Y2M10DT2H30M/2008-05-11T15:30:00Z" + 4. Duration only, such as "P1Y2M10DT2H30M", with additional context information + + P is the duration designator (for period) placed at the start of the duration representation. + - Y is the year designator that follows the value for the number of years. + - M is the month designator that follows the value for the number of months. + - W is the week designator that follows the value for the number of weeks. + - D is the day designator that follows the value for the number of days. + T is the time designator that precedes the time components of the representation. + - H is the hour designator that follows the value for the number of hours. + - M is the minute designator that follows the value for the number of minutes. + - S is the second designator that follows the value for the number of seconds. + +WMS Capabilities +================ + +The following is an example of the resulting WMS capabilities in MapProxy: + +.. code-block:: xml + + + + + WMS + Test Dimension + + + none + none + 4000 + 4000 + + + + + text/xml + + + + + + + + + + image/png + image/jpeg + image/gif + image/GeoTIFF + image/tiff + + + + + + + + + + text/plain + text/html + text/xml + + + + + + + + + + + XML + INIMAGE + BLANK + + + global-air-temperature-15km + Global Air Temperature (°C) - 15km + EPSG:4326 + EPSG:3857 + + -180 + 180 + -89.999999 + 89.999999 + + + + + 2020-09-22T11:20:00Z,2020-09-22T13:20:00Z,2020-09-22T15:20:00Z + 2020-09-22T11:20:00Z,2020-09-22T13:20:00Z,2020-09-22T15:20:00Z + + + + + +Known limitations +================= + +- some WMS time-enabled servers provide dimension support for real-time + data with ongoing updates to retention time. In this case, a given + WMS layer's temporal extent may be updated a few hours after, for + example. It is up to the MapProxy configuration to manage dimensions/ + extents accordingly. This can be done with custom scripts + to run WMS ``GetCapabilities`` requests and write the updated temporal + dimensions into the MapProxy configuration. An example of such a tool + is `geomet-mapproxy`_ +- caches of layers with dimensions need to be cleaned/deleted by the MapProxy + administrator. This can typically be done via cron/schedule accordingly +- dimemsion support is only implemented in the default file cache backend + at this time + + +Tests +===== + +All tests related to caching layer dimensions: ``mapproxy/test/system/test_dimensions.py`` + +.. _`OGC OGC Best Practice for using Web Map Services (WMS) with Time-Dependent or Elevation-Dependent Data`: https://portal.ogc.org/files/?artifact_id=56394 +.. _`geomet-mapproxy`: https://github.com/ECCC-MSC/geomet-mapproxy diff -Nru mapproxy-1.14.0/doc/conf.py mapproxy-1.15.1/doc/conf.py --- mapproxy-1.14.0/doc/conf.py 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/doc/conf.py 2022-06-14 12:39:29.000000000 +0000 @@ -49,9 +49,9 @@ # built documents. # # The short X.Y version. -version = '1.14' +version = '1.15' # The full version, including alpha/beta/rc tags. -release = '1.14.0' +release = '1.15.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff -Nru mapproxy-1.14.0/doc/index.rst mapproxy-1.15.1/doc/index.rst --- mapproxy-1.14.0/doc/index.rst 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/doc/index.rst 2022-06-14 12:39:29.000000000 +0000 @@ -23,6 +23,7 @@ auth decorate_img development + plugins mapproxy_2 .. todolist:: diff -Nru mapproxy-1.14.0/doc/plugins.rst mapproxy-1.15.1/doc/plugins.rst --- mapproxy-1.14.0/doc/plugins.rst 1970-01-01 00:00:00.000000000 +0000 +++ mapproxy-1.15.1/doc/plugins.rst 2022-06-14 12:39:29.000000000 +0000 @@ -0,0 +1,303 @@ +Writing plugins +=============== + +Since MapProxy 1.15, it is possible to write plugins for MapProxy that can +add new sources, services or commands. This requires Python >= 3.7 + +Example +------- + +The mapproxy_hips plugin at https://github.com/rouault/mapproxy_hips is an +example of a plugin, which adds a new source, service and customizes the demo +service, demonstrating all the below points. + +How to add a plugin ? +--------------------- + +A plugin should be written as a Python package whose setuptools ``setup()`` +method has a ``entry_points`` keyword with a group ``mapproxy`` pointing to +a module with a ``plugin_entrypoint`` method. + +.. code-block:: python + + entry_points={"mapproxy": ["hips = mapproxy_hips.pluginmodule"]}, + + +In this example, the ``mapproxy_hips/pluginmodule.py`` file should have +a ``plugin_entrypoint`` method taking no argument and returning nothing. + +.. code-block:: python + + def plugin_entrypoint(): + # call different registration methods, like register_service_configuration(), + # register_source_configuration() + pass + +That method is in charge of registering the various registration methods +detailed hereafter. + +Plugins will often by dependent on MapProxy internal classes. It is their +responsibility to check the MapProxy version, in case the MapProxy internal +API or behavior would change and make them incompatible. + +Adding a new service +-------------------- + +The ``mapproxy.config.loader`` module has a ``register_service_configuration()`` +method to register a new service and specify the allowed keywords for it in +the YAML configuration file. + +.. code-block:: python + + def register_service_configuration(service_name, service_creator, + yaml_spec_service_name = None, yaml_spec_service_def = None): + """ Method used by plugins to register a new service. + + :param config_name: Name of the service + :type config_name: str + :param service_creator: Creator method of the service + :type service_creator: method of type (serviceConfiguration: ServiceConfiguration, conf: dict) -> Server + :param yaml_spec_service_name: Name of the service in the YAML configuration file + :type yaml_spec_service_name: str + :param yaml_spec_service_def: Definition of the service in the YAML configuration file + :type yaml_spec_service_def: dict + """ + + +This can for example by used like the following snippet: + +.. code-block:: python + + from mapproxy.config.loader import register_service_configuration + from mapproxy.service.base import Server + + class MyExtraServiceServer(Server): + # Look at classes at https://github.com/mapproxy/mapproxy/tree/master/mapproxy/service + # for a real implementation + names = ('my_extra_service',) + def __init__(self): + pass + + def my_extra_service_method(serviceConfiguration, conf): + return MyExtraServiceServer() + + register_service_configuration('my_extra_service', my_extra_service_method, + 'my_extra_service', {'foo': str()}) + + +This allows the following declaration in the YAML mapproxy configuration file: + +.. code-block:: yaml + + services: + my_extra_service: + foo: bar + +A real-world implementation can be found at https://github.com/rouault/mapproxy_hips/blob/master/mapproxy_hips/service/hips.py + + +Customizing layer metadata in YAML configuration file +----------------------------------------------------- + +When implementing a new service, it might be useful to add per-layer metadata +for it. The YAML validator needs to be updated to recognize the new keywords. +The ``add_subcategory_to_layer_md()`` method of the ``mapproxy.config.spec`` module +can be used to do that. + +.. code-block:: python + + + def add_subcategory_to_layer_md(category_name, category_def): + """ Add a new category to wms_130_layer_md. + Used by plugins + """ + +This can for example be used like in the following snippet: + +.. code-block:: python + + from mapproxy.config.spec import add_subcategory_to_layer_md + + # Add a 'hips' subcategory to layer spec to be able to define hips service + # specific layer metadata + add_subcategory_to_layer_md('hips', anything()) + + +Adding a new source +------------------- + +The ``mapproxy.config.loader`` module has a ``register_source_configuration()`` +method to register a new source and specify the allowed keywords for it in +the YAML configuration file. + +.. code-block:: python + + + def register_source_configuration(config_name, config_class, + yaml_spec_source_name = None, yaml_spec_source_def = None): + """ Method used by plugins to register a new source configuration. + + :param config_name: Name of the source configuration + :type config_name: str + :param config_class: Class of the source configuration + :type config_name: SourceConfiguration + :param yaml_spec_source_name: Name of the source in the YAML configuration file + :type yaml_spec_source_name: str + :param yaml_spec_source_def: Definition of the source in the YAML configuration file + :type yaml_spec_source_def: dict + """ + + +This can for example by used like the following snippet: + +.. code-block:: python + + from mapproxy.config.loader import register_source_configuration + from mapproxy.config.loader import SourceConfiguration + + class my_source_configuration(SourceConfiguration): + source_type = ('my_extra_source',) + + def source(self, params=None): + # Look at classes at https://github.com/mapproxy/mapproxy/tree/master/mapproxy/source + # for a real implementation + class MySource(object): + def __init__(self): + self.extent = None + return MySource() + + register_source_configuration('my_extra_source', my_source_configuration, + 'my_extra_source', {'foo': str()}) + + +This allows the following declaration in the YAML mapproxy configuration file: + +.. code-block:: yaml + + sources: + some_source_name: + type: my_extra_source + foo: bar + +A real-world implementation can be found at https://github.com/rouault/mapproxy_hips/blob/master/mapproxy_hips/source/hips.py + +Customizing the demo service +---------------------------- + +The :ref:`demo_service_label` can be customized in two ways: + +- Customizing the output of the ``/demo`` HTML output, typically by adding entries + for new services. This is done with the ``register_extra_demo_substitution_handler()`` + method of the ``mapproxy.service.demo`` module. + + .. code-block:: python + + def register_extra_demo_substitution_handler(handler): + """ Method used by plugins to register a new handler for doing substitutions + to the HTML template used by the demo service. + The handler passed to this method is invoked by the DemoServer._render_template() + method. The handler may modify the passed substitutions dictionary + argument. Keys of particular interest are 'extra_services_html_beginning' + and 'extra_services_html_end' to add HTML content before/after built-in + services. + + :param handler: New handler for incoming requests + :type handler: function that takes 3 arguments(DemoServer instance, req and a substitutions dictionary argument). + """ + +- Handling new request paths under the ``/demo/`` hierarchy, typically to implement a new + service. This is done with the ``register_extra_demo_server_handler()`` + method of the ``mapproxy.service.demo`` module. + + .. code-block:: python + + def register_extra_demo_server_handler(handler): + """ Method used by plugins to register a new handler for the demo service. + The handler passed to this method is invoked by the DemoServer.handle() + method when receiving an incoming request. This enables handlers to + process it, in case it is relevant to it. + + :param handler: New handler for incoming requests + :type handler: function that takes 2 arguments (DemoServer instance and req) and + returns a string with HTML content or None + """ + +This can for example be used like in the following snippet: + +.. code-block:: python + + from mapproxy.service.demo import register_extra_demo_server_handler, register_extra_demo_substitution_handler + + def demo_server_handler(demo_server, req): + if 'my_service' in req.args: + return 'my_return' + return None + + def demo_substitution_handler(demo_server, req, substitutions): + html = '

My extra service

' + html += 'My service' + substitutions['extra_services_html_beginning'] += html + + register_extra_demo_server_handler(demo_server_handler) + register_extra_demo_substitution_handler(demo_substitution_handler) + + +A real-world example can be found at https://github.com/rouault/mapproxy_hips/blob/master/mapproxy_hips/service/demo_extra.py + + +Adding new commands to mapproxy-util +------------------------------------ + +New commands can be added to :ref:`mapproxy-util` by using the +``register_command()`` method of the ``mapproxy.script.util`` module + +.. code-block:: python + + def register_command(command_name, command_spec): + """ Method used by plugins to register a command. + + :param command_name: Name of the command + :type command_name: str + :param command_spec: Definition of the command. Dictionary with a 'func' and 'help' member + :type command_spec: dict + """ + +This can for example be used like in the following snippet: + +.. code-block:: python + + import optparse + from mapproxy.script.util import register_command + + def my_command(args=None): + parser = optparse.OptionParser("%prog my_command [options] -f mapproxy_conf -l layer") + parser.add_option("-f", "--mapproxy-conf", dest="mapproxy_conf", + help="MapProxy configuration.") + parser.add_option("-l", "--layer", dest="layer", help="Layer") + + if args: + args = args[1:] # remove script name + + (options, args) = parser.parse_args(args) + if not options.mapproxy_conf or not options.layer: + parser.print_help() + sys.exit(1) + + # Do something + + + register_command('my_command', { + 'func': my_command, + 'help': 'Do something.' + }) + + +A real-world example can be found at +https://github.com/rouault/mapproxy_hips/blob/master/mapproxy_hips/script/hipsallsky.py +` + +Credits +------- + +The development of the plugin mechanism has been funded by +Centre National d'Etudes Spatiales (CNES): https://cnes.fr diff -Nru mapproxy-1.14.0/.github/workflows/ghpages.yml mapproxy-1.15.1/.github/workflows/ghpages.yml --- mapproxy-1.14.0/.github/workflows/ghpages.yml 1970-01-01 00:00:00.000000000 +0000 +++ mapproxy-1.15.1/.github/workflows/ghpages.yml 2022-06-14 12:39:29.000000000 +0000 @@ -0,0 +1,31 @@ +name: Build and deploy documentation on github pages + +on: + workflow_dispatch: + push: + tags: + - '*' + +jobs: + build: + runs-on: ubuntu-20.04 + + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install dependencies ⏬ + run: pip install sphinx sphinx-bootstrap-theme + + - name: Run documentation build and publish 🏗️ + run: | + git config --global user.name 'ghpages' + git config --global user.email 'ghpages@users.noreply.github.com' + git fetch --all + git checkout gh-pages + git pull origin gh-pages + git rebase origin/master + sphinx-build doc/ docs + git add docs + git commit -m "Automated documentation" + git push -f origin gh-pages diff -Nru mapproxy-1.14.0/.github/workflows/test.yml mapproxy-1.15.1/.github/workflows/test.yml --- mapproxy-1.14.0/.github/workflows/test.yml 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/.github/workflows/test.yml 2022-06-14 12:39:29.000000000 +0000 @@ -11,7 +11,7 @@ jobs: test: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 services: redis-server: image: redis @@ -29,7 +29,7 @@ strategy: matrix: - python-version: [2.7, 3.6, 3.7, 3.8, 3.9] + python-version: [2.7, 3.6, 3.7, 3.8, 3.9, "3.10"] env: MAPPROXY_TEST_COUCHDB: 'http://localhost:5984' @@ -70,4 +70,6 @@ pip freeze - name: Run tests 🏗️ - run: pytest mapproxy + run: | + export LD_PRELOAD=/lib/x86_64-linux-gnu/libstdc++.so.6:$LD_PRELOAD + pytest mapproxy diff -Nru mapproxy-1.14.0/mapproxy/cache/base.py mapproxy-1.15.1/mapproxy/cache/base.py --- mapproxy-1.14.0/mapproxy/cache/base.py 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/cache/base.py 2022-06-14 12:39:29.000000000 +0000 @@ -42,40 +42,40 @@ supports_timestamp = True - def load_tile(self, tile, with_metadata=False): + def load_tile(self, tile, with_metadata=False, dimensions=None): raise NotImplementedError() - def load_tiles(self, tiles, with_metadata=False): + def load_tiles(self, tiles, with_metadata=False, dimensions=None): all_succeed = True for tile in tiles: - if not self.load_tile(tile, with_metadata=with_metadata): + if not self.load_tile(tile, with_metadata=with_metadata, dimensions=dimensions): all_succeed = False return all_succeed - def store_tile(self, tile): + def store_tile(self, tile, dimensions=None): raise NotImplementedError() - def store_tiles(self, tiles): + def store_tiles(self, tiles, dimensions=None): all_succeed = True for tile in tiles: - if not self.store_tile(tile): + if not self.store_tile(tile, dimensions=dimensions): all_succeed = False return all_succeed - def remove_tile(self, tile): + def remove_tile(self, tile, dimensions=None): raise NotImplementedError() - def remove_tiles(self, tiles): + def remove_tiles(self, tiles, dimensions=None): for tile in tiles: - self.remove_tile(tile) + self.remove_tile(tile, dimensions=dimensions) - def is_cached(self, tile): + def is_cached(self, tile, dimensions=None): """ Return ``True`` if the tile is cached. """ raise NotImplementedError() - def load_tile_metadata(self, tile): + def load_tile_metadata(self, tile, dimensions=None): """ Fill the metadata attributes of `tile`. Sets ``.timestamp`` and ``.size``. diff -Nru mapproxy-1.14.0/mapproxy/cache/compact.py mapproxy-1.15.1/mapproxy/cache/compact.py --- mapproxy-1.14.0/mapproxy/cache/compact.py 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/cache/compact.py 2022-06-14 12:39:29.000000000 +0000 @@ -53,21 +53,21 @@ bundle_fname, offset = self._get_bundle_fname_and_offset(tile_coord) return self.bundle_class(bundle_fname, offset=offset) - def is_cached(self, tile): + def is_cached(self, tile, dimensions=None): if tile.coord is None: return True if tile.source: return True - return self._get_bundle(tile.coord).is_cached(tile) + return self._get_bundle(tile.coord).is_cached(tile, dimensions=dimensions) - def store_tile(self, tile): + def store_tile(self, tile, dimensions=None): if tile.stored: return True - return self._get_bundle(tile.coord).store_tile(tile) + return self._get_bundle(tile.coord).store_tile(tile, dimensions=dimensions) - def store_tiles(self, tiles): + def store_tiles(self, tiles, dimensions=None): if len(tiles) > 1: # Check if all tiles are from a single bundle. bundle_files = set() @@ -78,23 +78,23 @@ bundle_files.add(self._get_bundle_fname_and_offset(t.coord)[0]) tile_coord = t.coord if len(bundle_files) == 1: - return self._get_bundle(tile_coord).store_tiles(tiles) + return self._get_bundle(tile_coord).store_tiles(tiles, dimensions=dimensions) # Tiles are across multiple bundles failed = False for tile in tiles: - if not self.store_tile(tile): + if not self.store_tile(tile, dimensions=dimensions): failed = True return not failed - def load_tile(self, tile, with_metadata=False): + def load_tile(self, tile, with_metadata=False, dimensions=None): if tile.source or tile.coord is None: return True - return self._get_bundle(tile.coord).load_tile(tile) + return self._get_bundle(tile.coord).load_tile(tile, dimensions=dimensions) - def load_tiles(self, tiles, with_metadata=False): + def load_tiles(self, tiles, with_metadata=False, dimensions=None): if len(tiles) > 1: # Check if all tiles are from a single bundle. bundle_files = set() @@ -105,23 +105,23 @@ bundle_files.add(self._get_bundle_fname_and_offset(t.coord)[0]) tile_coord = t.coord if len(bundle_files) == 1: - return self._get_bundle(tile_coord).load_tiles(tiles) + return self._get_bundle(tile_coord).load_tiles(tiles, dimensions=dimensions) # No support_bulk_load or tiles are across multiple bundles missing = False for tile in tiles: - if not self.load_tile(tile, with_metadata=with_metadata): + if not self.load_tile(tile, with_metadata=with_metadata, dimensions=dimensions): missing = True return not missing - def remove_tile(self, tile): + def remove_tile(self, tile, dimensions=None): if tile.coord is None: return True - return self._get_bundle(tile.coord).remove_tile(tile) + return self._get_bundle(tile.coord).remove_tile(tile, dimensions=dimensions) - def load_tile_metadata(self, tile): - if self.load_tile(tile): + def load_tile_metadata(self, tile, dimensions=None): + if self.load_tile(tile, dimensions=dimensions): tile.timestamp = -1 def remove_level_tiles_before(self, level, timestamp): @@ -152,7 +152,7 @@ def index(self): return BundleIndexV1(self.base_filename + BUNDLEX_V1_EXT) - def is_cached(self, tile): + def is_cached(self, tile, dimensions=None): if tile.source or tile.coord is None: return True @@ -168,12 +168,12 @@ size = bundle.read_size(offset) return size != 0 - def store_tile(self, tile): + def store_tile(self, tile, dimensions=None): if tile.stored: return True - return self.store_tiles([tile]) + return self.store_tiles([tile], dimensions=dimensions) - def store_tiles(self, tiles): + def store_tiles(self, tiles, dimensions=None): tiles_data = [] for t in tiles: if t.stored: @@ -194,12 +194,12 @@ return True - def load_tile(self, tile, with_metadata=False): + def load_tile(self, tile, with_metadata=False, dimensions=None): if tile.source or tile.coord is None: return True - return self.load_tiles([tile], with_metadata) + return self.load_tiles([tile], with_metadata, dimensions=dimensions) - def load_tiles(self, tiles, with_metadata=False): + def load_tiles(self, tiles, with_metadata=False, dimensions=None): missing = False with self.index().readonly() as idx: @@ -223,7 +223,7 @@ return not missing - def remove_tile(self, tile): + def remove_tile(self, tile, dimensions=None): if tile.coord is None: return True @@ -501,7 +501,7 @@ offset = val - (size << 40) return offset, size - def _load_tile(self, fh, tile): + def _load_tile(self, fh, tile, dimensions=None): if tile.source or tile.coord is None: return True @@ -516,13 +516,13 @@ tile.source = ImageSource(BytesIO(data)) return True - def load_tile(self, tile, with_metadata=False): + def load_tile(self, tile, with_metadata=False, dimensions=None): if tile.source or tile.coord is None: return True - return self.load_tiles([tile], with_metadata) + return self.load_tiles([tile], with_metadata, dimensions=dimensions) - def load_tiles(self, tiles, with_metadata=False): + def load_tiles(self, tiles, with_metadata=False, dimensions=None): missing = False with self._readonly() as fh: @@ -537,7 +537,7 @@ return not missing - def is_cached(self, tile): + def is_cached(self, tile, dimensions=None): with self._readonly() as fh: if not fh: return False @@ -576,7 +576,7 @@ fh.seek(24) fh.write(struct.pack("0: + items = list(dimensions.keys()) + items.sort() + dimensions_str = ['{key}-{value}'.format(key=i, value=dimensions[i].replace('/', '_')) for i in items] + cache_dir = os.path.join(self.cache_dir, '_'.join(dimensions_str)) + return self._tile_location(tile, self.cache_dir, self.file_ext, create_dir=create_dir, dimensions=dimensions) - def level_location(self, level): + def level_location(self, level, dimensions=None): """ Return the path where all tiles for `level` will be stored. @@ -56,7 +61,7 @@ >>> c.level_location(2) '/tmp/cache/02' """ - return self._level_location(level, self.cache_dir) + return self._level_location(level, self.cache_dir, dimensions) def _single_color_tile_location(self, color, create_dir=False): """ @@ -74,8 +79,8 @@ ensure_directory(location) return location - def load_tile_metadata(self, tile): - location = self.tile_location(tile) + def load_tile_metadata(self, tile, dimensions=None): + location = self.tile_location(tile, dimensions=dimensions) try: stats = os.lstat(location) tile.timestamp = stats.st_mtime @@ -85,12 +90,12 @@ tile.timestamp = 0 tile.size = 0 - def is_cached(self, tile): + def is_cached(self, tile, dimensions=None): """ Returns ``True`` if the tile data is present. """ if tile.is_missing(): - location = self.tile_location(tile) + location = self.tile_location(tile, dimensions=dimensions) if os.path.exists(location): return True else: @@ -98,7 +103,7 @@ else: return True - def load_tile(self, tile, with_metadata=False): + def load_tile(self, tile, with_metadata=False, dimensions=None): """ Fills the `Tile.source` of the `tile` if it is cached. If it is not cached or if the ``.coord`` is ``None``, nothing happens. @@ -106,23 +111,23 @@ if not tile.is_missing(): return True - location = self.tile_location(tile) + location = self.tile_location(tile, dimensions=dimensions) if os.path.exists(location): if with_metadata: - self.load_tile_metadata(tile) + self.load_tile_metadata(tile, dimensions=dimensions) tile.source = ImageSource(location) return True return False - def remove_tile(self, tile): - location = self.tile_location(tile) + def remove_tile(self, tile, dimensions=None): + location = self.tile_location(tile, dimensions=dimensions) try: os.remove(location) except OSError as ex: if ex.errno != errno.ENOENT: raise - def store_tile(self, tile): + def store_tile(self, tile, dimensions=None): """ Add the given `tile` to the file cache. Stores the `Tile.source` to `FileCache.tile_location`. @@ -130,7 +135,7 @@ if tile.stored: return - tile_loc = self.tile_location(tile, create_dir=True) + tile_loc = self.tile_location(tile, create_dir=True, dimensions=dimensions) if self.link_single_color_images: color = is_single_color_image(tile.source.as_image()) diff -Nru mapproxy-1.14.0/mapproxy/cache/geopackage.py mapproxy-1.15.1/mapproxy/cache/geopackage.py --- mapproxy-1.14.0/mapproxy/cache/geopackage.py 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/cache/geopackage.py 2022-06-14 12:39:29.000000000 +0000 @@ -355,21 +355,21 @@ db.commit() db.close() - def is_cached(self, tile): + def is_cached(self, tile, dimensions=None): if tile.coord is None: return True if tile.source: return True - return self.load_tile(tile) + return self.load_tile(tile, dimensions=dimensions) - def store_tile(self, tile): + def store_tile(self, tile, dimensions=None): if tile.stored: return True return self._store_bulk([tile]) - def store_tiles(self, tiles): + def store_tiles(self, tiles, dimensions=None): tiles = [t for t in tiles if not t.stored] return self._store_bulk(tiles) @@ -399,7 +399,7 @@ return False return True - def load_tile(self, tile, with_metadata=False): + def load_tile(self, tile, with_metadata=False, dimensions=None): if tile.source or tile.coord is None: return True @@ -416,7 +416,7 @@ else: return False - def load_tiles(self, tiles, with_metadata=False): + def load_tiles(self, tiles, with_metadata=False, dimensions=None): # associate the right tiles with the cursor tile_dict = {} coords = [] @@ -459,7 +459,7 @@ return loaded_tiles == len(tile_dict) - def remove_tile(self, tile): + def remove_tile(self, tile, dimensions=None): cursor = self.db.cursor() cursor.execute( "DELETE FROM [{0}] WHERE (tile_column = ? AND tile_row = ? AND zoom_level = ?)".format(self.table_name), @@ -480,13 +480,13 @@ return True return False - def load_tile_metadata(self, tile): + def load_tile_metadata(self, tile, dimensions=None): if not self.supports_timestamp: # GPKG specification does not include tile timestamps. # This sets the timestamp of the tile to epoch (1970s) tile.timestamp = -1 else: - self.load_tile(tile) + self.load_tile(tile, dimensions=dimensions) class GeopackageLevelCache(TileCacheBase): @@ -527,35 +527,35 @@ for gp in self._geopackage.values(): gp.cleanup() - def is_cached(self, tile): + def is_cached(self, tile, dimensions=None): if tile.coord is None: return True if tile.source: return True - return self._get_level(tile.coord[2]).is_cached(tile) + return self._get_level(tile.coord[2]).is_cached(tile, dimensions=dimensions) - def store_tile(self, tile): + def store_tile(self, tile, dimensions=None): if tile.stored: return True - return self._get_level(tile.coord[2]).store_tile(tile) + return self._get_level(tile.coord[2]).store_tile(tile, dimensions=dimensions) - def store_tiles(self, tiles): + def store_tiles(self, tiles, dimensions=None): failed = False for level, tiles in itertools.groupby(tiles, key=lambda t: t.coord[2]): tiles = [t for t in tiles if not t.stored] - res = self._get_level(level).store_tiles(tiles) + res = self._get_level(level).store_tiles(tiles, dimensions=dimensions) if not res: failed = True return failed - def load_tile(self, tile, with_metadata=False): + def load_tile(self, tile, with_metadata=False,dimensions=None): if tile.source or tile.coord is None: return True - return self._get_level(tile.coord[2]).load_tile(tile, with_metadata=with_metadata) + return self._get_level(tile.coord[2]).load_tile(tile, with_metadata=with_metadata, dimensions=dimensions) - def load_tiles(self, tiles, with_metadata=False): + def load_tiles(self, tiles, with_metadata=False, dimensions=None): level = None for tile in tiles: if tile.source or tile.coord is None: @@ -566,13 +566,13 @@ if not level: return True - return self._get_level(level).load_tiles(tiles, with_metadata=with_metadata) + return self._get_level(level).load_tiles(tiles, with_metadata=with_metadata, dimensions=dimensions) - def remove_tile(self, tile): + def remove_tile(self, tile, dimensions=None): if tile.coord is None: return True - return self._get_level(tile.coord[2]).remove_tile(tile) + return self._get_level(tile.coord[2]).remove_tile(tile, dimensions=dimensions) def remove_level_tiles_before(self, level, timestamp): level_cache = self._get_level(level) diff -Nru mapproxy-1.14.0/mapproxy/cache/mbtiles.py mapproxy-1.15.1/mapproxy/cache/mbtiles.py --- mapproxy-1.14.0/mapproxy/cache/mbtiles.py 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/cache/mbtiles.py 2022-06-14 12:39:29.000000000 +0000 @@ -134,20 +134,20 @@ db.commit() db.close() - def is_cached(self, tile): + def is_cached(self, tile, dimensions=None): if tile.coord is None: return True if tile.source: return True - return self.load_tile(tile) + return self.load_tile(tile, dimensions=dimensions) - def store_tile(self, tile): + def store_tile(self, tile, dimensions=None): if tile.stored: return True return self._store_bulk([tile]) - def store_tiles(self, tiles): + def store_tiles(self, tiles, dimensions=None): tiles = [t for t in tiles if not t.stored] return self._store_bulk(tiles) @@ -182,7 +182,7 @@ return False return True - def load_tile(self, tile, with_metadata=False): + def load_tile(self, tile, with_metadata=False, dimensions=None): if tile.source or tile.coord is None: return True @@ -208,7 +208,7 @@ else: return False - def load_tiles(self, tiles, with_metadata=False): + def load_tiles(self, tiles, with_metadata=False, dimensions=None): #associate the right tiles with the cursor tile_dict = {} coords = [] @@ -287,13 +287,13 @@ return True return False - def load_tile_metadata(self, tile): + def load_tile_metadata(self, tile, dimensions=None): 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) + self.load_tile(tile, dimensions=dimensions) class MBTilesLevelCache(TileCacheBase): supports_timestamp = True @@ -330,25 +330,25 @@ for mbtile in self._mbtiles.values(): mbtile.cleanup() - def is_cached(self, tile): + def is_cached(self, tile, dimensions=None): if tile.coord is None: return True if tile.source: return True - return self._get_level(tile.coord[2]).is_cached(tile) + return self._get_level(tile.coord[2]).is_cached(tile, dimensions=dimensions) - def store_tile(self, tile): + def store_tile(self, tile, dimensions=None): if tile.stored: return True - return self._get_level(tile.coord[2]).store_tile(tile) + return self._get_level(tile.coord[2]).store_tile(tile, dimensions=dimensions) - def store_tiles(self, tiles): + def store_tiles(self, tiles, dimensions): failed = False for level, tiles in itertools.groupby(tiles, key=lambda t: t.coord[2]): tiles = [t for t in tiles if not t.stored] - res = self._get_level(level).store_tiles(tiles) + res = self._get_level(level).store_tiles(tiles, dimensions=dimensions) if not res: failed = True return failed @@ -358,7 +358,7 @@ return self._get_level(tile.coord[2]).load_tile(tile, with_metadata=with_metadata) - def load_tiles(self, tiles, with_metadata=False): + def load_tiles(self, tiles, with_metadata=False, dimensions=None): level = None for tile in tiles: if tile.source or tile.coord is None: @@ -369,7 +369,7 @@ if not level: return True - return self._get_level(level).load_tiles(tiles, with_metadata=with_metadata) + return self._get_level(level).load_tiles(tiles, with_metadata=with_metadata, dimensions=dimensions) def remove_tile(self, tile): if tile.coord is None: @@ -377,8 +377,8 @@ return self._get_level(tile.coord[2]).remove_tile(tile) - def load_tile_metadata(self, tile): - self.load_tile(tile) + def load_tile_metadata(self, tile, dimensions): + self.load_tile(tile, dimensions=dimensions) def remove_level_tiles_before(self, level, timestamp): level_cache = self._get_level(level) diff -Nru mapproxy-1.14.0/mapproxy/cache/path.py mapproxy-1.15.1/mapproxy/cache/path.py --- mapproxy-1.14.0/mapproxy/cache/path.py 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/cache/path.py 2022-06-14 12:39:29.000000000 +0000 @@ -16,7 +16,7 @@ import os from mapproxy.compat import string_type from mapproxy.util.fs import ensure_directory - +from mapproxy.request.base import NoCaseMultiDict def location_funcs(layout): if layout == 'tc': @@ -34,18 +34,39 @@ else: raise ValueError('unknown directory_layout "%s"' % layout) -def level_location(level, cache_dir): +def level_location(level, cache_dir, dimensions=None): """ Return the path where all tiles for `level` will be stored. >>> os.path.abspath(level_location(2, '/tmp/cache')) == os.path.abspath('/tmp/cache/02') True """ + dim_path = dimensions_part(dimensions) + if isinstance(level, string_type): - return os.path.join(cache_dir, level) + return os.path.join(cache_dir, dim_path, level) else: - return os.path.join(cache_dir, "%02d" % level) + return os.path.join(cache_dir, dim_path, "%02d" % level) +def dimensions_part(dimensions): + """ + Return the subpath where all tiles for `dimensions` will be stored. + Dimensions prefixed with "dim_" are sorted after the predefined elevation and time dimensions + >>> dimensions_part({'time': '2020-08-25T00:00:00Z'}) + 'time-2020-08-25T00:00:00Z' + >>> dimensions_part({'time': '2020-08-25T00:00:00Z', 'dim_reference_time': '2020-08-25T00:00:00Z', 'dim_level': '700'}) + 'time-2020-08-25T00:00:00Z/dim_level-700/dim_reference_time-2020-08-25T00:00:00Z' + + """ + if dimensions: + dims = NoCaseMultiDict(dimensions) + predefined_dims, custom_dims = [], [] + for dim in dims.keys(): + (custom_dims if dim.startswith('dim_') else predefined_dims).append(dim) + dim_keys = sorted(predefined_dims) + sorted(custom_dims) + return os.path.join(*(map(lambda k: k + "-" + str(dims.get(k, 'default')), dim_keys))) + else: + return "" def level_part(level): """ @@ -62,7 +83,7 @@ return "%02d" % level -def tile_location_tc(tile, cache_dir, file_ext, create_dir=False): +def tile_location_tc(tile, cache_dir, file_ext, create_dir=False, dimensions=None): """ Return the location of the `tile`. Caches the result as ``location`` property of the `tile`. @@ -77,7 +98,9 @@ """ if tile.location is None: x, y, z = tile.coord + parts = (cache_dir, + dimensions_part(dimensions), level_part(z), "%03d" % int(x / 1000000), "%03d" % (int(x / 1000) % 1000), @@ -90,7 +113,7 @@ ensure_directory(tile.location) return tile.location -def tile_location_mp(tile, cache_dir, file_ext, create_dir=False): +def tile_location_mp(tile, cache_dir, file_ext, create_dir=False, dimensions=None): """ Return the location of the `tile`. Caches the result as ``location`` property of the `tile`. @@ -108,6 +131,7 @@ if tile.location is None: x, y, z = tile.coord parts = (cache_dir, + dimensions_part(dimensions), level_part(z), "%04d" % int(x / 10000), "%04d" % (int(x) % 10000), @@ -118,7 +142,7 @@ ensure_directory(tile.location) return tile.location -def tile_location_tms(tile, cache_dir, file_ext, create_dir=False): +def tile_location_tms(tile, cache_dir, file_ext, create_dir=False, dimensions=None): """ Return the location of the `tile`. Caches the result as ``location`` property of the `tile`. @@ -134,14 +158,14 @@ if tile.location is None: x, y, z = tile.coord tile.location = os.path.join( - cache_dir, level_part(str(z)), + cache_dir,dimensions_part(dimensions) ,level_part(str(z)), str(x), str(y) + '.' + file_ext ) if create_dir: ensure_directory(tile.location) return tile.location -def tile_location_reverse_tms(tile, cache_dir, file_ext, create_dir=False): +def tile_location_reverse_tms(tile, cache_dir, file_ext, create_dir=False, dimensions=None): """ Return the location of the `tile`. Caches the result as ``location`` property of the `tile`. @@ -157,16 +181,16 @@ if tile.location is None: x, y, z = tile.coord tile.location = os.path.join( - cache_dir, str(y), str(x), str(z) + '.' + file_ext + cache_dir,dimensions_part(dimensions),str(y), str(x), str(z) + '.' + file_ext ) if create_dir: ensure_directory(tile.location) return tile.location -def level_location_tms(level, cache_dir): +def level_location_tms(level, cache_dir, dimensions=None): return level_location(str(level), cache_dir=cache_dir) -def tile_location_quadkey(tile, cache_dir, file_ext, create_dir=False): +def tile_location_quadkey(tile, cache_dir, file_ext, create_dir=False, dimensions=None): """ Return the location of the `tile`. Caches the result as ``location`` property of the `tile`. @@ -197,11 +221,11 @@ ensure_directory(tile.location) return tile.location -def no_level_location(level, cache_dir): +def no_level_location(level, cache_dir, dimensions=None): # dummy for quadkey cache which stores all tiles in one directory raise NotImplementedError('cache does not have any level location') -def tile_location_arcgiscache(tile, cache_dir, file_ext, create_dir=False): +def tile_location_arcgiscache(tile, cache_dir, file_ext, create_dir=False, dimensions=None): """ Return the location of the `tile`. Caches the result as ``location`` property of the `tile`. @@ -222,5 +246,5 @@ ensure_directory(tile.location) return tile.location -def level_location_arcgiscache(z, cache_dir): - return level_location('L%02d' % z, cache_dir=cache_dir) \ No newline at end of file +def level_location_arcgiscache(z, cache_dir, dimensions=None): + return level_location('L%02d' % z, cache_dir=cache_dir, dimensions=dimensions) diff -Nru mapproxy-1.14.0/mapproxy/cache/redis.py mapproxy-1.15.1/mapproxy/cache/redis.py --- mapproxy-1.14.0/mapproxy/cache/redis.py 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/cache/redis.py 2022-06-14 12:39:29.000000000 +0000 @@ -48,13 +48,13 @@ x, y, z = tile.coord return self.prefix + '-%d-%d-%d' % (z, x, y) - def is_cached(self, tile): + def is_cached(self, tile, dimensions=None): if tile.coord is None or tile.source: return True return self.r.exists(self._key(tile)) - def store_tile(self, tile): + def store_tile(self, tile, dimensions=None): if tile.stored: return True @@ -69,7 +69,7 @@ self.r.pexpire(key, int(self.ttl * 1000)) return r - def load_tile(self, tile, with_metadata=False): + def load_tile(self, tile, with_metadata=False, dimensions=None): if tile.source or tile.coord is None: return True key = self._key(tile) @@ -79,7 +79,7 @@ return True return False - def remove_tile(self, tile): + def remove_tile(self, tile, dimensions=None): if tile.coord is None: return True diff -Nru mapproxy-1.14.0/mapproxy/cache/riak.py mapproxy-1.15.1/mapproxy/cache/riak.py --- mapproxy-1.14.0/mapproxy/cache/riak.py 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/cache/riak.py 2022-06-14 12:39:29.000000000 +0000 @@ -87,7 +87,7 @@ obj.usermeta = {'timestamp': '0'} return 0.0 - def is_cached(self, tile): + def is_cached(self, tile, dimensions=None): return self.load_tile(tile, True) def _store_bulk(self, tiles): @@ -112,24 +112,24 @@ return True - def store_tile(self, tile): + def store_tile(self, tile,dimensions=None): if tile.stored: return True return self._store_bulk([tile]) - def store_tiles(self, tiles): + def store_tiles(self, tiles,dimensions=None): tiles = [t for t in tiles if not t.stored] return self._store_bulk(tiles) - def load_tile_metadata(self, tile): + def load_tile_metadata(self, tile,dimensions=None): if tile.timestamp: return # is_cached loads metadata self.load_tile(tile, True) - def load_tile(self, tile, with_metadata=False): + def load_tile(self, tile, with_metadata=False, dimensions=None): if tile.timestamp is None: tile.timestamp = 0 if tile.source or tile.coord is None: diff -Nru mapproxy-1.14.0/mapproxy/cache/s3.py mapproxy-1.15.1/mapproxy/cache/s3.py --- mapproxy-1.14.0/mapproxy/cache/s3.py 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/cache/s3.py 2022-06-14 12:39:29.000000000 +0000 @@ -91,10 +91,10 @@ except Exception as e: raise S3ConnectionError('Error during connection %s' % e) - def load_tile_metadata(self, tile): + def load_tile_metadata(self, tile, dimensions=None): if tile.timestamp: return - self.is_cached(tile) + self.is_cached(tile, dimensions=dimensions) def _set_metadata(self, response, tile): if 'LastModified' in response: @@ -102,7 +102,7 @@ if 'ContentLength' in response: tile.size = response['ContentLength'] - def is_cached(self, tile): + def is_cached(self, tile, dimensions=None): if tile.is_missing(): key = self.tile_key(tile) try: @@ -115,11 +115,11 @@ return True - def load_tiles(self, tiles, with_metadata=True): + def load_tiles(self, tiles, with_metadata=True, dimensions=None): p = async_.Pool(min(4, len(tiles))) return all(p.map(self.load_tile, tiles)) - def load_tile(self, tile, with_metadata=True): + def load_tile(self, tile, with_metadata=True, dimensions=None): if not tile.is_missing(): return True @@ -143,11 +143,11 @@ log.debug('remove_tile, key: %s' % key) self.conn().delete_object(Bucket=self.bucket_name, Key=key) - def store_tiles(self, tiles): + def store_tiles(self, tiles, dimensions=None): p = async_.Pool(min(self._concurrent_writer, len(tiles))) p.map(self.store_tile, tiles) - def store_tile(self, tile): + def store_tile(self, tile, dimensions=None): if tile.stored: return diff -Nru mapproxy-1.14.0/mapproxy/cache/tile.py mapproxy-1.15.1/mapproxy/cache/tile.py --- mapproxy-1.14.0/mapproxy/cache/tile.py 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/cache/tile.py 2022-06-14 12:39:29.000000000 +0000 @@ -34,8 +34,6 @@ """ - - from functools import partial from contextlib import contextmanager from mapproxy.grid import MetaGrid @@ -48,6 +46,8 @@ from mapproxy.util import async_ from mapproxy.util.py import reraise, reraise_exception import sys +import logging +log = logging.getLogger('mapproxy.cache.tile') class TileManager(object): @@ -66,6 +66,7 @@ bulk_meta_tiles=False, rescale_tiles=0, cache_rescaled_tiles=False, + dimensions=None ): self.grid = grid self.cache = cache @@ -82,6 +83,7 @@ self.pre_store_filter = pre_store_filter or [] self.concurrent_tile_creators = concurrent_tile_creators self.tile_creator_class = tile_creator_class or TileCreator + self.dimensions = dimensions self.rescale_tiles = rescale_tiles self.cache_rescaled_tiles = cache_rescaled_tiles @@ -152,7 +154,7 @@ return tiles def _load_tile_coords(self, tiles, dimensions=None, with_metadata=False, - rescale_till_zoom=None, rescaled_tiles=None, + rescale_till_zoom=None, rescaled_tiles=None ): uncached_tiles = [] @@ -162,7 +164,7 @@ t.source = rescaled_tiles[t.coord].source # load all in batch - self.cache.load_tiles(tiles, with_metadata) + self.cache.load_tiles(tiles, with_metadata, dimensions=dimensions) for tile in tiles: if tile.coord is not None and not self.is_cached(tile, dimensions=dimensions): @@ -201,7 +203,7 @@ tile = Tile(tile) if tile.coord is None: return True - cached = self.cache.is_cached(tile) + cached = self.cache.is_cached(tile, dimensions=dimensions) max_mtime = self.expire_timestamp(tile) if cached and max_mtime is not None: self.cache.load_tile_metadata(tile) @@ -218,9 +220,9 @@ """ if isinstance(tile, tuple): tile = Tile(tile) - if self.cache.is_cached(tile): + if self.cache.is_cached(tile, dimensions=dimensions): # tile exists - if not self.is_cached(tile): + if not self.is_cached(tile, dimensions=dimensions): # expired return True return False @@ -320,11 +322,11 @@ self.dimensions = dimensions self.image_merger = image_merger - def is_cached(self, tile): + def is_cached(self, tile, dimensions=None): """ Return True if the tile is cached. """ - return self.tile_mgr.is_cached(tile) + return self.tile_mgr.is_cached(tile, dimensions=dimensions) def is_stale(self, tile): """ @@ -370,12 +372,13 @@ result.extend(new_tiles) return result - def _create_single_tile(self, tile): + def _create_single_tile(self, tile, dimensions=None): 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): + if not self.is_cached(tile, dimensions=dimensions): + source = None try: source = self._query_sources(query) # if source is not available, try to serve tile in cache @@ -462,18 +465,18 @@ 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): + if not all(self.is_cached(t, dimensions=self.dimensions) 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) + self.cache.store_tiles(splitted_tiles,dimensions=self.dimensions) return splitted_tiles # else tiles = [Tile(coord) for coord in meta_tile.tiles] - self.cache.load_tiles(tiles) + self.cache.load_tiles(tiles, dimensions=self.dimensions) return tiles def _create_bulk_meta_tile(self, meta_tile): @@ -484,7 +487,7 @@ tile_size = self.grid.tile_size main_tile = Tile(meta_tile.main_tile_coord) with self.tile_mgr.lock(main_tile): - if not all(self.is_cached(t) for t in meta_tile.tiles if t is not None): + if not all(self.is_cached(t, dimensions=self.dimensions) for t in meta_tile.tiles if t is not None): async_pool = async_.Pool(self.tile_mgr.concurrent_tile_creators) def query_tile(coord): try: @@ -525,7 +528,7 @@ # else tiles = [Tile(coord) for coord in meta_tile.tiles] - self.cache.load_tiles(tiles) + self.cache.load_tiles(tiles, dimensions=self.dimensions) return tiles diff -Nru mapproxy-1.14.0/mapproxy/client/http.py mapproxy-1.15.1/mapproxy/client/http.py --- mapproxy-1.14.0/mapproxy/client/http.py 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/client/http.py 2022-06-14 12:39:29.000000000 +0000 @@ -181,7 +181,7 @@ self.header_list = headers.items() if headers else [] self.hide_error_details = hide_error_details - def open(self, url, data=None): + def open(self, url, data=None, method=None): code = None result = None try: @@ -191,6 +191,8 @@ reraise_exception(err, sys.exc_info()) for key, value in self.header_list: req.add_header(key, value) + if method: + req.method=method try: start_time = time.time() if self._timeout is not None: diff -Nru mapproxy-1.14.0/mapproxy/client/wms.py mapproxy-1.15.1/mapproxy/client/wms.py --- mapproxy-1.14.0/mapproxy/client/wms.py 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/client/wms.py 2022-06-14 12:39:29.000000000 +0000 @@ -41,6 +41,7 @@ self.fwd_req_params = fwd_req_params or set() def retrieve(self, query, format): + log.debug(query) if self.http_method == 'POST': request_method = 'POST' elif self.http_method == 'GET': @@ -102,7 +103,6 @@ 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 diff -Nru mapproxy-1.14.0/mapproxy/compat/image.py mapproxy-1.15.1/mapproxy/compat/image.py --- mapproxy-1.14.0/mapproxy/compat/image.py 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/compat/image.py 2022-06-14 12:39:29.000000000 +0000 @@ -42,6 +42,9 @@ Image.NEAREST = Image.BILINEAR = Image.BICUBIC = 1 Image.Image = NoPIL ImageColor = NoPIL() + ImageFileDirectory_v2 = NoPIL() + TiffTags = NoPIL() + ImageMath = NoPIL() ImageColor.getrgb = lambda x: x PIL_VERSION = None diff -Nru mapproxy-1.14.0/mapproxy/config/loader.py mapproxy-1.15.1/mapproxy/config/loader.py --- mapproxy-1.14.0/mapproxy/config/loader.py 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/config/loader.py 2022-06-14 12:39:29.000000000 +0000 @@ -30,7 +30,7 @@ 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.config.spec import validate_options, add_source_to_mapproxy_yaml_spec, add_service_to_mapproxy_yaml_spec from mapproxy.util.py import memoize from mapproxy.util.ext.odict import odict from mapproxy.util.yaml import load_yaml_file, YAMLError @@ -84,6 +84,7 @@ def load_sources(self): self.sources = SourcesCollection() for source_name, source_conf in iteritems((self.configuration.get('sources') or {})): + source_conf['name'] = source_name self.sources[source_name] = SourceConfiguration.load(conf=source_conf, context=self) def load_tile_layers(self): @@ -919,7 +920,8 @@ 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']) + mapfile = self.conf['mapfile'] + md5 = hashlib.md5(mapfile.encode('utf-8')) lock_file = os.path.join(lock_dir, md5.hexdigest() + '.lck') lock = lambda: SemLock(lock_file, concurrent_requests) @@ -1018,6 +1020,32 @@ } +def register_source_configuration(config_name, config_class, + yaml_spec_source_name = None, yaml_spec_source_def = None): + """ Method used by plugins to register a new source configuration. + + :param config_name: Name of the source configuration + :type config_name: str + :param config_class: Class of the source configuration + :type config_name: SourceConfiguration + :param yaml_spec_source_name: Name of the source in the YAML configuration file + :type yaml_spec_source_name: str + :param yaml_spec_source_def: Definition of the source in the YAML configuration file + :type yaml_spec_source_def: dict + + Example: + register_source_configuration('hips', HIPSSourceConfiguration, + 'hips', { required('url'): str(), + 'resampling_method': str(), + 'image': image_opts, + }) + """ + log.info('Registering configuration for plugin source %s' % config_name) + source_configuration_types[config_name] = config_class + if yaml_spec_source_name is not None and yaml_spec_source_def is not None: + add_source_to_mapproxy_yaml_spec(yaml_spec_source_name, yaml_spec_source_def) + + class CacheConfiguration(ConfigurationBase): defaults = {'format': 'image/png'} @@ -1067,6 +1095,7 @@ log.warning('link_single_color_images not supported on windows') link_single_color_images = False + return FileCache( cache_dir, file_ext=file_ext, @@ -1562,6 +1591,7 @@ 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, @@ -1717,18 +1747,32 @@ lg_sources.append(lg_source) res_range = resolution_range(self.conf) + dimensions = None + if 'dimensions' in self.conf.keys(): + dimensions = self.dimensions() layer = WMSLayer(self.conf.get('name'), self.conf.get('title'), - sources, fi_sources, lg_sources, res_range=res_range, md=self.conf.get('md')) + sources, fi_sources, lg_sources, res_range=res_range, md=self.conf.get('md'),dimensions=dimensions) return layer @memoize def dimensions(self): from mapproxy.layer import Dimension + from mapproxy.util.ext.wmsparse.util import parse_datetime_range dimensions = {} - for dimension, conf in iteritems(self.conf.get('dimensions', {})): - values = [str(val) for val in conf.get('values', ['default'])] + raw_values = conf.get('values') + if len(raw_values) == 1: + # look for time or dim_reference_time + if 'time' in dimension.lower(): + log.debug('Determining values as datetime strings') + values = parse_datetime_range(raw_values[0]) + else: + log.debug('Determining values as plain strings') + values = raw_values[0].strip().split('/') + else: + 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 @@ -1737,7 +1781,7 @@ def tile_layers(self, grid_name_as_path=False): from mapproxy.service.tile import TileLayer from mapproxy.cache.dummy import DummyCache - + from mapproxy.cache.file import FileCache sources = [] fi_only_sources = [] if 'tile_sources' in self.conf: @@ -1777,9 +1821,14 @@ fi_source = self.context.sources[fi_source_name].fi_source() if fi_source: fi_sources.append(fi_source) - + for grid, extent, cache_source in self.context.caches[cache_name].caches(): - if dimensions and not isinstance(cache_source.cache, DummyCache): + disable_storage = self.context.configuration['caches'][cache_name].get('disable_storage', False) + if disable_storage: + supported_cache_class = DummyCache + else: + supported_cache_class = FileCache + if dimensions and not isinstance(cache_source.cache, supported_cache_class): # caching of dimension layers is not supported yet raise ConfigurationError( "caching of dimension layer (%s) is not supported yet." @@ -1839,6 +1888,28 @@ return extents +plugin_services = {} + +def register_service_configuration(service_name, service_creator, + yaml_spec_service_name = None, yaml_spec_service_def = None): + """ Method used by plugins to register a new service. + + :param service_name: Name of the service + :type service_name: str + :param service_creator: Creator method of the service + :type service_creator: method of type (serviceConfiguration: ServiceConfiguration, conf: dict) -> Server + :param yaml_spec_service_name: Name of the service in the YAML configuration file + :type yaml_spec_service_name: str + :param yaml_spec_service_def: Definition of the service in the YAML configuration file + :type yaml_spec_service_def: dict + """ + + log.info('Registering configuration for plugin service %s' % service_name) + plugin_services[service_name] = service_creator + if yaml_spec_service_name is not None and yaml_spec_service_def is not None: + add_service_to_mapproxy_yaml_spec(yaml_spec_service_name, yaml_spec_service_def) + + class ServiceConfiguration(ConfigurationBase): def __init__(self, conf, context): if 'wms' in conf: @@ -1855,9 +1926,15 @@ 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) + # If not a known service, try to use the plugin mechanism + global plugin_services + creator = plugin_services.get(service_name, None) + if not creator: + raise ValueError('unknown service: %s' % service_name) + new_services = creator(self, service_conf or {}) + else: + new_services = creator(service_conf or {}) - 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] @@ -2059,7 +2136,27 @@ image_formats=image_formats, srs=srs, services=services, restful_template=restful_template) +def load_plugins(): + """ Locate plugins that belong to the 'mapproxy' group and load them """ + if sys.version_info >= (3, 8): + from importlib import metadata as importlib_metadata + else: + try: + import importlib_metadata + except ImportError: + return + + for dist in importlib_metadata.distributions(): + for ep in dist.entry_points: + if ep.group == 'mapproxy': + log.info('Loading plugin from package %s' % dist.metadata['name']) + ep.load().plugin_entrypoint() + + def load_configuration(mapproxy_conf, seed=False, ignore_warnings=True, renderd=False): + + load_plugins() + conf_base_dir = os.path.abspath(os.path.dirname(mapproxy_conf)) # A configuration is checked/validated four times, each step has a different @@ -2074,13 +2171,11 @@ 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.warning(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.warning(error) @@ -2098,9 +2193,7 @@ 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') @@ -2108,9 +2201,8 @@ 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): diff -Nru mapproxy-1.14.0/mapproxy/config/spec.py mapproxy-1.15.1/mapproxy/config/spec.py --- mapproxy-1.14.0/mapproxy/config/spec.py 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/config/spec.py 2022-06-14 12:39:29.000000000 +0000 @@ -604,3 +604,29 @@ # from other sections (e.g. coverages, dimensions, etc.) 'parts': anything(), } + + +def add_source_to_mapproxy_yaml_spec(source_name, source_spec): + """ Add a new source type to mapproxy_yaml_spec. + Used by plugins. + """ + + # sources has a single anything() : {} member + values = list(mapproxy_yaml_spec['sources'].values()) + assert len(values) == 1 + values[0].add_subspec(source_name, source_spec) + + +def add_service_to_mapproxy_yaml_spec(service_name, service_spec): + """ Add a new service type to mapproxy_yaml_spec. + Used by plugins. + """ + + mapproxy_yaml_spec['services'][service_name] = service_spec + + +def add_subcategory_to_layer_md(category_name, category_def): + """ Add a new category to wms_130_layer_md. + Used by plugins + """ + wms_130_layer_md[category_name] = category_def diff -Nru mapproxy-1.14.0/mapproxy/layer.py mapproxy-1.15.1/mapproxy/layer.py --- mapproxy-1.14.0/mapproxy/layer.py 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/layer.py 2022-06-14 12:39:29.000000000 +0000 @@ -136,7 +136,11 @@ 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__ + info = self.__dict__ + serialized_dimensions = ", ".join(["'%s': '%s'" % (key, value) for (key, value) in self.dimensions.items()]) + info["serialized_dimensions"] = serialized_dimensions + return "MapQuery(bbox=%(bbox)s, size=%(size)s, srs=%(srs)r, format=%(format)s, dimensions={%(serialized_dimensions)s)}" % info + class InfoQuery(object): def __init__(self, bbox, size, srs, pos, info_format, format=None, @@ -402,7 +406,7 @@ 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) + src_query = MapQuery(bbox, size, query.srs, query.format, dimensions=query.dimensions) resp = self._image(src_query) result = SubImageSource(resp, size=query.size, offset=offset, image_opts=self.image_opts, cacheable=resp.cacheable) @@ -440,7 +444,7 @@ 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) + tile_collection = self.tile_manager.load_tile_coords(affected_tile_coords, with_metadata=query.tiled_only, dimensions=query.dimensions) if tile_collection.empty: raise BlankImage() diff -Nru mapproxy-1.14.0/mapproxy/request/base.py mapproxy-1.15.1/mapproxy/request/base.py --- mapproxy-1.14.0/mapproxy/request/base.py 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/request/base.py 2022-06-14 12:39:29.000000000 +0000 @@ -402,7 +402,7 @@ """ request_params = RequestParams - def __init__(self, param=None, url='', validate=False, http=None): + def __init__(self, param=None, url='', validate=False, http=None, dimensions=None): self.delimiter = ',' self.http = http diff -Nru mapproxy-1.14.0/mapproxy/request/wms/__init__.py mapproxy-1.15.1/mapproxy/request/wms/__init__.py --- mapproxy-1.14.0/mapproxy/request/wms/__init__.py 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/request/wms/__init__.py 2022-06-14 12:39:29.000000000 +0000 @@ -17,6 +17,7 @@ Service requests (parsing, handling, etc). """ import codecs +import re from mapproxy.request.wms import exception from mapproxy.exception import RequestError from mapproxy.srs import SRS, make_lin_transf @@ -185,10 +186,29 @@ #pylint: disable-msg=E1102 xml_exception_handler = None prevent_image_exception = False + dimension_params = ['time', 'elevation'] + dimension_prefix = 'dim_' def __init__(self, param=None, url='', validate=False, non_strict=False, **kw): + self.dimensions = self._get_dimensions(param) WMSRequest.__init__(self, param=param, url=url, validate=validate, non_strict=non_strict, **kw) + + def _get_dimensions(self, param): + if param: + regex = "(?i)%s%s" % (("^%s|" % self.dimension_prefix if self.dimension_prefix else ""), + "^(%s)$" % "|".join(self.dimension_params)) + keys = [] + if isinstance(param, RequestParams): + keys = list(map (lambda k: k[0], param.iteritems())) + else: + keys = list(param.keys()) + if len(keys) > 0: + return dict(map(lambda k: (k, param.get(k)), filter (lambda k: re.search(regex, k), keys))) + else: + return None + else: + return None def validate(self): self.validate_param() @@ -280,6 +300,8 @@ fixed_params = {'request': 'map', 'wmtver': '1.0.0'} expected_param = ['wmtver', 'request', 'layers', 'styles', 'srs', 'bbox', 'width', 'height', 'format'] + dimension_params = [] + dimension_prefix = '' def adapt_to_111(self): del self.params['wmtver'] self.params['version'] = '1.0.0' diff -Nru mapproxy-1.14.0/mapproxy/script/util.py mapproxy-1.15.1/mapproxy/script/util.py --- mapproxy-1.14.0/mapproxy/script/util.py 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/script/util.py 2022-06-14 12:39:29.000000000 +0000 @@ -25,6 +25,7 @@ import logging from mapproxy.compat import iteritems +from mapproxy.config.loader import load_plugins from mapproxy.script.conf.app import config_command from mapproxy.script.defrag import defrag_command from mapproxy.script.export import export_command @@ -321,6 +322,18 @@ } +def register_command(command_name, command_spec): + """ Method used by plugins to register a command. + + :param command_name: Name of the service + :type command_name: str + :param command_spec: Definition of the command. Dictionary with a 'func' and 'help' member + :type command_spec: dict + """ + + commands[command_name] = command_spec + + class NonStrictOptionParser(optparse.OptionParser): def _process_args(self, largs, rargs, values): while rargs: @@ -361,6 +374,9 @@ print(' %s%s' % (name, help), file=sys.stdout) def main(): + + load_plugins() + parser = NonStrictOptionParser("usage: %prog COMMAND [options]", add_help_option=False) options, args = parser.parse_args() diff -Nru mapproxy-1.14.0/mapproxy/seed/util.py mapproxy-1.15.1/mapproxy/seed/util.py --- mapproxy-1.14.0/mapproxy/seed/util.py 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/seed/util.py 2022-06-14 12:39:29.000000000 +0000 @@ -191,7 +191,7 @@ time.sleep(0.01) except exceptions as ex: if n >= max_repeat: - print("An error occured. Giving up", file=sys.strerr) + print("An error occured. Giving up", file=sys.stderr) raise BackoffError wait_for = start_backoff_sec * 2**n if wait_for > max_backoff: diff -Nru mapproxy-1.14.0/mapproxy/service/demo.py mapproxy-1.15.1/mapproxy/service/demo.py --- mapproxy-1.14.0/mapproxy/service/demo.py 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/service/demo.py 2022-06-14 12:39:29.000000000 +0000 @@ -41,6 +41,40 @@ env = {'bunch': bunch} get_template = template_loader(__name__, 'templates', namespace=env) +# Used by plugins +extra_demo_server_handlers = set() + +def register_extra_demo_server_handler(handler): + """ Method used by plugins to register a new handler for the demo service. + The handler passed to this method is invoked by the DemoServer.handle() + method when receiving an incoming request, and let a chance to the + handler to process it, if it is relevant to it. + + :param handler: New handler for incoming requests + :type handler: function that takes 2 arguments (DemoServer instance and req) and + returns a string with HTML content or None + """ + + extra_demo_server_handlers.add(handler) + + +extra_substitution_handlers = set() + +def register_extra_demo_substitution_handler(handler): + """ Method used by plugins to register a new handler for doing substitutions + to the HTML template used by the demo service. + The handler passed to this method is invoked by the DemoServer._render_template() + method. The handler may modify the passed substitutions dictionary + argument. Keys of particular interest are 'extra_services_html_beginning' + and 'extra_services_html_end' to add HTML content before/after built-in + services. + + :param handler: New handler for incoming requests + :type handler: function that takes 3 arguments(DemoServer instance, req and a substitutions dictionary argument). + """ + + extra_substitution_handlers.add(handler) + def static_filename(name): if base_config().template_dir: @@ -86,6 +120,11 @@ if not authorized: return Response('forbidden', content_type='text/plain', status=403) + for handler in extra_demo_server_handlers: + demo = handler(self, req) + if demo is not None: + return Response(demo, content_type='text/html') + if 'wms_layer' in req.args: demo = self._render_wms_template('demo/wms_demo.html', req) elif 'tms_layer' in req.args: @@ -124,7 +163,7 @@ url = internal_url.replace(req.server_script_url, req.script_url) demo = self._render_capabilities_template('demo/capabilities_demo.html', capabilities, 'TMS', url) elif req.path == '/demo/': - demo = self._render_template('demo/demo.html') + demo = self._render_template(req, 'demo/demo.html') else: resp = Response('', status=301) resp.headers['Location'] = req.script_url.rstrip('/') + '/demo/' @@ -157,26 +196,40 @@ if srs_code not in cached_srs: uncached_srs.append(srs_code) - sorted_cached_srs = sorted(cached_srs, key=lambda srs: get_epsg_num(srs)) - sorted_uncached_srs = sorted(uncached_srs, key=lambda srs: get_epsg_num(srs)) + def get_srs_key(srs): + epsg_num = get_epsg_num(srs) + return str(epsg_num if epsg_num else srs) + + sorted_cached_srs = sorted(cached_srs, key=lambda srs: get_srs_key(srs)) + sorted_uncached_srs = sorted(uncached_srs, key=lambda srs: get_srs_key(srs)) sorted_cached_srs = [(s + '*', s) for s in sorted_cached_srs] sorted_uncached_srs = [(s, s) for s in sorted_uncached_srs] return sorted_cached_srs + sorted_uncached_srs - def _render_template(self, template): + def _render_template(self, req, template): template = get_template(template, default_inherit="demo/static.html") tms_tile_layers = defaultdict(list) for layer in self.tile_layers: name = self.tile_layers[layer].md.get('name') tms_tile_layers[name].append(self.tile_layers[layer]) wmts_layers = tms_tile_layers.copy() - return template.substitute(layers=self.layers, - formats=self.image_formats, - srs=self.srs, - layer_srs=self.layer_srs, - tms_layers=tms_tile_layers, - wmts_layers=wmts_layers, - services=self.services) + + substitutions = dict( + extra_services_html_beginning='', + extra_services_html_end='', + layers=self.layers, + formats=self.image_formats, + srs=self.srs, + layer_srs=self.layer_srs, + tms_layers=tms_tile_layers, + wmts_layers=wmts_layers, + services=self.services + ) + + for add_substitution in extra_substitution_handlers: + add_substitution(self, req, substitutions) + + return template.substitute(substitutions) def _render_wms_template(self, template, req): template = get_template(template, default_inherit="demo/static.html") diff -Nru mapproxy-1.14.0/mapproxy/service/templates/demo/demo.html mapproxy-1.15.1/mapproxy/service/templates/demo/demo.html --- mapproxy-1.14.0/mapproxy/service/templates/demo/demo.html 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/service/templates/demo/demo.html 2022-06-14 12:39:29.000000000 +0000 @@ -35,6 +35,7 @@ {{enddef}}

About

MapProxy Version {{version}}

+ {{ extra_services_html_beginning }}

WMS

{{if 'wms' in services}}
@@ -177,3 +178,4 @@ This service is not available with the current configuration.
{{endif}} + {{ extra_services_html_end }} diff -Nru mapproxy-1.14.0/mapproxy/service/templates/demo/wms_demo.html mapproxy-1.15.1/mapproxy/service/templates/demo/wms_demo.html --- mapproxy-1.14.0/mapproxy/service/templates/demo/wms_demo.html 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/service/templates/demo/wms_demo.html 2022-06-14 12:39:29.000000000 +0000 @@ -26,11 +26,19 @@ var layer = new OpenLayers.Layer.WMS( "WMS {{layer.name}}", "../service?", {layers: "{{layer.name}}", format: "{{format}}", srs:"{{srs}}", + {{if layer.dimensions}}{{for d in layer.dimensions.keys()}}"{{d}}": "{{layer.dimensions[d].default}}",{{endfor}}{{endif}} exceptions: "application/vnd.ogc.se_inimage"{{if format == 'image/png'}}, transparent: true{{endif}}}, {singleTile: true, ratio: 1, isBaseLayer: true} ); map.addLayer(layer); map.zoomToMaxExtent(); + + + } + + function setDimension (layer, dimension, value) { + layer.params[dimension.toUpperCase()] = value; + layer.redraw(); } {{enddef}} @@ -40,6 +48,9 @@ Coordinate System Image format + {{for dim in layer.dimensions}} + {{str(dim)}} + {{endfor}} @@ -64,6 +75,19 @@ {{endfor}} + {{for dim in layer.dimensions}} + + + + {{endfor}} diff -Nru mapproxy-1.14.0/mapproxy/service/templates/wms100capabilities.xml mapproxy-1.15.1/mapproxy/service/templates/wms100capabilities.xml --- mapproxy-1.14.0/mapproxy/service/templates/wms100capabilities.xml 2021-11-24 13:28:24.000000000 +0000 +++ mapproxy-1.15.1/mapproxy/service/templates/wms100capabilities.xml 2022-06-14 12:39:29.000000000 +0000 @@ -78,6 +78,17 @@ {{for srs_code, bbox in layer_srs_bbox(layer)}} {{endfor}} + {{if hasattr(layer, 'dimensions')}} + {{if layer.dimensions}} + {{for d in layer.dimensions}} + {{if "time" in d }} + {{','.join(layer.dimensions[d])}} + {{else}} + {{','.join(layer.dimensions[d])}} + {{endif}} + {{endfor}} + {{endif}} + {{endif}} {{if layer.is_active and layer.has_legend and layer.legend_url}}