Leaflet – Correct CRS Options for Proj4Leaflet to Show WMTS Layer

coordinate systemleafletproj4leafletwmts

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.

Best Answer

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