[Git][debian-gis-team/mapproxy][upstream] New upstream version 2.0.1+dfsg

Bas Couwenberg (@sebastic) gitlab at salsa.debian.org
Fri Jan 5 14:49:55 GMT 2024



Bas Couwenberg pushed to branch upstream at Debian GIS Project / mapproxy


Commits:
5bcd1bab by Bas Couwenberg at 2024-01-05T15:39:40+01:00
New upstream version 2.0.1+dfsg
- - - - -


30 changed files:

- .github/workflows/dockerbuild.yml
- .github/workflows/test.yml
- + .github/workflows/trivy.yml
- .gitignore
- CHANGES.txt
- doc/conf.py
- doc/configuration.rst
- doc/configuration_examples.rst
- docker/Dockerfile
- + docker/Dockerfile-alpine
- + docker/nginx-alpine-default.conf
- docker/nginx-default.conf
- + docker/start_alpine.sh
- mapproxy/cache/azureblob.py
- mapproxy/cache/file.py
- mapproxy/cache/path.py
- mapproxy/cache/s3.py
- mapproxy/cache/tile.py
- mapproxy/config/defaults.py
- mapproxy/config/loader.py
- mapproxy/config_template/base_config/full_example.yaml
- mapproxy/service/demo.py
- mapproxy/service/templates/demo/tms_demo.html
- mapproxy/service/templates/demo/wms_demo.html
- mapproxy/service/templates/demo/wmts_demo.html
- mapproxy/test/mocker.py
- mapproxy/test/system/test_mixed_mode_format.py
- mapproxy/test/unit/test_image_messages.py
- requirements-tests.txt
- setup.py


Changes:

=====================================
.github/workflows/dockerbuild.yml
=====================================
@@ -70,6 +70,34 @@ jobs:
             ghcr.io/${{ github.repository }}/mapproxy:${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
           platforms: linux/amd64,linux/arm64
 
+      - name: Build and push base alpine image
+        uses: docker/build-push-action at v5
+        if: ${{ inputs.tags }}
+        with:
+          context: docker/
+          file: ./docker/Dockerfile-alpine
+          push: true
+          build-args: |
+            MAPPROXY_VERSION=${{ inputs.tags }}
+          target: base
+          tags: |
+            ghcr.io/${{ github.repository }}/mapproxy:${{ inputs.tags }}-alpine
+          platforms: linux/amd64,linux/arm64
+
+      - name: Build and push base alpine image
+        uses: docker/build-push-action at v5
+        if: ${{ !inputs.tags }}
+        with:
+          context: docker/
+          file: ./docker/Dockerfile-alpine
+          push: true
+          build-args: |
+            MAPPROXY_VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
+          target: base
+          tags: |
+            ghcr.io/${{ github.repository }}/mapproxy:${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}-alpine
+          platforms: linux/amd64,linux/arm64
+
       - name: Build and push development image
         uses: docker/build-push-action at v5
         if: ${{ inputs.tags }}
@@ -125,3 +153,31 @@ jobs:
           tags: |
             ghcr.io/${{ github.repository }}/mapproxy:${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}-nginx
           platforms: linux/amd64,linux/arm64
+
+      - name: Build and push alpine based nginx image
+        uses: docker/build-push-action at v5
+        if: ${{ inputs.tags }}
+        with:
+          context: docker/
+          file: ./docker/Dockerfile-alpine
+          push: true
+          build-args: |
+            MAPPROXY_VERSION=${{ inputs.tags }}
+          target: nginx
+          tags: |
+            ghcr.io/${{ github.repository }}/mapproxy:${{ inputs.tags }}-alpine-nginx
+          platforms: linux/amd64,linux/arm64
+
+      - name: Build and push alpine based nginx image
+        uses: docker/build-push-action at v5
+        if: ${{ !inputs.tags }}
+        with:
+          context: docker/
+          file: ./docker/Dockerfile-alpine
+          push: true
+          build-args: |
+            MAPPROXY_VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
+          target: nginx
+          tags: |
+            ghcr.io/${{ github.repository }}/mapproxy:${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}-alpine-nginx
+          platforms: linux/amd64,linux/arm64


=====================================
.github/workflows/test.yml
=====================================
@@ -33,7 +33,7 @@ jobs:
 
     strategy:
       matrix:
-        python-version: [3.8, 3.9, "3.10", "3.11"]
+        python-version: [3.8, 3.9, "3.10", "3.11", "3.12"]
 
     env:
       MAPPROXY_TEST_COUCHDB: 'http://localhost:5984'


=====================================
.github/workflows/trivy.yml
=====================================
@@ -0,0 +1,45 @@
+name: build
+
+on:
+  workflow_dispatch:
+    inputs:
+      tags:
+        description: 'Manual supplied image tag like 1.16.0'
+        required: true
+        type: string
+  push:
+    tags:
+      - "*.*.*"
+jobs:
+  build:
+    name: Build
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout code
+        uses: actions/checkout at v4
+      - name: Build and push nginx image
+        uses: docker/build-push-action at v5
+        if: ${{ inputs.tags }}
+        with:
+          context: docker/
+          file: ./docker/Dockerfile
+          push: true
+          build-args: |
+            MAPPROXY_VERSION=${{ inputs.tags }}
+          target: nginx
+          tags: |
+            ghcr.io/${{ github.repository }}/mapproxy:${{ inputs.tags }}
+          platforms: linux/amd64,linux/arm64
+      - name: Run trivy
+        uses: aquasecurity/trivy-action at master
+        with:
+          format: 'table'
+          ignore-unfixed: true
+          image-ref: 'ghcr.io/${{ github.repository }}/mapproxy:${{ inputs.tags }}'
+          output: 'trivy-results.sarif'
+          severity: 'CRITICAL,HIGH'
+          vuln-type: 'os,library'
+      - name: Upload Trivy scan results to GitHub Security tab
+        uses: github/codeql-action/upload-sarif at v3
+        with:
+          sarif_file: 'trivy-results.sarif'


=====================================
.gitignore
=====================================
@@ -15,5 +15,6 @@ nosetests*.xml
 .settings
 .pydevproject
 .tox/
-
+
 .idea/
+*.iml


=====================================
CHANGES.txt
=====================================
@@ -1,6 +1,20 @@
 Nightly
 ~~~~~~~~~~~~~~~~~
 
+2.0.1 2024-01-05
+~~~~~~~~~~~~~~~~~
+Improvements:
+
+- Alpine based docker images have been added
+- Added parameter to change background map source in layer preview
+- Dependency updates
+
+Fixes:
+
+- Rendering issue: Check for existing tile coordinates before using them
+- Fix mixed image format for file, S3 and azureblob caches
+
+
 2.0.0 2023-12-19
 ~~~~~~~~~~~~~~~~~
 Breaking:


=====================================
doc/conf.py
=====================================
@@ -51,7 +51,7 @@ copyright = u'Oliver Tonnhofer, Omniscale'
 # The short X.Y version.
 version = '2.0'
 # The full version, including alpha/beta/rc tags.
-release = '2.0.0'
+release = '2.0.1'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.


=====================================
doc/configuration.rst
=====================================
@@ -820,6 +820,18 @@ globals
 
 Here you can define some internals of MapProxy and default values that are used in the other configuration directives.
 
+.. _globals_background:
+
+``background``
+""""""""""""""
+
+Configuration of the background displayed in the map viewer. This background map can be observed in the /demo service 
+of MapProxy, in any of the three types of service (WMS, WMTS and TMS).
+
+.. _background_url:
+
+``url``
+  URL of the tile service (it MUST be a service that offers tiles in XYZ format e.g. "https://tile.openstreetmap.org/{z}/{x}/{y}.png")
 
 ``image``
 """""""""


=====================================
doc/configuration_examples.rst
=====================================
@@ -139,6 +139,25 @@ Example configuration for an OpenStreetMap tile service::
 
 .. 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>`_.
 
+
+.. _display_custom_background:
+
+Display custom background map in the map viewer of the demo service
+===================================================================
+
+In order to setup the background displayed in the map viewer of the /demo service of Mapproxy
+you need to add the service of the background map to ``globals``.
+
+Here is a minimal example with the default configuration::
+
+  globals:
+    # background map of the demo service
+    background:
+      # tile source in ZXY format
+      url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png" 
+
+.. note:: URL of the tile service MUST be in XYZ format. 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


=====================================
docker/Dockerfile
=====================================
@@ -1,8 +1,10 @@
-FROM python:3.10-slim-bullseye AS base
+FROM python:3.10-slim-bookworm AS base
+
+LABEL maintainer="mapproxy.org"
 
 # The MAPPROXY_VERSION argument can be used like this to overwrite the default:
 # docker build --build-arg MAPPROXY_VERSION=1.15.1 [--target base|development|nginx] -t mapproxy:1.15.1 .
-ARG MAPPROXY_VERSION=1.15.1
+ARG MAPPROXY_VERSION=1.16.0
 
 RUN apt update && apt -y install --no-install-recommends \
   python3-pil \


=====================================
docker/Dockerfile-alpine
=====================================
@@ -0,0 +1,70 @@
+FROM python:3.11-alpine AS base
+
+LABEL maintainer="mapproxy.org"
+
+# The MAPPROXY_VERSION argument can be used like this to overwrite the default:
+# docker build -f Dockerfile-alpine --build-arg MAPPROXY_VERSION=1.16.0 [--target base|development|nginx] -t mapproxy-alpine:1.16.0 .
+ARG MAPPROXY_VERSION=1.16.0
+
+RUN apk -U upgrade --update \
+    && apk add g++ py3-pip gdal gdal-dev gdal gdal-dev libxslt-dev libxml2 proj proj-dev proj-util geos geos-dev \
+    && rm -rf /var/cache/apk/* \
+    && pip install numpy pillow pyyaml pyproj lxml shapely \
+    && pip install MapProxy==$MAPPROXY_VERSION
+
+RUN mkdir /mapproxy
+
+WORKDIR /mapproxy
+
+COPY app.py .
+
+COPY start_alpine.sh /start_alpine.sh
+
+ENTRYPOINT ["sh", "-c", "/start_alpine.sh base"]
+
+###### development image ######
+FROM base AS development
+
+EXPOSE 8080
+
+ENTRYPOINT ["sh", "-c", "/start_alpine.sh development"]
+
+##### nginx image ######
+FROM base AS nginx
+
+# use current version of nginx
+ARG NGINX_VERSION=1.25.3
+
+RUN \
+  apk --no-cache add build-base linux-headers openssl-dev pcre-dev wget zlib-dev ca-certificates uwsgi uwsgi-python3 supervisor && \
+  pip install uwsgi && \
+  cd /tmp && \
+  wget https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz && \
+  tar xzf nginx-${NGINX_VERSION}.tar.gz && \
+  cd /tmp/nginx-${NGINX_VERSION} && \
+  ./configure \
+    --prefix=/etc/nginx \
+    --sbin-path=/usr/sbin/nginx \
+    --conf-path=/etc/nginx/nginx.conf \
+    --error-log-path=/var/log/nginx/error.log \
+    --http-log-path=/var/log/nginx/access.log \
+    --pid-path=/var/run/nginx.pid \
+    --lock-path=/var/run/nginx.lock \
+    --http-client-body-temp-path=/var/cache/nginx/client_temp \
+    --http-proxy-temp-path=/var/cache/nginx/proxy_temp \
+    --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
+    --user=mapproxy \
+    --group=mapproxy && \
+  make && \
+  make install && \
+  sed -i -e 's/#access_log  logs\/access.log  main;/access_log \/dev\/stdout;/' -e 's/#error_log  logs\/error.log  notice;/error_log stderr notice;/' /etc/nginx/nginx.conf && \
+  rm -rf /tmp/* && \
+  apk del build-base linux-headers openssl-dev pcre-dev wget zlib-dev && \
+  rm -rf /var/cache/apk/*
+
+COPY uwsgi.conf .
+COPY nginx-alpine-default.conf /etc/nginx/nginx.conf
+
+EXPOSE 80
+
+ENTRYPOINT ["sh", "-c", "/start_alpine.sh nginx"]


=====================================
docker/nginx-alpine-default.conf
=====================================
@@ -0,0 +1,31 @@
+events {}
+http {
+    server {
+        listen 80;
+
+        # Setting a keep-alive timeout on the server side helps mitigate denial of service attacks
+        keepalive_timeout 10;
+
+        # Setting the send_timeout directive on the server side helps mitigate slow HTTP denial of
+        # service attacks
+        send_timeout 10;
+
+        # Hiding the version will slow down and deter some potential attackers since nginx version number is not visible
+        # for them
+        server_tokens off;
+
+        root /var/www/html/;
+
+        location /mapproxy/ {
+            rewrite /mapproxy/(.+) /$1 break;
+            uwsgi_param SCRIPT_NAME /mapproxy;
+            uwsgi_pass 0.0.0.0:8080;
+            include uwsgi_params;
+
+            # The server and x-powered-by header specify the version of the nginx => Do not show this information
+            # of the server to potential attackers
+            proxy_hide_header X-Powered-By;
+            proxy_hide_header Server;
+        }
+    }
+}


=====================================
docker/nginx-default.conf
=====================================
@@ -4,6 +4,17 @@ upstream mapproxy {
 server {
     listen 80;
 
+    # Setting a keep-alive timeout on the server side helps in mitigation of DOS attacks
+    keepalive_timeout 10;
+
+    # Setting the send_timeout directive on the server side helps mitigate slow HTTP denial of
+    # service attacks
+    send_timeout 10;
+
+    # Hiding the version will slow down and deter some potential attackers since nginx version number is not visible
+    # for them
+    server_tokens off;
+
     root /var/www/html/;
 
     location /mapproxy/ {
@@ -11,5 +22,10 @@ server {
         uwsgi_param SCRIPT_NAME /mapproxy;
         uwsgi_pass mapproxy;
         include uwsgi_params;
+
+        # The server and x-powered-by header specify the version of the nginx => Do not show this information
+        # of the server to potential attackers
+        proxy_hide_header X-Powered-By;
+        proxy_hide_header Server;
     }
 }


=====================================
docker/start_alpine.sh
=====================================
@@ -0,0 +1,32 @@
+#!/bin/sh
+
+TARGET=$1
+done=0
+trap 'done=1' TERM INT
+cd /mapproxy
+
+addgroup -S mapproxy && \
+  adduser -D -s /bin/sh -S -h /mapproxy/ -G mapproxy mapproxy && \
+  mkdir -p /var/cache/nginx/ /mapproxy/config/cache_data/ && \
+  chown -R mapproxy:mapproxy /mapproxy/config/ /var/cache/nginx/
+
+# create config files if they do not exist yet
+if [ ! -f /mapproxy/config/mapproxy.yaml ]; then
+  echo "No mapproxy configuration found. Creating one from template."
+  mapproxy-util create -t base-config config
+fi
+
+if [ "$TARGET" = "nginx" ]; then
+  su mapproxy -c "uwsgi --ini /mapproxy/uwsgi.conf --uid mapproxy &"
+  nginx &
+elif [ "$TARGET" = 'development' ]; then
+  su mapproxy -c "mapproxy-util serve-develop -b 0.0.0.0 /mapproxy/config/mapproxy.yaml &"
+else
+  echo "No-op container started. Overwrite ENTRYPOINT with needed mapproxy command."
+  su mapproxy -c "sleep infinity &"
+fi
+
+while [ $done = 0 ]; do
+  sleep 1 &
+  wait
+done


=====================================
mapproxy/cache/azureblob.py
=====================================
@@ -57,6 +57,7 @@ class AzureBlobCache(TileCacheBase):
 
         self.base_path = base_path
         self.file_ext = file_ext
+        self.is_mixed = self.file_ext == 'mixed'
         self._concurrent_writer = _concurrent_writer
         self._concurrent_reader = _concurrent_reader
         self._tile_location, _ = path.location_funcs(layout=directory_layout)
@@ -70,7 +71,16 @@ class AzureBlobCache(TileCacheBase):
         return self._container_client_cache.client
 
     def tile_key(self, tile):
-        return self._tile_location(tile, self.base_path, self.file_ext).lstrip('/')
+        if self.is_mixed:
+            location = self._tile_location(tile, self.base_path, 'jpeg').lstrip('/')
+            blob = self.container_client.get_blob_client(location)
+            if not blob.exists():
+                tile.location = None
+                location = self._tile_location(tile, self.base_path, 'png').lstrip('/')
+        else:
+            location = self._tile_location(tile, self.base_path, self.file_ext).lstrip('/')
+        tile.location = location
+        return location
 
     def load_tile_metadata(self, tile, dimensions=None):
         if tile.timestamp:


=====================================
mapproxy/cache/file.py
=====================================
@@ -42,6 +42,7 @@ class FileCache(TileCacheBase):
         self.cache_dir = cache_dir
         self.file_ext = file_ext
         self.image_opts = image_opts
+        self.is_mixed = self.file_ext == 'mixed'
         self.link_single_color_images = link_single_color_images
         self._tile_location, self._level_location = path.location_funcs(layout=directory_layout)
         if self._level_location is None:
@@ -53,7 +54,16 @@ class FileCache(TileCacheBase):
             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)
+        
+        if self.is_mixed:
+            location = self._tile_location(tile, self.cache_dir, 'jpeg', create_dir=create_dir, dimensions=dimensions)
+            if not os.path.exists(location):
+                tile.location = None
+                location = self._tile_location(tile, self.cache_dir, 'png', create_dir=create_dir, dimensions=dimensions)
+        else:
+            location = self._tile_location(tile, self.cache_dir, self.file_ext, create_dir=create_dir, dimensions=dimensions)
+        tile.location = location
+        return location
 
     def level_location(self, level, dimensions=None):
         """


=====================================
mapproxy/cache/path.py
=====================================
@@ -96,22 +96,23 @@ def tile_location_tc(tile, cache_dir, file_ext, create_dir=False, dimensions=Non
     >>> tile_location_tc(Tile((3, 4, 2)), '/tmp/cache', 'png').replace('\\\\', '/')
     '/tmp/cache/02/000/000/003/000/000/004.png'
     """
-    if tile.location is None:
+    location = tile.location
+    if 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),
-                 "%03d" % (int(x) % 1000),
-                 "%03d" % int(y / 1000000),
-                 "%03d" % (int(y / 1000) % 1000),
-                 "%03d.%s" % (int(y) % 1000, file_ext))
-        tile.location = os.path.join(*parts)
+                    "%03d" % int(x / 1000000),
+                    "%03d" % (int(x / 1000) % 1000),
+                    "%03d" % (int(x) % 1000),
+                    "%03d" % int(y / 1000000),
+                    "%03d" % (int(y / 1000) % 1000),
+                    "%03d.%s" % (int(y) % 1000, file_ext))
+        location = os.path.join(*parts)
     if create_dir:
-        ensure_directory(tile.location)
-    return tile.location
+        ensure_directory(location)
+    return location
 
 def tile_location_mp(tile, cache_dir, file_ext, create_dir=False, dimensions=None):
     """
@@ -128,19 +129,20 @@ def tile_location_mp(tile, cache_dir, file_ext, create_dir=False, dimensions=Non
     >>> tile_location_mp(Tile((12345678, 98765432, 22)), '/tmp/cache', 'png').replace('\\\\', '/')
     '/tmp/cache/22/1234/5678/9876/5432.png'
     """
-    if tile.location is None:
+    location = tile.location
+    if 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),
-                 "%04d" % int(y / 10000),
-                 "%04d.%s" % (int(y) % 10000, file_ext))
-        tile.location = os.path.join(*parts)
+                    "%04d" % int(x / 10000),
+                    "%04d" % (int(x) % 10000),
+                    "%04d" % int(y / 10000),
+                    "%04d.%s" % (int(y) % 10000, file_ext))
+        location = os.path.join(*parts)
     if create_dir:
-        ensure_directory(tile.location)
-    return tile.location
+        ensure_directory(location)
+    return location
 
 def tile_location_tms(tile, cache_dir, file_ext, create_dir=False, dimensions=None):
     """
@@ -155,15 +157,16 @@ def tile_location_tms(tile, cache_dir, file_ext, create_dir=False, dimensions=No
     >>> tile_location_tms(Tile((3, 4, 2)), '/tmp/cache', 'png').replace('\\\\', '/')
     '/tmp/cache/2/3/4.png'
     """
-    if tile.location is None:
+    location = tile.location
+    if location is None:
         x, y, z = tile.coord
-        tile.location = os.path.join(
+        location = os.path.join(
             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
+        ensure_directory(location)
+    return location
 
 def tile_location_reverse_tms(tile, cache_dir, file_ext, create_dir=False, dimensions=None):
     """
@@ -178,14 +181,15 @@ def tile_location_reverse_tms(tile, cache_dir, file_ext, create_dir=False, dimen
     >>> tile_location_reverse_tms(Tile((3, 4, 2)), '/tmp/cache', 'png').replace('\\\\', '/')
     '/tmp/cache/4/3/2.png'
     """
-    if tile.location is None:
+    location = tile.location
+    if location is None:
         x, y, z = tile.coord
-        tile.location = os.path.join(
+        location = os.path.join(
             cache_dir,dimensions_part(dimensions),str(y), str(x), str(z) + '.' + file_ext
         )
     if create_dir:
-        ensure_directory(tile.location)
-    return tile.location
+        ensure_directory(location)
+    return location
 
 def level_location_tms(level, cache_dir, dimensions=None):
     return level_location(str(level), cache_dir=cache_dir)
@@ -203,7 +207,8 @@ def tile_location_quadkey(tile, cache_dir, file_ext, create_dir=False, dimension
     >>> tile_location_quadkey(Tile((3, 4, 2)), '/tmp/cache', 'png').replace('\\\\', '/')
     '/tmp/cache/11.png'
     """
-    if tile.location is None:
+    location = tile.location
+    if location is None:
         x, y, z = tile.coord
         quadKey = ""
         for i in range(z,0,-1):
@@ -214,12 +219,12 @@ def tile_location_quadkey(tile, cache_dir, file_ext, create_dir=False, dimension
             if (y & mask) != 0:
                 digit += 2
             quadKey += str(digit)
-        tile.location = os.path.join(
+        location = os.path.join(
             cache_dir, quadKey + '.' + file_ext
         )
     if create_dir:
-        ensure_directory(tile.location)
-    return tile.location
+        ensure_directory(location)
+    return location
 
 def no_level_location(level, cache_dir, dimensions=None):
     # dummy for quadkey cache which stores all tiles in one directory
@@ -238,13 +243,14 @@ def tile_location_arcgiscache(tile, cache_dir, file_ext, create_dir=False, dimen
     >>> tile_location_arcgiscache(Tile((1234567, 87654321, 9)), '/tmp/cache', 'png').replace('\\\\', '/')
     '/tmp/cache/L09/R05397fb1/C0012d687.png'
     """
-    if tile.location is None:
+    location = tile.location
+    if location is None:
         x, y, z = tile.coord
         parts = (cache_dir, 'L%02d' % z, 'R%08x' % y, 'C%08x.%s' % (x, file_ext))
-        tile.location = os.path.join(*parts)
+        location = os.path.join(*parts)
     if create_dir:
-        ensure_directory(tile.location)
-    return tile.location
+        ensure_directory(location)
+    return location
 
 def level_location_arcgiscache(z, cache_dir, dimensions=None):
     return level_location('L%02d' % z, cache_dir=cache_dir, dimensions=dimensions)


=====================================
mapproxy/cache/s3.py
=====================================
@@ -79,6 +79,7 @@ class S3Cache(TileCacheBase):
 
         self.base_path = base_path
         self.file_ext = file_ext
+        self.is_mixed = self.file_ext == 'mixed'
         self._concurrent_writer = _concurrent_writer
 
         self._tile_location, _ = path.location_funcs(layout=directory_layout)
@@ -87,7 +88,18 @@ class S3Cache(TileCacheBase):
         return "https://{bucket}.s3.{region}.amazonaws.com/{key}".format(bucket=self.bucket_name, region=self.region_name, key=self.tile_key(tile))
 
     def tile_key(self, tile):
-        return self._tile_location(tile, self.base_path, self.file_ext).lstrip('/')
+        if self.is_mixed:
+            location = self._tile_location(tile, self.base_path, 'jpeg').lstrip('/')
+            try:
+                self.conn().head_object(Bucket=self.bucket_name, Key=location)
+            except botocore.exceptions.ClientError as e:
+                if e.response['Error']['Code'] in ('404', 'NoSuchKey'):
+                    tile.location = None
+                    location = self._tile_location(tile, self.base_path, 'png').lstrip('/')
+        else:
+            location = self._tile_location(tile, self.base_path, self.file_ext).lstrip('/')
+        tile.location = location
+        return location
 
     def conn(self):
         if boto3 is None:


=====================================
mapproxy/cache/tile.py
=====================================
@@ -143,10 +143,12 @@ class TileManager(object):
                 rescale_till_zoom = self.grid.levels
         
         # Remove tiles that are not in the cache coverage
-        for t in tiles.tiles:
-            tile_bbox = self.grid.tile_bbox(t.coord)
-            if self.cache.coverage and not self.cache.coverage.intersects(tile_bbox, self.grid.srs):
-                t.coord = None
+        if self.cache.coverage:
+            for t in tiles.tiles:
+                if t.coord:
+                    tile_bbox = self.grid.tile_bbox(t.coord)
+                    if not self.cache.coverage.intersects(tile_bbox, self.grid.srs):
+                        t.coord = None
 
         tiles = self._load_tile_coords(
             tiles, dimensions=dimensions, with_metadata=with_metadata,
@@ -155,7 +157,7 @@ class TileManager(object):
 
         for t in tiles.tiles:
             # Clip tiles if clipping is enabled for coverage
-            if self.cache.coverage and self.cache.coverage.clip and t.source:
+            if t.coord and self.cache.coverage and self.cache.coverage.clip and t.source:
                 tile_bbox = self.grid.tile_bbox(t.coord)
                 coverage = self.cache.coverage
                 


=====================================
mapproxy/config/defaults.py
=====================================
@@ -26,6 +26,10 @@ wms = dict(
 )
 debug_mode = False
 
+background = dict(
+    url = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
+)
+
 srs = dict(
     # user sets
     axis_order_ne = set(),


=====================================
mapproxy/config/loader.py
=====================================
@@ -2208,9 +2208,10 @@ class ServiceConfiguration(ConfigurationBase):
                 services.append('wms_111')
 
         layers = odict(sorted(layers.items(), key=lambda x: x[1].name))
+        background =  self.context.globals.get_value('background', conf)
 
         return DemoServer(layers, md, tile_layers=tile_layers,
-            image_formats=image_formats, srs=srs, services=services, restful_template=restful_template)
+            image_formats=image_formats, srs=srs, services=services, restful_template=restful_template, background = background)
 
 
 def load_plugins():


=====================================
mapproxy/config_template/base_config/full_example.yaml
=====================================
@@ -591,3 +591,8 @@ globals:
         encoding_options:
           # jpeg quality [0-100]
           jpeg_quality: 60
+
+  # background map of the demo service
+  background:
+    # tile source in ZXY format
+    url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png" 


=====================================
mapproxy/service/demo.py
=====================================
@@ -86,7 +86,7 @@ def static_filename(name):
 class DemoServer(Server):
     names = ('demo',)
     def __init__(self, layers, md, request_parser=None, tile_layers=None,
-                 srs=None, image_formats=None, services=None, restful_template=None):
+                 srs=None, image_formats=None, services=None, restful_template=None, background = None):
         Server.__init__(self)
         self.layers = layers
         self.tile_layers = tile_layers or {}
@@ -100,6 +100,7 @@ class DemoServer(Server):
         self.srs = srs
         self.services = services or []
         self.restful_template = restful_template
+        self.background = background
 
     def handle(self, req):
         if req.path.startswith('/demo/static/'):
@@ -258,13 +259,17 @@ class DemoServer(Server):
         width = bbox[2] - bbox[0]
         height = bbox[3] - bbox[1]
         min_res = max(width/256, height/256)
+        background_url = base_config().background.url
+        if self.background:
+            background_url = self.background["url"]
         return template.substitute(layer=layer,
                                    image_formats=self.image_formats,
                                    format=escape(req.args['format']),
                                    srs=srs,
                                    layer_srs=self.layer_srs,
                                    bbox=bbox,
-                                   res=min_res)
+                                   res=min_res,
+                                   background_url = background_url)
 
     def _render_tms_template(self, template, req):
         template = get_template(template, default_inherit="demo/static.html")
@@ -288,13 +293,17 @@ class DemoServer(Server):
             add_res_to_options = True
         else:
             add_res_to_options = False
+        background_url = base_config().background.url
+        if self.background:
+            background_url = self.background["url"]
         return template.substitute(layer=tile_layer,
                                    srs=escape(req.args['srs']),
                                    format=escape(req.args['format']),
                                    resolutions=res,
                                    units=units,
                                    add_res_to_options=add_res_to_options,
-                                   all_tile_layers=self.tile_layers)
+                                   all_tile_layers=self.tile_layers,
+                                   background_url = background_url)
 
     def _render_wmts_template(self, template, req):
         template = get_template(template, default_inherit="demo/static.html")
@@ -312,6 +321,9 @@ class DemoServer(Server):
             units = 'degree'
         else:
             units = 'm'
+        background_url = base_config().background.url
+        if self.background:
+            background_url = self.background["url"]
         return template.substitute(layer=wmts_layer,
                                    matrix_set=wmts_layer.grid.name,
                                    format=escape(req.args['format']),
@@ -319,7 +331,8 @@ class DemoServer(Server):
                                    resolutions=wmts_layer.grid.resolutions,
                                    units=units,
                                    all_tile_layers=self.tile_layers,
-                                   restful_url=restful_url)
+                                   restful_url=restful_url,
+                                   background_url = background_url)
 
     def _render_capabilities_template(self, template, xmlfile, service, url):
         template = get_template(template, default_inherit="demo/static.html")


=====================================
mapproxy/service/templates/demo/tms_demo.html
=====================================
@@ -42,9 +42,13 @@ jscript_functions=None
             maxResolution: {{resolutions[0]}}
         });
 
+        const background_source = new ol.source.XYZ({
+            url: "{{background_url}}"
+        });
+
         const layers = [
             new ol.layer.Tile({
-                source: new ol.source.OSM()
+                source: background_source
             }),
             new ol.layer.Tile({source})
         ];


=====================================
mapproxy/service/templates/demo/wms_demo.html
=====================================
@@ -42,9 +42,13 @@ jscript_functions=None
             }
         });
 
+        const background_source = new ol.source.XYZ({
+            url: "{{background_url}}"
+        });
+
         const layers = [
             new ol.layer.Tile({
-                source: new ol.source.OSM()
+                source: background_source
             }),
             new ol.layer.Image({source})
         ];


=====================================
mapproxy/service/templates/demo/wmts_demo.html
=====================================
@@ -58,9 +58,13 @@ jscript_functions=None
             })
         });
 
+        const background_source = new ol.source.XYZ({
+            url: "{{background_url}}"
+        });
+
         const layers = [
             new ol.layer.Tile({
-                source: new ol.source.OSM()
+                source: background_source
             }),
             new ol.layer.Tile({source})
         ];


=====================================
mapproxy/test/mocker.py
=====================================
@@ -453,8 +453,12 @@ class MockerTestCase(unittest.TestCase):
     assertNotIsInstance = failIfIsInstance # Poor choice in 2.7/3.2+.
 
     # The following are missing in Python < 2.4.
-    assertTrue = unittest.TestCase.failUnless
-    assertFalse = unittest.TestCase.failIf
+    if sys.version_info < (2, 4):
+        assertTrue = unittest.TestCase.failUnless
+        assertFalse = unittest.TestCase.failIf
+    else:
+        assertTrue = unittest.TestCase.assertTrue
+        assertFalse = unittest.TestCase.assertFalse
 
     # The following is provided for compatibility with Twisted's trial.
     assertIdentical = assertIs


=====================================
mapproxy/test/system/test_mixed_mode_format.py
=====================================
@@ -85,8 +85,8 @@ class TestWMS(SysTest):
 
         # check cache formats
         for f, format in [
-            ["mixed_cache_EPSG900913/01/000/000/000/000/000/001.mixed", "png"],
-            ["mixed_cache_EPSG900913/01/000/000/001/000/000/001.mixed", "jpeg"],
+            [f"mixed_cache_EPSG900913/01/000/000/000/000/000/001.{req_format}", "png"],
+            [f"mixed_cache_EPSG900913/01/000/000/001/000/000/001.{req_format}", "jpeg"],
         ]:
             assert cache_dir.join(f).check()
             check_format(cache_dir.join(f).read_binary(), format)
@@ -119,10 +119,10 @@ class TestTMS(SysTest):
 
                 assert resp.content_type == "image/jpeg"
         assert cache_dir.join(
-            "mixed_cache_EPSG900913/01/000/000/000/000/000/000.mixed"
+            "mixed_cache_EPSG900913/01/000/000/000/000/000/000.png"
         ).check()
         assert cache_dir.join(
-            "mixed_cache_EPSG900913/01/000/000/001/000/000/000.mixed"
+            "mixed_cache_EPSG900913/01/000/000/001/000/000/000.png"
         ).check()
 
 
@@ -169,10 +169,10 @@ class TestWMTS(SysTest):
                 resp = app.get(self.common_tile_req)
                 assert resp.content_type == "image/jpeg"
         assert cache_dir.join(
-            "mixed_cache_EPSG900913/01/000/000/000/000/000/001.mixed"
+            "mixed_cache_EPSG900913/01/000/000/000/000/000/001.png"
         ).check()
         assert cache_dir.join(
-            "mixed_cache_EPSG900913/01/000/000/001/000/000/001.mixed"
+            "mixed_cache_EPSG900913/01/000/000/001/000/000/001.png"
         ).check()
 
 


=====================================
mapproxy/test/unit/test_image_messages.py
=====================================
@@ -23,9 +23,13 @@ from mapproxy.image.message import TextDraw, message_image
 from mapproxy.image.opts import ImageOptions
 from mapproxy.tilefilter import watermark_filter
 
+import pytest
 
 PNG_FORMAT = ImageOptions(format="image/png")
 
+requires_freetype = pytest.mark.skipif(
+    not isinstance(ImageFont.load_default(), ImageFont.FreeTypeFont),
+    reason="Test expects the default Pillow FreeTypeFont")
 
 class TestTextDraw(object):
 
@@ -38,6 +42,7 @@ class TestTextDraw(object):
         assert total_box == boxes[0]
         assert len(boxes) == 1
 
+    @requires_freetype
     def test_multiline_ul(self):
         font = ImageFont.load_default()
         td = TextDraw("Hello\nWorld", font)
@@ -47,6 +52,7 @@ class TestTextDraw(object):
         assert total_box == (5, 7, 33, 28)
         assert boxes == [(5, 7, 30, 15), (5, 20, 33, 28)]
 
+    @requires_freetype
     def test_multiline_lr(self):
         font = ImageFont.load_default()
         td = TextDraw("Hello\nWorld", font, placement="lr")
@@ -56,6 +62,7 @@ class TestTextDraw(object):
         assert total_box == (67, 76, 95, 97)
         assert boxes == [(67, 76, 92, 84), (67, 89, 95, 97)]
 
+    @requires_freetype
     def test_multiline_center(self):
         font = ImageFont.load_default()
         td = TextDraw("Hello\nWorld", font, placement="cc")
@@ -65,6 +72,7 @@ class TestTextDraw(object):
         assert total_box == (36, 42, 64, 63)
         assert boxes == [(36, 42, 61, 50), (36, 55, 64, 63)]
 
+    @requires_freetype
     def test_unicode(self):
         font = ImageFont.load_default()
         td = TextDraw(u"Héllö\nWørld", font, placement="cc")
@@ -124,6 +132,7 @@ class TestMessageImage(object):
             15000,
         ]
 
+    @requires_freetype
     def test_message(self):
         image_opts = PNG_FORMAT.copy()
         image_opts.bgcolor = "#113399"


=====================================
requirements-tests.txt
=====================================
@@ -8,13 +8,13 @@ Shapely==2.0.1
 WebTest==3.0.0
 attrs==19.3.0;python_version<"3.10"
 attrs==23.1.0;python_version>="3.10"
-aws-sam-translator==1.81.0
+aws-sam-translator==1.82.0
 aws-xray-sdk==2.12.1
 beautifulsoup4==4.12.2
-boto3==1.33.12
+boto3==1.34.4
 boto==2.49.0
-botocore==1.33.12
-certifi==2023.7.22
+botocore==1.34.5
+certifi==2023.11.17
 cffi==1.16.0;
 cfn-lint==0.80.3
 chardet==5.2.0
@@ -25,7 +25,7 @@ docutils==0.20.1
 ecdsa==0.18.0
 future==0.18.3
 idna==2.9
-importlib-metadata==5.2.0
+importlib-metadata==7.0.0
 iniconfig==2.0.0
 jmespath==1.0.1
 jsondiff==1.3.1
@@ -34,10 +34,10 @@ jsonpickle==3.0.2
 jsonpointer==2.4
 jsonschema==3.2.0
 junit-xml==1.9
-lxml==4.9.3
+lxml==4.9.4
 mock==5.1.0
 more-itertools==10.1.0
-moto==4.2.11
+moto==4.2.12
 networkx==3.1
 numpy==1.26.0;python_version>="3.9"
 numpy==1.24.0;python_version=="3.8"
@@ -60,14 +60,14 @@ requests==2.31.0
 responses==0.24.1
 riak==2.7.0
 rsa==4.9
-s3transfer==0.8.2
+s3transfer==0.9.0
 six==1.16.0
 soupsieve==2.0.1
 sshpubkeys==3.3.1
 toml==0.10.2
 urllib3==1.26.18
 waitress==2.1.2
-websocket-client==1.6.1
+websocket-client==1.7.0
 werkzeug==1.0.1
 wrapt==1.16.0
 xmltodict==0.13.0


=====================================
setup.py
=====================================
@@ -54,7 +54,7 @@ def long_description(changelog_releases=10):
 
 setup(
     name='MapProxy',
-    version="2.0.0",
+    version="2.0.1",
     description='An accelerating proxy for tile and web map services',
     long_description=long_description(7),
     author='Oliver Tonnhofer',



View it on GitLab: https://salsa.debian.org/debian-gis-team/mapproxy/-/commit/5bcd1babc3ed8f8650af907f02dd3c8acd6cf3f2

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/mapproxy/-/commit/5bcd1babc3ed8f8650af907f02dd3c8acd6cf3f2
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20240105/edae9c4e/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list