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

Adding projection control and map instance logic #132

Draft
wants to merge 7 commits into
base: RD-369
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# MapTiler SDK Changelog

## NEXT
### New Features
- New `MaptilerProjectionControl` to toggle globe/Mercator projection

### Bug Fixes
- Navigation now relies on `Map` methods instead of `Transform` methods for bearing due to globe projection being available



## 2.4.0
### New Features
- Shows a warning message in the map container if WebGL context is lost
Expand Down
3 changes: 3 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
},
"linter": {
"rules": {
"style": {
"noInferrableTypes": "off"
},
"suspicious": {
"noExplicitAny": {
"level": "off"
Expand Down
Binary file added images/screenshots/fov_1.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/screenshots/fov_10.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/screenshots/fov_20.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/screenshots/fov_30.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/screenshots/fov_37.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/screenshots/fov_40.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/screenshots/fov_50.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/screenshots/fov_60.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/screenshots/globe_hybrid.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/screenshots/globe_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/screenshots/globe_outdoor_alaska.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/screenshots/globe_outdoor_emea.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/screenshots/globe_outdoor_panamerica.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/screenshots/globe_projection.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/screenshots/globe_topo_arctica.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/screenshots/mercator_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/screenshots/mercator_projection.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@
"vitest": "^0.34.2"
},
"dependencies": {
"@maplibre/maplibre-gl-style-spec": "^20.3.1",
"@maplibre/maplibre-gl-style-spec": "^21.0.0",
"@maptiler/client": "^2.0.0",
"events": "^3.3.0",
"js-base64": "^3.7.4",
"maplibre-gl": "^5.0.0-pre.3",
"maplibre-gl": "^5.0.0-pre.4",
"uuid": "^9.0.0"
}
}
117 changes: 117 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |

Comment on lines +355 to +369
Copy link
Member

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.)

Copy link
Member

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


## 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is getting fixed soon: maplibre/maplibre-gl-js#4977

Copy link

@birkskyum birkskyum Nov 4, 2024

Choose a reason for hiding this comment

The 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 mercator projection will allow the fog to render at high zoom. Tickets here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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.
Expand Down
98 changes: 98 additions & 0 deletions src/Map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
*/
Expand Down Expand Up @@ -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;
};

/**
Expand All @@ -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);
Expand Down Expand Up @@ -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...
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The 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";
}
}
Loading
Loading