maplibre / maplibre-gl-js

MapLibre GL JS - Interactive vector tile maps in the browser
https://maplibre.org/maplibre-gl-js/docs/
Other
6.61k stars 713 forks source link

Support rendering in multiple CRS #168

Open jailln opened 3 years ago

jailln commented 3 years ago

Many GIS need to have the ability to render maps in different coordinate reference systems, e.g. local ones, whether for precision or for displaying data of users in their source coordinate systems. From what I know, maplibre always displays maps using the web mercator projection (EPSG:3857).

Since I'm not familiar with maplibre code, I'm wondering how hard would it be to allow displaying maps in different projection systems ? Would it require a major refactoring ?

In addition, I'm also wondering if other people would be interested by this feature and if it could be something that goes in the roadmap ?

Cheers

wipfli commented 3 years ago

As far as I know web mercator is the only projection available. And it sounded like it suits peoples needs quite well. You can transform mouse positions and marker locations on-the-fly I guess. Or what would be your use case for anything different than web mercator?

kylebarron commented 3 years ago

Other projections would be really nice, for example to work with polar data or to avoid web mercator distortion issues, but it would be a HUGE undertaking for a general case. First you'd need to support the rendering and placement of non-web mercator tiles. Then you'd need to create your own basemap because the vector tile basemap pipeline Mapbox uses only supports Web Mercator data.

Note that you can hack alternative projections into Mapbox/Maplibre GL in specific circumstances. For example, the New York Times provides its interactive maps of the U.S. (e.g. elections and Covid cases) in an Equal Albers projection. (Note the northern border with Canada curves instead of being a straight line as it would be in Mercator).

image

If you're willing to do your own math for your custom vector tiles, you can achieve something like this by using the lat/lon boundaries of [-180, -90, 180, 90] as an arbitrary canvas. So you'd convert your data from their coordinates in their non-Web Mercator projection to their relative points in this canvas without reprojecting. But of course those NYT maps can't use a Mapbox basemap because the Northeast of the U.S. would correspond to "upper right" in web mercator and load basemap data from Russia.

davenquinn commented 3 years ago

This seems quite out of scope for this library, as it basically only speaks Web Mercator. Other mapping libraries (ex. d3 as @kylebarron mentioned) are probably more fit for your needs here. There are also ways to "fool" web mercator for polar data etc.

jailln commented 3 years ago

As far as I know web mercator is the only projection available. And it sounded like it suits peoples needs quite well. You can transform mouse positions and marker locations on-the-fly I guess. Or what would be your use case for anything different than web mercator?

Mainly to avoid web mercator distortions issues for e.g. measurements or edition, without having to transform positions on the fly every time. Also, to display raster data in local projections.

Other projections would be really nice, for example to work with polar data or to avoid web mercator distortion issues, but it would be a HUGE undertaking for a general case. First you'd need to support the rendering and placement of non-web mercator tiles. Then you'd need to create your own basemap because the vector tile basemap pipeline Mapbox uses only supports Web Mercator data.

Note that you can hack alternative projections into Mapbox/Maplibre GL in specific circumstances. For example, the New York Times provides its interactive maps of the U.S. (e.g. elections and Covid cases) in an Equal Albers projection. (Note the northern border with Canada curves instead of being a straight line as it would be in Mercator).

image

If you're willing to do your own math for your custom vector tiles, you can achieve something like this by the lat/lon boundaries of [-180, -90, 180, 90] as an arbitrary canvas. So you'd convert your data from their coordinates in their non-Web Mercator projection to their relative points in this canvas without reprojecting. But of course those NYT maps can't use a Mapbox basemap because the Northeast of the U.S. would correspond to "upper right" in web mercator and load basemap data from Russia.

Thanks for this detailed answer! I guess that some parts of the code assume that tiles are are web-mercator and so th'at why it would require many changes to make it a general case.

This seems quite out of scope for this library, as it basically only speaks Web Mercator. Other mapping libraries (ex. d3 as @kylebarron mentioned) are probably more fit for your needs here. There are also ways to "fool" web mercator for polar data etc.

I'm suprised that you consider this out of scope, could you elaborate on why please ? Regarding d3, do you mean d3js ? It is more focused on dataviz than on GIS from what I know ?

Also, I just found out that the mapbox team have started working on this: https://github.com/mapbox/mapbox-gl-js/issues/3184#issuecomment-831385021

github-actions[bot] commented 3 years ago

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days.

PacoDu commented 3 years ago

mapbox-gl-js recently introduced non-mercator projection support in https://github.com/mapbox/mapbox-gl-js/pull/11124

iacopoff commented 2 years ago

TBH this would be extremely useful for rendering rasters representing graph-like data such as rivers. You don't want to reproject/regrid those rasters to Pseudo-Mercator as you would completely disrupt the topology.
Is there an interest in moving this forward?

wipfli commented 2 years ago

I think there is interest, yes. Are you in the slack channel @iacopoff?

wipfli commented 2 years ago

https://roadmap.maplibre.org/c/91-custom-coordinate-system-epsg-non-mercator-projection

erikvullings commented 1 year ago

As far as I know web mercator is the only projection available. And it sounded like it suits peoples needs quite well. You can transform mouse positions and marker locations on-the-fly I guess. Or what would be your use case for anything different than web mercator?

Another use case is the following: In The Netherlands, the government offers a free vector tile service of the whole country, which you can include in your own web apps, However, they offer the vector tiles in RD/EPSG:28992, basically a rectangular grid introduced by Napoleon with the center in Paris. The reason is that many open data in NL is in this format too. However, I cannot use them in MapLibre and therefore need to host my own vector tiles, which is a nuisance.

HarelM commented 1 year ago

@erikvullings You might be able to use addProtocol to convert the data from one projection to another, but I'm not sure if the tiles use the regular x-y-z grid that is used by maplibre, and in that case I'm not sure what your options are. If this something you would like to introduce to maplibre you are more than welcome to submit a PR :-)

geekdenz commented 1 year ago

A hint where to find the code with which tiles are read into geometries would be very helpful.

HarelM commented 1 year ago

The worker that reads the data from the network: https://github.com/maplibre/maplibre-gl-js/blob/bd37870b6fcc613a021c523d4eb5aa8638d71382/src/source/vector_tile_worker_source.ts#L134C9-L134C9 Worker tile is responsible for parsing the data: https://github.com/maplibre/maplibre-gl-js/blob/bd37870b6fcc613a021c523d4eb5aa8638d71382/src/source/worker_tile.ts#L89C19-L89C31 https://github.com/maplibre/maplibre-gl-js/blob/bd37870b6fcc613a021c523d4eb5aa8638d71382/src/source/worker_tile.ts#L102

This is just the parsing of the vector tiles, if you want pointer to other part of the code, let me know and I'll look them for you. Having this in this library would be awesome, I'd love to help anyone who would like to help push this through.

geekdenz commented 1 year ago

Wow that was a super quick answer. Considering there is proj4js reprojecting on the fly might be reasonably doable. There might be some caveats though that I am missing. As always the devil is probably in the detail. But I am using this library for a project of mine and I need support for other projections.

HarelM commented 1 year ago

Let me know if you would like me to assign a bounty for it and which bounty size you think is appropriate. Bounty direction: https://github.com/maplibre/maplibre/issues/272

In order to avoid bounty size misalignment, I would consider creating a design first detailing where this change impacts as a first bounty and then decide on the implementation bounty size.

But these are just my thoughts, let me know how you would like to proceed.

geekdenz commented 1 year ago

I'll have a poke around and get a debug session going for a while before committing to anything. I might be biting off more than I can chew. However, this would be a project right up my alley and expertise.

HarelM commented 1 year ago

Cool, looking forward to hearing more from you. Note that there are multiple README.md files scattered in this repo, each one with its own info, I would advise to try and read as many of them as you need.

geekdenz commented 1 year ago

Thanks @HarelM ! Definitely very interested, also in the native implementation since I did quite a bit of competitive programming with C++ and I've got extensive experience with TypeScript. Would be interested in the bounty as well as the work.

However, I'll need quite some time to familiarize myself with the project first. Planning on reading the native book and some source code before I drive into a design. If someone else beats me to it I'd encourage them. Maybe it could be a friendly competition as is the spirit of FOSS.

geekdenz commented 1 year ago

Would this need a styles change or would projections be a part of the API and handing it to the Map options?

If it is in styles, this might need quite a bit of thought and maybe discussion as hinted in the CONTRIBUTING.md file.

I guess you could provide optional projections in the style document and specify a default. But if we are reprojecting on the fly, why not just have it part of the API and Map options for now. That would be a faster way to get to the smaller goal.

What do you think?

HarelM commented 1 year ago

I think we can safely start with the API. After that (or in parallel) we can have a discussion about how it should be in the style. I think the end goal would be to define it in the style, but a solution in the API would be a very good way to test it and see how it looks and behaves.

geekdenz commented 1 year ago

Apologies. Haven't replied for quite a while.

This is not the highest on my priority list at the moment. So I'm happy if someone else takes over.

However, I'm still very interested in this problem and apart from work and my current side project and of course family, this is the next item on my priority list. I expect to finish my first iteration on my side project in about 4 weeks which is when I'd need the projection feature of maplibre gl js.

eslindsey commented 1 year ago

I'd love to experiment with the performance of this library vs. Leaflet, but my application is for a game and so currently uses Leaflet's CRS.Simple. Support for non-Earthlike CRS would be fantastic!

geekdenz commented 9 months ago

I am still keen to work on this. I am time-constrained as an engaged father who also has a full time job and many commitments though so please understand that I cannot sink too much time into this.

Also, I am concerned it might be quite hard to achieve. But that is also the attraction. Plus it would help a hobby project of mine.

Would it make sense to do the 2D part of this first, since it is easier?

Is it as simple as using the proj4js library and hooking that in somewhere?

I only really need the 2D part in my project. So, this is a selfish consideration. Also, it might be easier as a hack or an extension of sorts only reprojecting on the client. My interest is in that mainly, so that existing tiles can be used.

HarelM commented 9 months ago

I think an initial work on this has started as part of https://github.com/maplibre/maplibre-gl-js/issues/307 and https://github.com/maplibre/maplibre/discussions/161 2D only is a possible way, I think the projection of the vector stuff might be complicated and I'm not sure proj4js would solve everything easily, as if you only project back to lat-lon you still have issues with the poles, which is the main reason as far as I understand, people need other projections. But I'm no export on this, below are the relevant people. cc: @Pheonor @kubapelc

kubapelc commented 9 months ago

Hi, first of all, I've only skimmed through the discussion, so I might have missed some context. I'm implementing vector globe projection, which is a similar problem. I'm basically doing reprojection on-the-fly in the vertex shader, taking vertices in tile coordinates (basically mercator) and projecting them to a sphere. In my code, both old mercator projection and new globe projection use the same vertex buffer.

There is an important caveat though - since I'm only reprojecting the vertices of a polygon, its edges remain straight, which is a problem especially for large polygons. What is a straight edge in mercator projection will become a curve on a globe. To fix this, I first subdivide every polygon (and other features too), so that I get a better approximation of a curve when reprojecting the vertices. This subdivision step is relatively complex and it's hard to make it fast enough (otherwise tile loading gets noticeably slower).

Since proj4js seems to only reproject individual coordinates, the problem of edges not getting curved would remain. The easiest way to implement other projections would be to use my vector globe which already subdivides polygons and edges, and "just" replace the mercator->globe projection in the vertex shader with a custom projection, and possibly tweak how much subdivision happens at what zoom level. This would also mean that the reprojection code would need to be rewritten to GLSL.

geekdenz commented 9 months ago

Cool @kubapelc !

Thanks for reaching out.

I think reprojection can assume that only lines exist. The line segments should be small enough that curvature does not need to be implemented or if, it should be a setting that is off by default, especially if there is a performance hit.

But that is just my opinion and a thought.

Think agile and small increments. Curving lines is definitely a nice to have rather than a must imho.

Pheonor commented 9 months ago

Effectively, 'globe' could be consider as a specific projection and to switch between 'mercator' and 'globe' a first step on a projection class have been implemented but with very limited support. This class should at least to encapsulate the project / unproject methods instead of direct call to mercatorXfromLng, mercatorYfromLat, lngFromMercatorX and latFromMercatorY.

My current version is based on an abstract class:

import {LngLat} from './lng_lat';

/**
 * A `ProjectedPoint` represents a projected point.
 * It allows to store the 3D coordinates of any point.
 */
export class ProjectedPoint {
    x: number;
    y: number;
    z: number;
}

/**
 * A `Projection` abstract class to represent a projection coordinate system.
 * It could represent a 3D globe projection or any 2D projection.
 * It allows to project and unproject coordinate to and from the coordinate system.
 *
 * @group Geography and Geometry
 */
export abstract class Projection {
    name: string;

    constructor(name: string) {
        this.name = name;
    }

    abstract isGlobe(zoom: number): boolean;
    getFactor(_zoom: number): number { return 0.0; }
    abstract project(lng: number, lat: number, zoom: number) : ProjectedPoint;
    abstract unproject(x: number, y: number, zoom: number) : LngLat;
}

But propably we should add some options into constructor to manage projection parameters.

As @kubapelc and @geekdenz explain, the tesselation of mesh to a sufficient level coud be sufficient to transform the mercator geometry into another projection and manage the different curvature.

exotfboy commented 8 months ago

Regarding multiple CRS, there can be varying interpretations:

I believe that the first option (1) is sufficient and simpler to implement.

wipfli commented 8 months ago

@pka you did something like this, right?

pka commented 8 months ago

@pka you did something like this, right?

I did option (1) to display tiles in Equal Earth projection with MapLibre:

image

(Screencast)

I'm currently investigating to make the tile grid more similar to Web Mercator, but for many applications the coordinate transformation from geographic coordinates to the tile CRS is needed. I plan to provide that for Equal Earth projection for all major map viewer libraries.

HarelM commented 8 months ago

@pka note that there's a globe effort which touches some aspects of this I believe. The first PR is underway here: #3783. I would recommend seeing which part is similar and what can be used to maybe create a PR that only addresses this part without the full implementation of a globe so that this can be added regardless of the globe's progress. Meaning if we can have this feature implemented and merged while continue working on the globe in parallel, this way we bring more value to customers early on.

scaddenp commented 7 months ago

I am all for the simple version. Two use case - one is large no. of vector tile sets being published in our national grid system. They are the common and natural basemaps to use. No. 2 is for work in Antarctica. Web mercator is seriously misleading there.

NicholasEwing commented 2 months ago

Still waiting on this to be resolved - is there still no equivalent of Leaflet's CRS.Simple?

geekdenz commented 2 months ago

There might be a way to do this in an extension. Is this correct @HarelM ?

HarelM commented 2 months ago

Depending on what you would like to achieve. In theory, you could create a source that traslates to wgs84 (or via addProtocol), but I'm not sure this is the definition of supporting different CRSs...

scaddenp commented 2 months ago

That wouldn't work for displaying vector tiles in CRS of choice. Option 1 would be an improvement for us on this - generally want to display vector tiles in the CRS they were created in. I would be keen to know how @pka achieved that. Was this on a fork of maplibre??

dzfranklin commented 1 month ago

For anyone looking to reproject raster tiles sticking openlayers in a custom protocol kinda works surprisingly well.

This example doesn't handle cancellation and the tile fetching is completely separate so it will have separate concurrency limits and so on.

https://jsfiddle.net/nfq6uLe1/35/

ml.addProtocol(... ```typescript ml.addProtocol('os-explorer', async (params, abortController) => { const url = new URL(params.url); const path = url.pathname.replace(/^\/\//, ''); if (path === 'tile') { const z = parseInt(url.searchParams.get('z')!); const x = parseInt(url.searchParams.get('x')!); const y = parseInt(url.searchParams.get('y')!); if (isNaN(z) || isNaN(x) || isNaN(y)) throw new Error('bad params'); const data = await reprojectTile([z, x, y]); return { data }; } else { throw new Error('not implemented: ' + params.url); } }); const tileImageSource = new TileImageSource({ url: 'https://api.os.uk/maps/raster/v1/zxy/Leisure_27700/{z}/{x}/{y}.png?key=' + OS_KEY, projection: 'EPSG:27700', wrapX: false, crossOrigin: '', tileGrid: new TileGrid({ resolutions: [896.0, 448.0, 224.0, 112.0, 56.0, 28.0, 14.0, 7.0, 3.5, 1.75], origin: [-238375.0, 1376256.0] }), reprojectionErrorThreshold: errorThreshold, }); function reprojectTile(coord: TileCoord): Promise { console.log(`reprojectTile: requesting ${coord.join('/')}`); return new Promise((resolve, reject) => { const tile = tileImageSource.getTile( coord[0], coord[1], coord[2], 1, proj3857, ) as ReprojTile; tile.addEventListener('change', () => { switch (tile.getState()) { case TileState.LOADED: { const canvas = tile.getImage(); canvas.toBlob((blob) => blob!.arrayBuffer().then(resolve)); console.log(`reprojectTile: reprojected ${coord.join('/')}`); return; } case TileState.ERROR: { reject(new Error('openlayers: reprojection tile in error state')); return; } } }); tile.load(); }); } ```
neodescis commented 1 month ago

That's a really nifty idea. Thanks for sharing!