-
Notifications
You must be signed in to change notification settings - Fork 14
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
Adding projection control and map instance logic #132
base: RD-369
Are you sure you want to change the base?
Changes from all commits
f88bd8d
7c9b282
b84803b
532216b
57845db
71bd911
6035881
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -261,6 +261,123 @@ And can even be provided in the URI form: | |
map.setStyle("maptiler://c912ffc8-2360-487a-973b-59d037fb15b8"); | ||
``` | ||
|
||
# Globe or Mercator projection? | ||
The **Web Mercator projection** [*(Wikipedia)*](https://en.wikipedia.org/wiki/Web_Mercator_projection) has been the go-to standard in cartography since the early days or web mapping. Partly for technical reasons but also because it is great for navigation as well as for showing the entire world in one screen, with no hidden face. That being said, Mercator's heavy distorsion at high latitudes, as well a the discontinuity at the poles can be a limitation for data visualization and has been critisized for providing a biased view of the world. | ||
|
||
The **globe projection**, available starting from MapTiler SDK v3, does not suffer from these biases and can feel overall more playfull than Mercator. It can be a great choice for semi-global data visualization, especially for data close to the poles, thanks to its geographic continuity. | ||
|
||
|
||
| Mercator projection | Globe projection | | ||
| :--------: | :-------: | | ||
| ![](images/screenshots/mercator_projection.jpeg) | ![](images/screenshots/globe_projection.jpeg) | | ||
|
||
The choice between Mercator and Globe can be done at different levels and moments in the lifecycle of the map, yet, unless stated otherwise, **Mercator remains the default**. | ||
|
||
- In the style, using the `projection` top-level property. | ||
For globe: | ||
```js | ||
{ | ||
"version": ..., | ||
"id": ..., | ||
"name": ..., | ||
"sources": ..., | ||
"layers": ..., | ||
|
||
// Make the style use the globe projection at load time | ||
"projection": { | ||
"type": "globe" | ||
} | ||
|
||
// ... | ||
} | ||
``` | ||
or for Mercator: | ||
```js | ||
{ | ||
"version": ..., | ||
"id": ..., | ||
"name": ..., | ||
"sources": ..., | ||
"layers": ..., | ||
|
||
// Make the style use the mercator projection at load time | ||
"projection": { | ||
"type": "mercator" | ||
} | ||
|
||
// ... | ||
} | ||
``` | ||
|
||
- In the constructor of the `Map` class, using the `projection` option. For globe: | ||
```ts | ||
const map = new maptilersdk.Map({ | ||
container: "map", | ||
projection: "globe", // Force a globe projection | ||
}); | ||
``` | ||
or for Mercator: | ||
```ts | ||
const map = new maptilersdk.Map({ | ||
container: "map", | ||
projection: "mercator", // Force a mercator projection | ||
}) | ||
``` | ||
This will overwrite the `projection` property from the style (if any) and will persist it later if the map style was to change. | ||
|
||
- Use the built-in methods: | ||
```ts | ||
const animate = false; | ||
map.enableGlobeProjection(animate); | ||
// or | ||
map.enableMercatorProjection(animate); | ||
``` | ||
|
||
The animated transition is enabled by default, but can be disabled by passing `false`, as in the example above. Similarly to the `projection` option in the constructor, this will overwrite the projection settings from style (if any) and persist it when the style is updated. | ||
|
||
The projection setter built in Maplibre GL JS is also usable: | ||
```ts | ||
map.setProjection({type: "mercator"}); | ||
// or | ||
map.setProjection({type: "globe"}); | ||
``` | ||
but this will not automatically animate the transition and may cause rendering glitches. | ||
|
||
- Using the `MaptilerProjectionControl`. Not mounted by default, it can easily be added with a single option in the `Map` constructor: | ||
```ts | ||
const map = new maptilersdk.Map({ | ||
container: "map", | ||
projectionControl: true, // or the position such as "top-left", "top-right", | ||
}); // "bottom-right" or "bottom-left" | ||
``` | ||
This dedicated control will show a globe icon <img src="images/screenshots/globe_icon.png" width="30px"/> to transition from Mercator to globe projection and will show a flat map icon <img src="images/screenshots/mercator_icon.png" width="30px"/> to transition from globe to Mercator projection. The chosen projection persist with future style changes. | ||
|
||
## Field of view (FOV) | ||
The internal camera has a default vertical field of view [*(wikipedia)*](https://en.wikipedia.org/wiki/Field_of_view) of a wide ~36.86 degrees. In globe mode, such a large *FOV* reduces the amount of the Earth that can be seen at once and exaggerates the central part, comparably to a fisheye lens. In many cases, a narrower *FOV* is preferable. Here is how to update if: | ||
|
||
```ts | ||
// Ajust de FOV, with values from 1 to 50 | ||
map.transform.setFov(10); | ||
map.redraw(); | ||
``` | ||
> 📣 *__Note:__* with the Mercator projection, it is possible to set a FOV of `0`, which yields a true orthographic projection [*(wikipedia)*](https://en.wikipedia.org/wiki/Orthographic_projection), but the globe projection does not allow this. | ||
|
||
Here is a table of FOV comparison: | ||
| 01° | 10° | 20° | 30° | 40° | 50° | | ||
| :--------: | :-------: |:-------: |:-------: |:-------: |:-------: | | ||
| ![](images/screenshots/fov_1.jpeg) | ![](images/screenshots/fov_10.jpeg) | ![](images/screenshots/fov_20.jpeg) | ![](images/screenshots/fov_30.jpeg) | ![](images/screenshots/fov_40.jpeg) | ![](images/screenshots/fov_50.jpeg) | | ||
|
||
|
||
## Globe screenshots | ||
![](images/screenshots/globe_topo_arctica.jpeg) | ||
![](images/screenshots/globe_hybrid.jpeg) | ||
![](images/screenshots/globe_outdoor_alaska.jpeg) | ||
![](images/screenshots/globe_outdoor_emea.jpeg) | ||
![](images/screenshots/globe_outdoor_panamerica.jpeg) | ||
|
||
|
||
> 📣 *__Note:__* Terrain is not fully compatible with the globe projection yet so it's better to disable it at low zoom level (from afar) and to choose the Mercator projection at higher zoom level (from up close). | ||
|
||
Comment on lines
+379
to
+380
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like this is getting fixed soon: maplibre/maplibre-gl-js#4977 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Kinda, yes, but there's a few gotchas that'll persist even after my PR is in, that could justify keeping the recommendation as is. There's still an issue with the poles on the globe if terrain is active, which often will be visible at low zoom levels. Disabling terrain from the globe will make it look better. - maplibre/maplibre-gl-js#4984. There's not support for fog on the globe yet with terrain, even after the transition to mercator at high zoom levels - due to the way it's disabled, switching to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks a lot for the advice @birkskyum ! |
||
# Centering the map on visitors | ||
It is sometimes handy to center the map on the visitor's location, and there are multiple ways of doing it but for the SDK, we have decided to make this extra simple by using the [IP geolocation](#%EF%B8%8F%EF%B8%8F-geolocation) API provided by [MapTiler Cloud](https://docs.maptiler.com/cloud/api/geolocation/), directly exposed as a single option of the `Map` constructor. There are two strategies: | ||
1. `POINT`: centering the map on the actual visitor location, optionally using the `zoom` option (zoom level `13` if none is provided). As a more precise option, if the user has previously granted access to the browser location (more precise) then this is going to be used. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,6 +37,7 @@ import { FullscreenControl } from "./MLAdapters/FullscreenControl"; | |
import Minimap from "./Minimap"; | ||
import type { MinimapOptionsInput } from "./Minimap"; | ||
import { CACHE_API_AVAILABLE, registerLocalCacheProtocol } from "./caching"; | ||
import { MaptilerProjectionControl } from "./MaptilerProjectionControl"; | ||
|
||
export type LoadWithTerrainEvent = { | ||
type: "loadWithTerrain"; | ||
|
@@ -62,6 +63,12 @@ type MapTerrainDataEvent = MapDataEvent & { | |
source: RasterDEMSourceSpecification; | ||
}; | ||
|
||
/** | ||
* The type of projection, `undefined` means it's decided by the style and if the style does not contain any projection info, | ||
* if falls back to the default Mercator | ||
*/ | ||
export type ProjectionTypes = "mercator" | "globe" | undefined; | ||
|
||
/** | ||
* Options to provide to the `Map` constructor | ||
*/ | ||
|
@@ -170,6 +177,17 @@ export type MapOptions = Omit<MapOptionsML, "style" | "maplibreLogo"> & { | |
* Default: `false` | ||
*/ | ||
geolocate?: (typeof GeolocationType)[keyof typeof GeolocationType] | boolean; | ||
|
||
/** | ||
* Show the projection control. (default: `false`, will show if `true`) | ||
*/ | ||
projectionControl?: boolean | ControlPosition; | ||
|
||
/** | ||
* Whether the projection should be "mercator" or "globe". | ||
* If not provided, the style takes precedence. If provided, overwrite the style. | ||
*/ | ||
projection?: ProjectionTypes; | ||
}; | ||
|
||
/** | ||
|
@@ -189,6 +207,7 @@ export class Map extends maplibregl.Map { | |
private terrainAnimationDuration = 1000; | ||
private monitoredStyleUrls!: Set<string>; | ||
private styleInProcess = false; | ||
private curentProjection: ProjectionTypes = undefined; | ||
|
||
constructor(options: MapOptions) { | ||
displayNoWebGlWarning(options.container); | ||
|
@@ -306,6 +325,19 @@ export class Map extends maplibregl.Map { | |
this.languageAlwaysBeenStyle = this.primaryLanguage === Language.STYLE; | ||
this.terrainExaggeration = options.terrainExaggeration ?? this.terrainExaggeration; | ||
|
||
this.curentProjection = options.projection; | ||
|
||
// Managing the type of projection and persist if not present in style | ||
this.on("styledata", () => { | ||
if (this.curentProjection === "mercator") { | ||
this.setProjection({ type: "mercator" }); | ||
} else if (this.curentProjection === "globe") { | ||
this.setProjection({ type: "globe" }); | ||
// @ts-ignore | ||
this.transform.setGlobeViewAllowed(true, true); // the first `true` means globe | ||
} | ||
}); | ||
|
||
// Map centering and geolocation | ||
this.once("styledata", async () => { | ||
// Not using geolocation centering if... | ||
|
@@ -508,6 +540,16 @@ export class Map extends maplibregl.Map { | |
this.addControl(new MaptilerTerrainControl(), position); | ||
} | ||
|
||
if (options.projectionControl) { | ||
// default position, if not provided, is top left corner | ||
const position = ( | ||
options.projectionControl === true || options.projectionControl === undefined | ||
? "top-right" | ||
: options.projectionControl | ||
) as ControlPosition; | ||
this.addControl(new MaptilerProjectionControl(), position); | ||
} | ||
|
||
// By default, no fullscreen control | ||
if (options.fullscreenControl) { | ||
// default position, if not provided, is top left corner | ||
|
@@ -1430,4 +1472,60 @@ export class Map extends maplibregl.Map { | |
super.setTransformRequest(combineTransformRequest(transformRequest)); | ||
return this; | ||
} | ||
|
||
/** | ||
* Returns whether a globe projection is currently being used | ||
*/ | ||
isGlobeprojection(): boolean { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Capital "P" here |
||
const projection = this.getProjection(); | ||
if (!projection) return false; | ||
// @ts-ignore | ||
return projection.type === "globe" && this.transform.getGlobeViewAllowed(); | ||
} | ||
|
||
/** | ||
* Uses the globe projection. Animated by default, it can be disabled | ||
*/ | ||
enableGlobeProjection(animate: boolean = true) { | ||
if (this.isGlobeprojection()) return; | ||
|
||
// From Mercator to Globe | ||
this.setProjection({ type: "globe" }); | ||
|
||
if (animate) { | ||
// @ts-ignore | ||
this.transform.setGlobeViewAllowed(false, true); // the `false` means mercator | ||
|
||
this.once("projectiontransition", () => { | ||
// @ts-ignore | ||
this.transform.setGlobeViewAllowed(true, true); | ||
}); | ||
} else { | ||
// @ts-ignore | ||
this.transform.setGlobeViewAllowed(true, true); | ||
} | ||
|
||
this.curentProjection = "globe"; | ||
} | ||
|
||
/** | ||
* Uses the Mercator projection. Animated by default, it can be disabled | ||
*/ | ||
enableMercatorProjection(animate: boolean = true) { | ||
if (!this.isGlobeprojection()) return; | ||
|
||
if (animate) { | ||
// From Globe to Mercator | ||
this.setProjection({ type: "globe" }); | ||
// @ts-ignore | ||
this.transform.setGlobeViewAllowed(false, true); | ||
this.once("projectiontransition", () => { | ||
this.setProjection({ type: "mercator" }); | ||
}); | ||
} else { | ||
this.setProjection({ type: "mercator" }); | ||
} | ||
|
||
this.curentProjection = "mercator"; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking whether this is not too specific and maybe does not need to be documented directly in the readme? (Especially since it adds several megabytes of data to the repo.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, there is now a public method to adjust the FoV nicely: maplibre/maplibre-gl-js@d3f2bca