Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom CRS example #31

Closed
imccreadie opened this issue Jun 27, 2020 · 11 comments
Closed

Custom CRS example #31

imccreadie opened this issue Jun 27, 2020 · 11 comments
Labels
enhancement New feature or request

Comments

@imccreadie
Copy link

Is there an example of how to specify the crs parameter for non-geographic maps through the python interface?

The snippet of JS code I'm trying to recreate from leaflet is:

export class MapLayer extends Component {
  constructor (props) {
    super(props);
    this.crs = L.extend({}, L.CRS.Simple, {
      transformation: new L.Transformation(3.12, 0, 3.12, 0)
    });
   ...

Whatever you currently assign to the crs parameter appears to require a function for latLngToPoint

TypeError: "this.options.crs.latLngToPoint is not a function

@emilhe
Copy link
Owner

emilhe commented Jun 27, 2020

In general, it is only possible to manipulate properties from Dash directly, if they can be represented in JSON form. This is not the case for the crs property (as you note, the options include a function pointer).

Therefore, some extra ‘glue’ is needed to translate changes in the Dash prop into changes in the JavaScript object. This is not implemented for the crs property, (yet), but if you decide to give it a try, I would be happy to look at a pull request.

@emilhe emilhe added the enhancement New feature or request label Aug 10, 2020
@Robbie-Palmer
Copy link

I'm looking to choose the Simple CRS without any changes to it
I'm not sure if this is possible via Dash-Leaflet's API, I don't understand what should/could be supplied to the crs property of Map
I found a workaround for now by placing a JavaScript function in the assets folder that registers maps as they're initialised

var maps = [];
L.Map.addInitHook(function () {
  maps.push(this);
});

And by registering a client-side callback that runs a function to alter the CRS when the map loads within the Dash app

change_crs_fn_string = """
function(map_children) {
    var map = maps.pop();
    map.options.crs  = L.CRS.Simple;
    map._resetView(map.getCenter(), map.getZoom());
}
"""
app.clientside_callback(change_crs_fn_string,
                        Output(hidden_div.id, 'children'),
                        [Input(map.id, 'children')])

@emilhe
Copy link
Owner

emilhe commented Sep 11, 2020

That's a super cool hack! Now that i got around to look at this issue again, i figured that it would actually be rather simple to support the built-in CRS options. I just made a sample implementation, where you simple pass the name as a string to the crs property (i.e. Simple in your case). You can try it here,

https://pypi.org/project/dash-leaflet/0.1.2rc2/

Let me know if it works and/or you would prefer a different syntax :)

@Robbie-Palmer
Copy link

Thank-you so much, this is perfect! 🏅
My hack was far from perfect especially since it didn't update any GeoJSON layers, this change fixes this by having this set on the Map before those layers are added

@emilhe
Copy link
Owner

emilhe commented Sep 12, 2020

Great! I'll add to the next release ;)

@emilhe emilhe closed this as completed Sep 12, 2020
@Robbie-Palmer
Copy link

@emilhe I was wondering if the custom CRSs could be implemented by passing the path to a JavaScript function like the GeoJSON pointToLayer example?
geojson = dl.GeoJSON(..., options = dict(pointToLayer = "window.dash_props.module.point_to_layer"))
Maybe that translation of the JavaScript function path to the function pointer is the extra glue you were talking about?

@emilhe
Copy link
Owner

emilhe commented Sep 14, 2020

Yes, that would be an option. It would be more flexible that the current solution (as you could then pass in any CRS), but it would be less user-friendly as it would require the user writing custom JavaScript (as opposed to simply passing a string with the current solution). I haven't used any other CRS than the default myself, so i am not sure if the extra flexibility is needed. Do you have any usecase(s) where the standard CRS object are not sufficient?

@Robbie-Palmer
Copy link

The type of data I'm working with isn't typical for mapping software, as it's not geographical
The Leaflet Simple CRS documentation gives some nice examples of these alternative maps
The maps I'm working with are flat maps with bounds, which works great with the Simple CRS, noWrap option of the TileLayer and by setting the bounds

The GeoJSON data I have treats the bottom-left as the origin rather than the top-left
So I was thinking of using theSimple CRS but flipping the y-axis
It's not a blocker though as is easy to flip the y-axis of the geo-spatial data, but setting it on the map would let me avoid this computation which would be nice as I could have millions of data points
Though there may be a different way to do this than by altering the CRS
The plotly express imshow function for example provides an origin parameter when plotting static images (not maps)

@emilhe
Copy link
Owner

emilhe commented Sep 14, 2020

Ah, i see. In that case, the use of a custom CRS would be neat. I guess it's possible to have the best of both worlds; if a string which is a "normal" coordinate CRS (e.g. "Simple") is provided, that object is created. Otherwise, the string is parsed to a function (which is what you need). I have implemented this approach here,

https://pypi.org/project/dash-leaflet/0.1.2rc3/

You would then just provide the path for the function,

dl.Map(... crs="window.dash_props.module.my_crs")

and create a .js asset like

window.dash_props = Object.assign({}, window.dash_props, {
    module: {
        my_crs: function () {
            return L.CRS["Simple"]  // here goes your crazy CRS
        }
    }
});

The only drawback of this approach is that by allowing arbitrary strings, it becomes possible for the user to get weird error messages if a weird string is provided. Before, the error message was clear since the allowed strings were limited...

@Robbie-Palmer
Copy link

Thanks for being so responsive! It's very appreciated!
I thought changing the CRS to invert the y-axis would make [0, 0] equal the bottom-left corner of my map instead of the top-left
It did make the GeoJSON render in the correct orientation but the TileLayer still had [0, 0] as its top left corner
I have my map working with Simple CRS and by flipping the y coordinates on my GeoJSON so I will be sticking with that for now and won't use a custom function.
Could still be useful for imccreadie's case but if you find it is causing issues with the design, e.g. lack of control of error messages, it's not something I'll be depending on :)

@emilhe
Copy link
Owner

emilhe commented Sep 15, 2020

In that case, i think i'll stick with the default-crs-only option. I think it will be easier to use for most people, and i guess it will cover almost all usecases.

If you just need to do a simple coordinate flip of the GeoJSON coordintates, i guess you could just set the coordsToLatLng option. I haven't tried it, but it seem to be designed with your usecase in mind.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants