4

I'm new to custom projections in Leaflet, and now I'm trying to set up a Leaflet map that uses a WMTS service for its tiles, with the custom projection EPSG:3008 (SWEREF99 13 30).

I'm using Proj4Leaflet to use this CRS with Leaflet, and when setting it up I'm required to specify the proj4 definition which I could easily find at https://spatialreference.org, along with options such as origin, scales/resolutions and bounds.

I've understood that I should be able to get/calculate these values by looking at the GetCapabilities document for this WTMS service. Here I'm using the layer ext_lm:topografiska_nedtonad_3008, the TileMatrixSet New_EPSG:3008 and the TileMatrix EPSG:3008.

As far as I can see, there are 11 levels in this TileMatrixSet. They all have a unique ScaleDenominator and TopLeftCorner values. I've tried to use these ScaleDenominator values for the resolution option without success.

I've read several threads here about calculating these values by multiplying the ScaleDenominator by pixel width (0.00028), and calculating bounds by multiplying TileWidth by MatrixWidth/MatrixHeight without success. I'm not sure how to get the correct options for this specific CRS, and make it work with Leaflet.

My current code is:

const crs = new L.Proj.CRS('EPSG:3008',
  '+proj=tmerc +lat_0=0 +lon_0=13.5 +k=1 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs', {
  resolutions: [200000, 100000, 50000, 25000, 15000, 10000, 5000, 2000, 1000, 400, 200],
});

const defaultPosition = new L.LatLng(12.82956, 56.68970);

const eventMap = L.map('mapid', {
  crs: crs
}).setView(defaultPosition, 0);

L.tileLayer('https://karta.halmstad.se/geoserver/gwc/service/wmts/rest/ext_lm:topografiska_nedtonad_3008/default/New_EPSG:3008/EPSG:3008:{z}/{y}/{x}?format=image/png', {
  maxZoom: 10,
  minZoom: 0,
  continuousWorld: true
}).addTo(eventMap);

This results in some correctly rendered tiles, while most tiles are either wrong or outside the tile range.

I'm not sure what values to look at while calculating the correct option values.

5
  • 1
    One thing is definitely wrong here. Since this is obviously Sweden and Leaflet has [lat, lng] coordinate order as input to it's methods (as it's obvious from method name), it should be const defaultPosition = new L.LatLng(56.68970, 12.82956);
    – TomazicM
    Commented Sep 28, 2021 at 10:08
  • You're right, TomazicM. Good point. I've been messing around with these numbers for some time now, but it should be the other way around. Unfortunately it doesn't make any difference for the missing/incorrect tiles though.
    – Tim
    Commented Sep 28, 2021 at 13:57
  • I would say you are of luck with this layer in Leaflet. It does not confirm to standard slippy map tiles (Google tiles). Zoom level 0 does not have one tile but 5 x 4 tiles, zoom level 1 has 10 x 7 tiles and so on. Besides that not all zoom levels have the same origin. For example, level 6 has top left corner [6314210.0, 72595.7168] and level 7 has top left corner at [6313924.0, 72595.7168].
    – TomazicM
    Commented Sep 28, 2021 at 19:02
  • Okay, so I'm just making sure that I'm following. Do you mean these tiles usually share the same top left corner value? Because that would make sense. But there's nothing wrong with a different number of tiles for every zoom level, right? I mean, that's how it has to be?
    – Tim
    Commented Sep 28, 2021 at 20:54
  • 1
    About slippy map tiles and standard tile naming: wiki.openstreetmap.org/wiki/Slippy_map_tilenames
    – TomazicM
    Commented Sep 28, 2021 at 21:06

1 Answer 1

2

As I wrote in my comment, this layer does not confirm to standard slippy map tiles (Google tiles), and besides that, not all zoom levels have the same origin. This makes it impossible to show layer with vanilla Leaflet, but with some hacking it can be done.

The fact that number of tiles for certain zoom does not conform with the slippy map tile standard can be overcome by overcome by extending L.Map object and using getTileUrl option to fetch the right tiles. Since at zoom level 0 there are 5 X 4 tiles which roughly conforms with zoom level 2, just zoom parameter has to be decreased by 2.

Bigger problem are different map origins for different zoom levels. This can be overcome by catching zoomstart event and setting the right origin for target zoom there. But how to get target zoom? Fot this L.Map object has to be extended and in the _resetView retrieve new zoom level with the internal _limitZoom method and save direction of the zoom in the custom _zoomType property. For his to work, map animation has to be disabled with the zoomAnimation: false option.

Resolutions are calculated from scale denominators by multiplying with 0.00028. Since actual map zoom now start at 2, zooms 0 and 1 are left to Leaflet to be extrapolated from level 2 by setting minNativeZoom option to 2.

Code could the look something like this (tested):

L.MyMap = L.Map.extend({
  _resetView: function (center, zoom) {
    var newZoom = this._limitZoom(zoom); 
    
    if (this._zoom !== newZoom)
      this._zoomType = (newZoom > this._zoom) ? 1 : -1;
    else {
      this._zoomType = 0;
    }

    L.Map.prototype._resetView.call(this, center, zoom);
  }
});

L.TileLayer.Modif = L.TileLayer.extend({
  getTileUrl: function(coords) {
    var x, y, z;
    var url;

    x = coords.x;
    y = coords.y;
    z = this._getZoomForUrl() - 2;           
    url = 'https://karta.halmstad.se/geoserver/gwc/service/wmts/rest/ext_lm:topografiska_nedtonad_3008/default/New_EPSG:3008/EPSG:3008:' + z + '/' + y + '/' + x + '?format=image/png';

    return(url);
  }
});

L.tileLayer.modif = function(templateUrl, options) {
  return new L.TileLayer.Modif(templateUrl, options);
}

var initZoom = 0;
var crsOrigin = [
  [72595.7168, 6326396],
  [72595.7168, 6326396],
  [72595.7168, 6326396],
  [72595.7168, 6319228],
  [72595.7168, 6315644],
  [72595.7168, 6315644],
  [72595.7168, 6314210],
  [72595.7168, 6314210],
  [72595.7168, 6314210],
  [72595.7168, 6313924],
  [72595.7168, 6313924],
  [72595.7168, 6313924],
  [72595.7168, 6313924]
];

const crs = new L.Proj.CRS('EPSG:3008',
  '+proj=tmerc +lat_0=0 +lon_0=13.5 +k=1 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs', {
  origin: crsOrigin[initZoom],
  resolutions: [224, 112, 56, 28, 14, 7, 4.2, 2.8, 1.4, 0.56, 0.28, 0.112, 0.056],
});

const defaultPosition = new L.LatLng(56.68970, 12.82956);

const eventMap = new L.MyMap('map', {
  zoomAnimation: false,
  crs: crs
}).setView(defaultPosition, initZoom);

L.tileLayer.modif('', {
  maxZoom: 10,
  minZoom: 0,
  minNativeZoom: 2,
}).addTo(eventMap);

L.marker(defaultPosition).addTo(eventMap);

eventMap.on('zoomstart', function(event) {
  var newZoom = event.target._zoom + event.target._zoomType;
  eventMap.options.crs.transformation._b = -crsOrigin[newZoom][0];
  eventMap.options.crs.transformation._d = crsOrigin[newZoom][1];
});

That's how map looks like then at zoom 0:

enter image description here

2
  • I can confirm this works, and I'm impressed by and very thankful for your solution. I will experiment with this a bit more when I get the chance. What I noticed when I tried this out now was that Leaflet still tries to load non-existing tiles outside the grid when zooming out and moving outside the actual map, resulting in faulty requests. Maybe this is easily fixed in the map configuration? Also, is there any risk that these modifications to Leaflet might affect the accuracy when referring to specific coordinates on the map? Any caveats?
    – Tim
    Commented Sep 29, 2021 at 14:10
  • Logic of Leaflet tile layer is that it simply tries to load all the tiles that would fall into the current view. To prevent this, getTileUrl option function should request only tiles that exist, getting info about that from some array, filled with info about existing tile rows and columns for specific zoom. This data can be found in the GetCapabilites info. The only problem with this layer is that info does not contain min and max row/column, but just number of rows and columns for each zoom, so some experimenting would be needed to get the right bounds.
    – TomazicM
    Commented Sep 29, 2021 at 16:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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