5

My Openlayers application can display tiled rasters from arbitrary online sources. It will parse capabilities from any user-specified URL for WMTS, TMS, OSM, XYZ or WMS services and present the user with a list of available layers which they can choose from.

Many of these layers are only available in standard resolution, but most of the users will be using Retina or high DPI screens. The result is that the map features are all too small, thin and spidery looking and text is very difficult to read for people with standard eye sight (impossible for anyone with aging eyes).

Is there some way to get Openlayers to display these standard resolution tiles at double the resolution (or x1.3 or x3 or whatever the screen scale is)?

Ie, To display the normal 256x256 tiles at 512x512 pixels?

I know this would make the rasters look a little blurry, but in theory no more so than if they were displayed on a standard resolution screen. At least they'd be big enough to view/read properly - and I can give the user the option of whether to use this feature or not. I've verified that the rasters look fine at double size by magnifying the view in which they are displayed by 2x, but of course this magnifies everything else in the web page too, which is undesirable Eg, widgets are double-sized, vector layer symbology gets blurry.

I can easily scale vector symbology, just my multiplying the various width and size parameters by the screen scale factor (this is tested and working), but I cannot do anything similar for rasters. So I think the best work around would be to show them at double size. Eg, grab the tiles that would normally be used if zoomed out by 2x and display them at 2x their actual size.

It seems that this should be possible but I can't figure out how it would be done. Perhaps redefining all the layer's resolutions and setting tilePixelRatio = 0.5 might be part of the solution, but my Openlayers knowledge is limited and I'm not sure if this is even on the right track.

I expected a lot of other people must be trying to deal with the same issue, but I've been unable to find any information that fits this scenario.

Is this possible? How can it be done?

JSFIDDLE UNSUITABLE TO DEMO THIS ISSUE:

I have created a jsfiddle to demonstrate this ( https://jsfiddle.net/nsands/n3cbwr95/20/ ) but jsfiddle is not suitable in this case, as it and/or iOS does its own strange scaling on the iPhone retina screen. With iPhone in portrait mode, everything is too tiny (not just the map, but even the HTML and Javascript panes have text that is unusually small), and when in landscape mode, the map text is large enough, but appears to have been scaled up (it is blurry - actually the kind of behaviour I WANT to achieve elsewhere, but only for raster layers).

ACTUAL DEMO OF THIS ISSUE:

However, this issue can be demonstrated using the WMTS example on the Openlayers examples page at: https://openlayers.org/en/latest/examples/wmts.html

I've loaded this up on both a standard resolution desktop PC and on a 'retina' (HiDPI) iPhone 6S and focused on the same location at the same zoom level on both devices. Below is what it looks like on both (note that the retina image is actually double the resolution shown here, but is the same size as shown here - and it is the size of the elements that is the problem for me).

Note that the elements in the HiDPI/'Retina' version are about half the size of the elements in the standard resolution version. For some people, this is too small to read, and on some maps the elements are rendered even smaller than this.

OpenStreetMap uses fairly large fonts when rendering its tiles. Some map services (such as that provided by my state government) use smaller fonts, and are completely unreadable in Openlayers on retina/HiDPI screens (ie, https://services.thelist.tas.gov.au/arcgis/rest/services/Basemaps/Topographic/ImageServer/WMTS/1.0.0/WMTSCapabilities.xml ).

STANDARD RESOLUTION:

Standard Resolution

'RETINA' (SCALED TO ACTUAL SIZE):

Retina (scaled to actual size)

  • Please create a jsfiddle showing what's wrong otherwise you will get no answer. – oterral Mar 15 '17 at 10:28
  • jsfiddle doesn't work well for this (see example added to post) as it does some weird scaling of its own on the iPhone retina screen (although it does still look terrible there, even when it does scale it up). However, I've added another clear demo as well and linked to the HTML/Javascript (ie, the Openlayers example site). – Son of a Beach Mar 17 '17 at 1:15
  • 1
    I think openlayers.org/en/master/examples/wmts-hidpi.html is what your are looking for. – oterral Mar 23 '17 at 10:43
  • @oterral, as far as I can tell, that example is for high DPI WMTS tiles. I need to be able to use standard DPI tiles since high DPI tiles are not available on all services. I want these standard DPI tiles to be rendered at double the size they usually appear on a high res screen. – Son of a Beach Mar 23 '17 at 20:32
  • Can you just call the specific resolution you want vis TMS? – SeldomSeenSlim Mar 24 '17 at 14:14
1

There's no coincidence about the location, it was based on your jsfiddle example. I've now taken a copy of your function and extended it to work for OSM, XYZ, WMS, WMTS and Bing. You are right about OSM, the constructor is simply shorthand for an XYZ source with

url: 'https://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png',
attributions: ol.source.OSM.ATTRIBUTION,

However, those properties can be read back regardless of which constructor is used (the url will be converted to a urls array).

getTileSize() and getOrigin() actually take an argument (the zoom level). In some services for non-3857 projections there are sometimes different values for each level. It's even possible to have tiles which aren't square. I've converted everything to arrays to be compatible with those. Also the slicing technique isn't compatible with non-3857 services where the resolutions can be irregularly spaced, instead each element of the array needs to be divided by 2 (also dividing means a different scaling constant could be used). For Bing it was necessary to separate the construction of the returned layer and the setting of the source in the layer via a callback as Bing sources load asynchronously.

function newLayerXYZ(sourceType, sourceOptions, layerOptions) {

var layer = new ol.layer.Tile(layerOptions);
var source;
switch(sourceType) {
case "osm":
    source = new ol.source.OSM(sourceOptions);
    setSource2();
    break;
case "xyz":
    source = new ol.source.XYZ(sourceOptions);
    setSource2();
    break;
case "wms":
    source = new ol.source.TileWMS(sourceOptions);
    setSource2();
    break;
case "wmts":
    source = new ol.source.WMTS(sourceOptions);
    setSource2();
    break;
case "bing":
    source = new ol.source.BingMaps(sourceOptions);
    var onKey = source.on("change", function() {
        if (source.getState() == 'ready') {
            ol.Observable.unByKey(onKey);
            setSource2();
        }
    });
    break;
}
return layer;

function setSource2() {
    var urls = source.getUrls();
    var projection = source.getProjection();     // returns a projection object even if set using a string
    var logo = source.getLogo ? source.getLogo() : undefined;  // deprecated in OL5, logos should be included in attributions
    var attributions = source.getAttributions();  // OL3/4 return an object array, OL5 returns a function
    var attributions2;
    if (!Array.isArray(attributions)) {
        attributions2 = source.getAttributions2 ? source.getAttributions2() : attributions;  // special case for Bing with OL4.5/4.6
        if (attributions2) {
            attributions2 = attributions2.bind(source);  // bind needed if custom functions use 'this'
        }
    } else {
        attributions2 = [];                      // must be set as an html array
        for (var i = 0; i < attributions.length; i++) {
            attributions2[i] = attributions[i].getHTML();
        }
    }
    var grid = source.getTileGrid();
    var resolutions = grid.getResolutions().slice(); // take a copy, leave original source unaltered for the WMS tileUrlFunction
    var origins = [];
    var tileSizes = [];
    for (var i = 0; i < resolutions.length; i++) {
        origins[i] = grid.getOrigin(i);
        tileSizes[i] = grid.getTileSize(i);
        if (!Array.isArray(tileSizes[i])) {
            tileSizes[i] = [tileSizes[i], tileSizes[i]];
        }
        if ( doubleSymbologySizes ) {
            tileSizes[i][0] = tileSizes[i][0] * scaling;
            tileSizes[i][1] = tileSizes[i][1] * scaling;
            resolutions[i] = resolutions[i] / scaling;
        }
    }
    var tileUrlFunction = source.getTileUrlFunction().bind(source);  // bind needed if custom functions use 'this'
    var source2 = new ol.source.XYZ({
        urls: urls,
        projection: projection,
        attributions: attributions2,
        tileGrid: new ol.tilegrid.TileGrid({
                resolutions: resolutions,
                tileSizes: tileSizes,
                origins: origins,
            }),
        logo: logo,
        tileUrlFunction: tileUrlFunction  // essential for Bing, WMS and WMTS but is valid for all
    });
    source2.set("originalSource", source);  // makes the WMS/WMTS parameters accessible if required
    layer.setSource(source2);
}

}

There's a new demo at http://mikenunn.16mb.com/demo/tasmania-xyz.htm and http://mikenunn.16mb.com/demo/tasmania-xyz.htm?false will show a standard display for each layer.

The only limitation I've noticed is that OL fails to read Bing attributions when using OL 4.5.x and 4.6.x, which I eventually discovered was due to an OL change where the new functionality is only available using ol-debug.js. Apart from that it now appears to be compatible with OL 3.20.0 through to 5.1.3 (using full builds of OL5). It even works with my UK Ordnance Survey OpenSpace setup, an unusual service with parameters changing at each level which needs customisation ( original maps at http://mikenunn.16mb.com/map/ol3-os-map-basic.htm and http://mikenunn.16mb.com/map/ol3-os-map-basic-wms.htm ) although the pure WMS implemention where changes are needed after the layer is created does need to access the original source to do that, which can be now done from the new source via .get("originalSource").

  • I've now edited the code as there was no need for a special case for WMTS when creating souce2, Using the tilelUrlFunction from the original source takes care of that just as it does for WMS and Bing. – Mike Jul 25 '18 at 10:06
  • I've worked through your code and implemented something similar in my app. It is working for everything I need apart from TMS now. Here's how I create a TMS source in OL before deriving a new scaled source from it: new ol.source.TileImage({ projection: projCode, tileGrid: new ol.tilegrid.TileGrid({ extent: extent, tileSize: tileSize, origin: origin, resolutions: resolutions, }), attributions: attributions, tileUrlFunction: function(coordinate) { return url + coordinate[0] + '/' + coordinate[1] + '/' + coordinate[2] + ext; } }); – Son of a Beach Jul 29 '18 at 4:57
  • TMS is really only an XYZ with the y axis reversed. I suspect your setup would work equally well as ol.source.TileImage or ol.source.XYZ Try passing the options to the function with "xyz" as the parameter. You might also be able to use ` url: url + "{z}/{x}/{-y}" + ext, which would avoid needing a tile url function. – Mike Jul 29 '18 at 10:47
  • I just checked the OL code and the only difference between the TileImage and XYZ constructors seems to be that XYZ will default the tileGrid and projection to 3857 if you don't specify them. – Mike Jul 29 '18 at 12:23
  • I'm trying this out, but it's producing some weird results. The URLs it's producing have the wrong Y value. If I use {y} in the URL, I get numbers such as "-521", for example, as expected. And if I manually remove the "-" from such a URL, it loads a tile that fits the area I was looking at. But if I put {-y} in the URL, I get numbers such as "1541", which is way out of range and doesn't work, of course. I can't figure out, just yet, why it's not simply negating the numbers, but I'm still looking into it. – Son of a Beach Jul 31 '18 at 7:58
2

From v3.20.0 right through to the current v5.0.3 you can set fake resolutions and tile sizes in the tile grid proportionately scaled up and down relative to the real ones and OL will render those regardless of the actual tile size. Here's a simple demo based on your jsfiddle. http://mikenunn.16mb.com/demo/tasmania.htm

It's seems to be an undocumented feature and earlier versions of OL3 rendered using the real tile size received from the server so the result would have been a mess. I've used the technique to fix problems with reprojecting raster layers where OL typically chooses smaller areas (i.e. tiles from detailed z levels) to suit its reprojection algorithm but at the view resolution the resulting compression makes them illegible.

  • Hey, this is so simple and it actually works. Thanks so much - after I gave up on this over a year ago, I have gone back to my project and mostly got it working the way I want now. If you would add some more details to your answer on actual implementation, I would like to mark this as the correct answer. In the meantime, I will add my own answer with some details of what I did. – Son of a Beach Jul 15 '18 at 6:48
1

You also mentioned WMS in the original question. That can also be done but is slightly more complicated since OL would pass the (fake) tile width/height to the WMS service. First problem a cached WMS service restricted to one size might not accept the request, and second problem with a bigger tile there would be no magnification. I've always got around that by setting up the WMS layer as an XYZ layer and simply added the bbox to a tile url preset with the other WMS parameters including the real tile size. But that wouldn't be practical for dynamically created layers. Instead you would need to wrap the default WMS tile url function of a proper WMS layer inside a custom tile url function which resets fake tile sizes in the url back to real sizes before it's used to load tiles. Here's a reworked example using both techniques http://mikenunn.16mb.com/demo/tasmania-wms.htm Hopefully you'll see from the source what I've done better than I can explain in words.

  • WMS is not so critical, but I should also include it, so thanks for this additional information. I will check it out. (And bonus points for your examples all focusing on somewhere that's only 20 minute drive from where I live - was that deliberate, or coincidence?) – Son of a Beach Jul 15 '18 at 22:54
1

Based on the demo in the URL linked to by @Mike, I have now resolved this to my satisfaction. As Mike's answer didn't include the actual details (at the time of writing), I will provide the relevant parts of my solution here. Note that this is a preliminary solution for testing purposes, and is therefore not quite complete yet, as I haven't yet connected the UI to switch between normal and 2x mode to the code.

Note that my project needs to work for dynamically created layers, where the user inputs the details of the data source (eg, the URL template, for XYZ tile layers). AND it must work for XYZ, OSM, TMS and WMTS. Therefore I've had to develop solutions for all 4 of those tile layer types.

The solution for XYZ tile layers is below. The solutions for the other tile layer types are each slightly different, but all based on the same principal of slicing off the last resolution, and doubling the tile size.

In the case of WMTS, I had to double all of the tileSizes, then slice one off, slice off the FIRST resolution and slice off the LAST origin and the LAST matrixId to get it to work (they all have to have the same number of elements).

For OSM, I cannot get it to work. I think this is because OSM is simply a subclass of XYZ and has the tile grid hard coded, similarly to how it has the URL hard coded (just guessing... haven't got around to looking at OpenLayers source code).

function newLayerXYZ(urlTemplate) {
    var projCode = 'EPSG:3857';
    var source = new ol.source.XYZ({
        url: urlTemplate,
        projection: projCode,
        attributions: attributions,
    });
    var grid = source.getTileGrid();
    var origin = grid.getOrigin();
    var resolutions = grid.getResolutions();
    var tileSize = grid.getTileSize();
    if ( doubleSymbologySizes ) {
        resolutions = resolutions.slice(1);
        tileSize = tileSize*2;
    }
    return new ol.layer.Tile({ source: new ol.source.XYZ({
        url: urlTemplate,
        projection: projCode,
        attributions: attributions,
        tileGrid: new ol.tilegrid.TileGrid({ resolutions: resolutions,
                tileSize: tileSize,
                origin: origin,
            }),
        }),
    });
}
-1

Inspired by http://openlayers.org/en/master/examples/xyz-retina.html

Add

 <
    meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width"
    >

to your page may be useful. As you can see from the below scrennshot, it works fine on Apple Devices. enter image description here

  • I'm aware of this optino, but this requires the server to have a separate set of tiles available (see the "2x" in the URL of that example). Therefore this is not an option. I need this to work for a map image service that does NOT cater explicitly for high resolution screens. – Son of a Beach Jul 29 '18 at 5:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.