maplibre-gl-raster-reprojection v1.0.4
Maplibre Gl Raster Reprojection
Reproject maplibre raster map tiles in the browser.
Purpose
This library is for when your maplibre map projection differs from your tile projection.
In a perfect world your map tiles would be in the same projection as your map. However, this is not always the case. Sometimes you may need to mix. For example,
- Your map and some of your tiles are in EPSG:3857 (web mercator), but you need to another tileset that is only served in EPSG:4326.
- Your map is in some exotic projection and there are only EPSG:3857 tile providers.
With maplibre-gl-raster-reprojection you can reproject those tiles on the fly so that you can use them.
This library is inteded to be a stopgap solution until non-mercator projections are supported in maplibre. See latest updates:
- Roadmap - Non-Mercator Projection
- Bounty Direction: Custom Coordinate System/EPSG/Non-Mercator Projection #272
How it works
This library uses maplibre addProtocol API to hook into the layer request/response lifecycle.
- Maplibre makes a request for a tile in EPSG:3857
maplibre-gl-raster-reprojectionconverts that request into 1 or many tile server requestsmaplibre-gl-raster-reprojectionslices and reprojects the tile server responses into 1 in order to match the maplibre expected request- Maplibre renders the repojected tile
Key Terms
- source: Original tile from the tile server
- destination: Maplibre tile (EPSG:3857)
Maplibre does not directly pass the tile request bbox, x, y, and z params to the protocol loader function. You must add a url prefix and url source params to your tile url in order for maplibre-gl-raster-reprojection to receive those values and build requests.
Install
npm install maplibre-gl-raster-reprojectionUsage
You must add the following url prefix and source params to your maplibre raster source config in order for maplibre-gl-raster-reprojection to work:
- Add the url prefix to your tile url
- Use the url source params to your tile url
URL Prefix: reproject://bbox={bbox-epsg-3857}&z={z}&x={x}&y={y}://
reproject: The protocol name. Can be changed viaprotocoloption.bbox={bbox-epsg-3857}: Pass destination (EPSG:3857)bboxto the loader function.z={z}: Pass the destination (EPSG:3857) tilezto the loader function.x={x}: Pass the destination (EPSG:3857) tilexto the loader function.y={y}: Pass the destination (EPSG:3857) tileyto the loader function.
URL Source Params:
{sz}: Pass the source tilezto the tile server request{sx}: Pass the source tilexto the tile server request{sy}: Pass the source tileyto the tile server request{sbbox}: Pass the source tilebboxto the tile server request{sxmin}: Pass the source tilexminto the tile server request{symin}: Pass the source tileyminto the tile server request{sxmax}: Pass the source tilexmaxto the tile server request{symax}: Pass the source tileymaxto the tile server request
Example URL:
reproject://bbox={bbox-epsg-3857}&z={z}&x={x}&y={y}://https://api.tilehost.com/map/{sz}/{sx}/{sy}.png
import maplibregl from 'maplibre-gl';
import { createProtocol, epsg4326ToEpsg3857Presets } from 'maplibre-gl-raster-reprojection';
const { protocol, loader } = createProtocol({
// Converts EPSG:4326 tile endpoint to EPSG:3857
...epsg4326ToEpsg3857Presets,
// Draw EPSG:3857 tile in 256 pixel width by 1 pixel height intervals (more accurate latitude)
interval: [256, 1],
});
maplibregl.addProtocol(protocol, loader);
const map = new maplibregl.Map({
style: {
...,
sources: {
version: 8,
epsg4326Source: {
type: 'raster',
tiles: ['reproject://bbox={bbox-epsg-3857}&z={z}&x={x}&y={y}://https://api.tilehost.com/map/{sz}/{sx}/{sy}.png'],
tileSize: 256,
scheme: 'xyz'
}
},
layers: [
{ id: 'reprojectedLayer', source: 'epsg4326Source', type: 'raster' }
]
}
})API
createProtocol: (options: CreateProtocolOptions) => CreateProtocolResult
Create and initialize input for maplibregl.addProtocol.
CreateProtocolOptions
| field | description |
|---|---|
protocol | string Url prefix that identifying a custom protocol. (Default: 'reproject') |
cacheSize | number Total images stored in the internal reprojection cache. (Default: 10) |
destinationTileSize | number The destination tile size. (Defaults to tileSize) |
destinationTileToSourceTiles | DestinationTileToSourceTilesFn See common type below |
destinationToPixel | DestinationToPixelFn See common type below |
destinationToSource | DestinationToSourceFn See common type below |
interval | [intervalX: number, intervalY: number] The pixel sampling interval when reprojecting the source to destination. Max interval value is the destination tile size. (Default: [1, 1]) |
pixelToDestination | PixelToDestinationFn See common type below |
sourceTileSize | number The source tile size. (Defaults to tileSize) |
sourceToPixel | SourceToPixelFn See common type below |
tileSize | number Shorthand for setting sourceTileSize and destinationTileSize to the same value. (Default: 256) |
zoomOffset | number An offset zoom value applied to the reprojection which makes the tile text appear smaller or bigger. The offset is applied when determining which source tiles are needed to cover a destination tile in destinationTileToSourceTiles. Must be an integer. (Default: 0) |
CreateProtocolResult
| field | description |
|---|---|
protocol | string Url prefix that identifying a custom protocol. |
loader | maplibregl.AddProtocolAction See maplibregl documentation |
epsg4326ToEpsg3857Presets: Partial<CreateProtocolOptions>
Preset options to convert EPSG:4326 to EPSG:3857.
Common
Tile: number[] | [number, number, number]
A reference to a map tile. x, y, z
Bbox: number[] | [number, number, number, number]
A bounding box. xmin, ymin, xmax, ymax
DestinationTileToSourceTilesFn: (destinationRequest: { tile: Tile, bbox: Bbox }, zoomOffset?: number) => { tile: Tile, bbox: Bbox }[]
Calculate the source tile references needed to cover destination tile reference. A zoomOffset is used to apply any source-to-destination zoom adjustments.
DestinationToPixelFn: ([dx, dy]: number[], zoom: number, tileSize: number) => number[]
Transform a destination tile reference to pixel coordinate x, y.
DestinationToSourceFn: ([dx, dy]: number[]) => number[]
Transform a destination coordinate x, y to a source coordinate x, y.
PixelToDestinationFn: ([px, py]: number[], zoom: number, tileSize: number) => number[]
Transform a pixel coordinate x, y to destination coordinate x, y.
SourceToPixelFn: ([sx, sy]: number[], zoom: number, tileSize: number) => number[]
Transform a source coordinate x, y to a pixel coordinate x, y.
Tradeoffs
Map tiles are best used in their native projection. Reprojecting a tile almost always be suboptimal and most easily visualized in the following ways.
Use params like interval, zoomOffset, etc. to adjust the reprojection based on your needs.
Visual effects
- Text labels will likely be distorted when reprojecting raster images. Labels are placed and "burned" into tiles. So when tile reprojects those labels will transform with the terrain. Those labels may also be smaller or larger due to scale differences.
- Pixel precision will likely be blured or pixelated.
Prior Art
- OpenLayers Image Reprojection
- Tiles à la Google Maps and globalmaptiles.js for map tile conversion
- Raster Reprojection (Mike Bostock)
- Reprojected Raster Tiles (Jason Davies)
- A stackoverflow deep dive on reprojecting map tiles in d3 (Andrew Reid)
Tests
npm run lint
npm run test
npm run e2eDevelopment
- Update the
CHANGELOG.mdwith new version and commit the change. - Run
npm version ...or somethign similar or tag manually - Push tag to remote
git push --tags - Optional Run the
publishworkflow with tag