Leaflet: change the crs property of map dynamically

Created on 19 Mar 2014  路  34Comments  路  Source: Leaflet/Leaflet

Recently one of our project need to change the crs at run-time(during we change the base layer of the map), and I found someone have meet the same requirement through #1818 , I have tried the manner it mentioned(by using the baselayerchange event, however the author use leaflet 0.5 while I am using 0.7.2 . Which means that it does not work.

So I tried to make it myself, this is what I tried:

layer.on('add',function(){
    var crs=getCrsByLayer(this);
    map.options.crs=crs; //reset the crs of the map

    //we have to clear this two properties, or the coord of the tile would be re-wrapped wrongly by the former crs of the map
    layer._wrapLng=null;
    layer._wrapLat=null;

       //it seems that the tiles calculation procedure have been done before the add event fired, so hack it to redraw the layer.
    layer.redraw();
});

However it does not work, so I tried to dig into the codes, and finally I found that the _initialTopLeftPoint property of the map does not changed when I reset the crs through map.options.crs, which means the tiles bounds will not calculated correctly. Since this is a private property, I do not think set it outside is a good idea.

So I think if you can re-consider this feature? It does not cause more codes, you just make the crs related property can be re-initialized.

Thank you.

feature later projections

Most helpful comment

It's the same problem at /issues/2553.And I use the baselayerchange Layer events to change multiple maps,Baidu use the CRS EPSG:900913,other map use EPSG3857.This is my demo:https://lhywell.github.io/map/example/example2/leaf2.html

map.on('baselayerchange', function(layer) {
        let center = map.getCenter();
        let zoom = map.getZoom();
        console.log(map.options.crs)
        if (layer.name.indexOf('鐧惧害') > -1) {
            map.options.crs = L.CRS.Baidu;
            map.options.tms = true;
            map._resetView(center, zoom, true);
        } else {
            map.options.crs = L.CRS.EPSG3857;
            map.options.tms = false;
            map._resetView(center, zoom, true);
        }
})

All 34 comments

You're right, I should look into this.

@perliedman thoughts?

My gut feeling is that allowing changing the CRS would introduce complexity that we don't necessarily want. It's sort of an unusual case, and at the same time forces any code dealing with pixel and/or projected coordinates to be aware that the CRS might change at any time. This might be especially hard for plugin writers, who are not likely to think of or test this scenario.

I would rather recommend using multiple map instances for this case.

You mentioned using multiple map, but how about the overlays? like the markers,polylines,polygon and etc in the former map? Add them to the new map one by one?

And I think the crs feature may not cause as many complex situation as you thought, people who does not care it can ignore it as if it does not exist. People need it can benefit from it .

For the plugin writers, if the feature the plugin provided is related to the crs, then I think the author must know it and have the according knowledge about the crs. Otherwise, it has no effect for the writer.

I'm pretty certain I've written plugins that would not handle CRS change gracefully, so it's perfectly possible while still having the knowledge about CRS.

Of course, you could say this is an error on my part, and that's sort of what we're discussing, but it certainly makes things a bit more complicated, since there's no way to know, as a plugin author, when the CRS changes.

So is using multiple map the only choice? Then, synchronizing the controls,markers,polylines and etc between different maps will be a nightmare. :(

At my first thought, I prefer to add two methods to map:

setCRS() and getCRS()

And expose an crs change event to the components which are crs sensitized.

See this http://jsfiddle.net/Hb5Hx/1/. Set the base layer with different crs, I do not change any other codes like the maker and etc, it just works. :)

I think an API along those lines would be preferable if we decide that it's worth supporting CRS changes after the map is created.

It seems that leaflet does not support changing crs at runtime because of two main aspects:
1 It will make the codes complicated than before.
I admit that it may.

2 There are not so much requirements.
However I think this may be not the truth. The commercial api like google map support this(through different maptype width different projection).

Take a look this:https://google-developers.appspot.com/maps/documentation/javascript/examples/full/aerial-simple

The 45掳 imagery is another kind of projection(if not, fix me).
And we have the same images like this which we want to combine them with the general tiles, that's why I prefer to this feature personally so much :).

And finally, if this feature can not be added to the core API, can you give me some suggestions to hack it? Though I have make it seems to work, but I use so many private internal methods which I think is unreliable, since the methods may changed sometime.

And in fact, I plan to create a plugin like the google map type control, each maptype can have it own crs. I hope this can be done with a reliable way. :)

Thanks for providing a use case for this - this seems like it could be more useful than I first thought.

However - are there any 45 degree tile sets available (AFAIK you're not allowed to show Google Maps tiles with any other code than Google Maps)? How does that CRS work? Any documentation available?

Have you tried what happens if you manually reset _initialTopLeftPoint that you mentioned in your initial comment? Are there other issues in Leaflet that prevents CRS from being changed after the map is created?

1 For the tile sets.
I admit that I have not found any 45 degree tile like google map or other 2.5D tiles for free use. In our application we create our own 45 degree tiles with building models on top of that.

For the crs, I think it does not use the popular CRS like 3857 4326 or something else. In our example, we use the custom crs with a custom transformation.

2 I have tried yet, and this is the result(I have posted it in my last comment)
http://jsfiddle.net/Hb5Hx/1/

The is what I done when I tried to change the crs after map is created:

               //remove the last layer
        map.removeLayer(wms);
               //change the crs
        map.options.crs = crs;
                //re-calculate the leftpoint
        map._initialTopLeftPoint = map._getNewTopLeftPoint(map.getCenter());
                //add the other layer
        map.addLayer(osm);
        //update the overlays(only mk and mk2 in example)
        mk.update();
        mk2.update();

And I found that there is a explicit issue: the map center is changed when switch to the other crs.
I have not checked what is the problem.

@perliedman , how about that?

This is my humble opinion on this feature:

It is kind of an unusual feature to request (I can't find any other issue requesting this, and not anything on uservoice). Given that, and the uncertainties on what effects this feature would have on both the core codebase and the plugins, I don't think you can expect this feature to be added by someone else, at least not in the short term.

Having said that, if you really need this feature, I really think you should go for it and implement this feature, and provide a pull request. Without more technical detail, it is probably hard to determine if this is a feature that should be included in Leaflet; perhaps it could even be made into a plugin (even if I have a bit of a hard time seeing how that would work). I would be happy to try to understand any questions you have, but it's much easier if you have actual code to review and test.

The worst thing that could come out of that is that you have a Leaflet fork with working CRS switching, which I guess would still be almost what you want.

@mourner do you have any opinion on the priority of this feature, and/or the difficulty in supporting it?

I like the idea that every map layer could have it's own projection. I added the tiles of a local map provider (swisstopo) that use a different projection along with the openstreetmap tiles and it would be great if I could switch to this map layer on the fly.
Just to let you know, that there are other people who would like this feature.

I agree with @AndyBlah. I have two map layers with different CRS. Right now I don't know how to handle this situation

@Hashedhash, I have found a solution, it works but as not well as I'd like to. Something like this:

map.removeLayer(map.curLayer);
var center = map.getCenter();
map.options.crs = map.newLayer.crs;
map.setView(center); //we need this, because after changing crs the center is shifted (as mentioned above probably it's an issue to)
map._resetView(map.getCenter(), map.getZoom(), true); //we need this to redraw all layers (polygons, markers...) in the new projection.
map.addLayer(map.newLayer);

@guliash, thanks for the solution, I guess it solves this issue. But in my case I have 2 layers added to the map with different CRS (EPSG3395 and EPSG3857) and I found out already that it is not possible to make it work with Leaflet right now

It is a hard job to convince the op. :)

I have this requirement as well, coming from scientific data on maps, where you want to switch seamlessly between e.g. different arctic projections (which sometimes is just rotation): http://webmap.arcticconnect.org/examples/Leaflet.PolarMap/polar-projections/index.html (I'm not affiliated with that website) I think it would be good to check what these guys did to make it work https://github.com/GeoSensorWebLab/polarmap.js and whether they had to use workarounds etc.

@neothemachine :+1: Good job.

different crs for base layer and overlay https://github.com/Leaflet/Leaflet/pull/4150

js fiddle example: http://jsfiddle.net/alekzonder/qxdxqsm3/

I have a use for this, too, for displaying non-map data that can be stretched or compressed horizontally in addition to the usual zooming (in my case, the horizontal coordinate represents time).

The status of this issue is still more or less described in my comment https://github.com/Leaflet/Leaflet/issues/2553#issuecomment-39296082.

To go forward with this, we have to discuss an actual proposal. Making it possible to switch CRS after the map is created _might_ be a good idea (but allowing layers with different CRS is not).

Someone who needs this feature will most likely have to step up and see what is required and if it is feasible. Don't hold your breath hoping that Leaflet devs will do this.

Also, as mentioned above, I'm open to discussing and trying to answer any questions on how to develop this.

(just a note): It might be interesting to see how https://www.windyty.com does the switch between level 4 and 5 to/from 3D globe. They are using Leaflet 0.7.5 and somehow manage this switch without reloading.

@hyperknot they use two completely different libs. The globe is drawn on canvas, not using Leaflet. Check the DOM inspector, and you can see that there's one Leaflet map and another div for the globe, they show/hide the globe when you zoom in or out.

@perliedman thanks, I see. So it's basically a hack of synching two viewers on top with an opacity 0/1.

I need this enhancement as well. My use case:
I use base layers from different sources with different CRS, for example 'OSM' and 'Dutch Aerial'.
The Dutch data is in the Dutch CRS.
I also load vector data from a WebAPI server. When calling this server I can add the CRS so I get the data back in the correct CRS. But I need to know that the user switched the base layer and changed the map crs. Eventually I'll be using ngx-leaflet from Asymmetrik.
This is a real show stopper for us. If Leaflet can't handle this I need to use a different map component.

@pmeems hi there, from what I can tell by your description, the vector data should not be a problem: Leaflet _always_ use WGS84 for its API, so this wouldn't change even if you alter the CRS of the map. However, you would need a way to change the CRS if your tilesets use different projections.

As I see it, you have two options to go ahead with this:

  • Use one of the workarounds posted above (I think there might be other workarounds posted in related issues as well)
  • See to that this feature is implemented in Leaflet - my earlier comment still holds, don't count on anyone just stepping up and doing this - either consider contributing this functionality (we would be 馃帀 馃敟 if you did), or maybe consider sponsoring development of this feature if it's important to you

BTW, I'm not sure how to interpret the "If Leaflet can't handle this I need to use a different map component", but it's easy to read as some sort of threat. Please consider that you are not a Leaflet customer: Leaflet is open source, and we don't make money from you using Leaflet. We're happy if you use Leaflet, and we're also happy to try to help you to do so within reasonable limits, but if you prefer to use another library, that is also totally fine with us. Again, if a certain feature is important to you, think about adding it (we're happy to help answering questions and give feedback), or maybe sponsor someone to add it.

Thanks Per for your quick response.
I didn't mean the threaten you. I know you are all doing great work and mostly as volunteers.
I can't contribute myself, this out of my debt but I will internally discuss the sponsor suggestion.

About your remark about the vector data. Do you mean that the vector data is always reprojected on the client side to WGS84? If so it would improve performance when I also request the data in WGS84, right?
The data is in WG84 in our PostGIS database and we reproject on the server-side before sending the data back.

@pmeems alright, nice to hear.

Re: projections, yes, Leaflet vectors are always created from WGS84 (that is, you input WGS84 data), and it's reprojected on the client to the current CRS. So if your data is WGS84 in your database, no need to reproject it to anything else. If your data is in some other format, you can for example use Proj4Leaflet or reproject to get it working with Leaflet's API.

It's the same problem at /issues/2553.And I use the baselayerchange Layer events to change multiple maps,Baidu use the CRS EPSG:900913,other map use EPSG3857.This is my demo:https://lhywell.github.io/map/example/example2/leaf2.html

map.on('baselayerchange', function(layer) {
        let center = map.getCenter();
        let zoom = map.getZoom();
        console.log(map.options.crs)
        if (layer.name.indexOf('鐧惧害') > -1) {
            map.options.crs = L.CRS.Baidu;
            map.options.tms = true;
            map._resetView(center, zoom, true);
        } else {
            map.options.crs = L.CRS.EPSG3857;
            map.options.tms = false;
            map._resetView(center, zoom, true);
        }
})

@lhywell Any idea for updating locations of Marker, Polygon, Polyline, etc. after changing CRS? Convert latlngs with new CRS and update them one by one? If so, that's too terrible.
BTW, after that CRS changed, center should be converted to a new point which uses new CRS, but not the same as before, right?

I wanted to use the layer control to change between layers with different CRS's with different zoom levels (WGS84 / British National Grid).

I found I had to override Control.Layers._checkDisabledLayers so that the layers weren't disabled in the layer control; set map.options.crs on a baselayerchange event; and invoke map.fitBounds() in both the baselayerchange and the zoomend event handlers.

L.Control.Layers.prototype._checkDisabledLayers = function() {};

map.on('baselayerchange', function(layer) {
    map.options.crs = layer.name.split(':')[0] == 'OS' ? crs27700() : L.CRS.EPSG3857;
    map.fitBounds(bounds);
    map.baselayerchange = true;
});

map.on('zoomend', function() {
    if (map.baselayerchange) map.fitBounds(bounds);
    delete map.baselayerchange;
});

A full working example is at movable-type.co.uk/dev/track.trail/leaflet-layer-control-multi-crs.html, with further notes; code is viewable through View Source.

This is how I ended up doing it:

function getMaxBounds(crs) {
  const { bounds } = crs.projection;
  return new L.LatLngBounds(
    crs.unproject(bounds.min),
    crs.unproject(bounds.max),
  );
}

function changeCRS(map, crs) {
  const bounds = map.getBounds();
  map.options.crs = crs;
  // Ensure zoom is not affected by differing CRS scales
  map.options.zoomSnap = 0;
  map.fitBounds(bounds);
  map.setMaxBounds(crs instanceof L.Proj.CRS ? getMaxBounds(crs) : null);
  map.options.zoomSnap = 1;
}

If using a L.markerClusterGroup, you'll also need to do this after changeCRS:

const layers = markerClusterGroup.getLayers();
markerClusterGroup.clearLayers();
markerClusterGroup.addLayers(layers);
Was this page helpful?
0 / 5 - 0 ratings