TransWikia.com

How to lock a WMS layer to scale in QGIS?

Geographic Information Systems Asked on June 13, 2021

I am making a A0 size poster map and need a very detailed basemap. Is there a way to lock the OpenStreetMap basemap to a certain scale/zoom so that it would be very detailed in the final print map? I can only get it to print in a big scale which simplifies the map a lot. The area I need to map is fairly big but I need it to still be detailed. I’m using QGIS but have access to ArcGis Pro as well.

3 Answers

The unfortunate truth is that WMS basemaps are designed for on-screen viewing, not for printing. To make a good printed map, you usually have to design the whole thing, with separate layers and custom styling.

One workaround is to get a basemap without any labels (there are several in the expanded provider pack of the quickmapservices plugin). Then you add manually any labels that you absolutely have to have, and live without the rest. Of course you still have the tradeoff between [high resolution + basemap features too small] vs. [low resolution/pixelated + basemap features the right size].

But, with that workaround, you can get labels by loading features (eg roads, towns, etc) from a WFS, turning off the symbology and only using them for labels. That way the labels are applied on top of the features baked into the basemap, and you don't have to set custom symbology.

Answered by csk on June 13, 2021

I struggled around with the same problem: always getting unreadable text when printing WMS layers to poster format in QGIS.

Even though the reduction of the layout resolution (i.e. 150DPI) helps sometimes, I found a really nice solution by using the fantastic MapProxy web map proxy server.

Therefor I did some minor adjustments to enable an optional WMS-URL parameter called "minres", which locks the resolution to a specific value.

Here are my changes to 3 of the MapProxy Python files:

/site-packages/mapproxy/grid.py

    ...

    def get_affected_tiles(self, bbox, size, req_srs=None, minres=0): # added minres 

        src_bbox, level = self.get_affected_bbox_and_level(bbox, size, req_srs=req_srs, minres=minres) # added minres
        return self.get_affected_level_tiles(src_bbox, level)

    def get_affected_bbox_and_level(self, bbox, size, req_srs=None, minres=0): # added minres
        if req_srs and req_srs != self.srs:
            src_bbox = req_srs.transform_bbox_to(self.srs, bbox)
        else:
            src_bbox = bbox

        if not bbox_intersects(self.bbox, src_bbox):
            raise NoTiles()

        res = get_resolution(src_bbox, size)

        ## added code for minres
        if res < minres:
            res = minres
        ##

        level = self.closest_level(res)

        if res > self.resolutions[0]*self.max_shrink_factor:
            raise NoTiles()

        return src_bbox, level
    ...

/site-packages/mapproxy/layer.py

    ...

    def _image(self, query):
        ### added code for minres
        if not hasattr(query,'minres'):
            query.minres = 0
        ###
        try:
            src_bbox, tile_grid, affected_tile_coords = 
                self.grid.get_affected_tiles(query.bbox, query.size,
                                             req_srs=query.srs,minres=query.minres) # added "minres"
        except NoTiles:
            raise BlankImage()
        except GridError as ex:
            raise MapBBOXError(ex.args[0])
    ...

/site-packages/mapproxy/service/wms.py

    ...

    def map(self, map_request):
        self.check_map_request(map_request)

        params = map_request.params
        query = MapQuery(params.bbox, params.size, SRS(params.srs), params.format)
        ### added code for minres
        query.minres = int(map_request.params.get('minres','0').replace('?',''))
        ###

        if map_request.params.get('tiled', 'false').lower() == 'true':
            query.tiled_only = True
        orig_query = query

     ...

     def capabilities(self, map_request):
        # TODO: debug layer
        # if '__debug__' in map_request.params:
        #     layers = self.layers.values()
        # else:
        #     layers = [layer for name, layer in iteritems(self.layers)
        #               if name != '__debug__']

        if map_request.params.get('tiled', 'false').lower() == 'true':
            tile_layers = self.tile_layers.values()
        else:
            tile_layers = []

        service = self._service_md(map_request)

        ### added code for minres
        if 'minres' in map_request.raw_params:
            service['url'] = service['url'] + '?minres=' + map_request.raw_params['minres']
        ###

        root_layer = self.authorized_capability_layers(map_request.http.environ)

     ...

After all changes we can add "minres=xx" to the WMS URL to lock the layer resolution in QGIS:

http://localhost:8080/mapproxy/service_name/service?minres=10

Please don't ask me about the right "minres" value ... I always have to try different values to achieve the best result. :-)

Answered by christoph on June 13, 2021

Good things come to those who wait!

Yesterday I found this nice tag called <TileLevel> in the GDAL WMS-XML description file, which helps us to freeze WMTS/TMS tile levels.

Take the XML description for the OpenStreetMap service as example (osm.xml):

<GDAL_WMS>
    <Service name="TMS">
        <ServerUrl>https://tile.openstreetmap.org/${z}/${x}/${y}.png</ServerUrl>
    </Service>
    <DataWindow>
        <UpperLeftX>-20037508.34</UpperLeftX>
        <UpperLeftY>20037508.34</UpperLeftY>
        <LowerRightX>20037508.34</LowerRightX>
        <LowerRightY>-20037508.34</LowerRightY>
        <TileLevel>14</TileLevel>
        <TileCountX>1</TileCountX>
        <TileCountY>1</TileCountY>
        <YOrigin>top</YOrigin>
    </DataWindow>
    <Projection>EPSG:3857</Projection>
    <BlockSizeX>256</BlockSizeX>
    <BlockSizeY>256</BlockSizeY>
    <BandsCount>3</BandsCount>
    <UserAgent>Mozilla/5.0 QGIS/31602</UserAgent>
    <Cache />
</GDAL_WMS>

Simply drag the file into QGIS map canvas and change the image resampling settings to Cubic.

If you create your map in a local projection (i.e. EPSG:31255), it's better to use GDALWARP with an additional resampling parameter (i.e. -r CubicSpline) to reproject the WMTS service into a virtual image (VRT) to improve the printout.

gdalwarp -r CubicSpline -t_srs EPSG:31255 -of VRT osm.xml osm_31255.vrt

If you're going to clip the WMTS to reduce the print size, don't forget to set the GDAL config option GDALWARP_DENSIFY_CUTLINE to NO. This setting will reduce the VRT size as well.

gdalwarp --config GDALWARP_DENSIFY_CUTLINE NO -r CubicSpline -dstalpha -cutline boundary.geojson -crop_to_cutline -t_srs EPSG:31255 -of VRT osm.xml osm_31255.vrt

And last but not least, you can do the same with WMTS-XML description files. Here you have to edit the tag <ZoomLevel> to get best results.

So there's really no need to install and customize a MapProxy server for this (see my first answer in this thread), but if you do, you won't regret it!

Answered by christoph on June 13, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP