Skip to content

Commit

Permalink
Merge branch 'main' into RD-399
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanlurie authored Nov 18, 2024
2 parents 369aeb9 + 94d3467 commit e93fca1
Show file tree
Hide file tree
Showing 7 changed files with 310 additions and 38 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# MapTiler SDK Changelog

## 2.4.2
### Bug Fixes
- The language switching is now more robust and preserves the original formatting from the style (`Map.setPrimaryLangage()`) (https://github.com/maptiler/maptiler-sdk-js/pull/134)

### Others
- Now able to GitHub action a beta on NPM from the GH release creation process
- Updated GH action to v4



## 2.4.1
### Bug Fixes
- The class `AJAXError` is now imported as part of the `maplibregl` namespace (CommonJS limitation from Maplibre GL JS) (https://github.com/maptiler/maptiler-sdk-js/pull/129)
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@maptiler/sdk",
"version": "2.4.1",
"version": "2.4.2",
"description": "The Javascript & TypeScript map SDK tailored for MapTiler Cloud",
"module": "dist/maptiler-sdk.mjs",
"types": "dist/maptiler-sdk.d.ts",
Expand Down
37 changes: 18 additions & 19 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ maptilersdk.config.apiKey = 'YOUR_API_KEY';
// Let's say you have a DIV ready to receive a map
const mapContainer = document.getElementById('my-container-div');

// Instanciate the map
// Instantiate the map
const map = new maptilersdk.Map({
container: mapContainer,
});
Expand All @@ -60,16 +60,16 @@ import * as maptilersdk from '@maptiler/sdk';
// Let's say you have a DIV ready to receive a map
const mapContainer = document.getElementById('my-container-div');

// Instanciate the map
// Instantiate the map
const map = new maptilersdk.Map({
container: mapContainer,
apiKey: 'YOUR_API_KEY';
apiKey: 'YOUR_API_KEY'
});
```

By default, the map will be initialized with the style [streets-v2](https://www.maptiler.com/maps/#style=streets-v2).

Depending on the framework and environment you are using for your application, you will have to also include the CSS file.
Depending on the framework and environment you are using for your application, you will have to also include the CSS file.

For example, with a [NextJS](https://nextjs.org/) app, this can take place at the top of the file `_app.ts/js`:
```ts
Expand All @@ -79,7 +79,7 @@ import "@maptiler/sdk/dist/maptiler-sdk.css";
### TypeScript
The SDK is fully typed, but it may happen that types defined in Maplibre GL JS are not visible in your project. This is a known issue that comes from Maplibre being a CommonJS bundle.

There are mainly two ways to addess this issue and access to the complete type definition.
There are mainly two ways to address this issue and access to the complete type definition.

1. **With `esModuleInterop`**

Expand Down Expand Up @@ -237,7 +237,7 @@ All reference styles (instances of `ReferenceMapStyle`) and style variants (inst
___


Still, you can still use some classic styles with just a *string* if you know their MapTiler Cloud ID:
You can also use classic styles with just a *string* if you know their MapTiler Cloud ID:

```ts
map.setStyle('outdoor-v2');
Expand Down Expand Up @@ -290,7 +290,7 @@ The `geolocation` options will not be taken into consideration in the following

> 📣 *__Note:__* if none of the options `center` or `hash` is provided to the `Map` constructor, then the map will be centered using the `POINT` strategy, unless the `geolocate` has the value `false`.
> 📣 *__Note 2:__* the term *IP geolocation* refers to finding the physical location of a computer using its *IP address*. The *IP address* is a numerical identifier of a computer within a network, just like the phone number for a telephone. The *IP geolocation* is **not** using the GPS of a device and usually provides a precision in the order of a few hundred meters. This precision may vary based on many local parameters such as the density of the network grid or the terrain, this is why it is generaly better not to use a zoom level higher than `14`.
> 📣 *__Note 2:__* the term *IP geolocation* refers to finding the physical location of a computer using its *IP address*. The *IP address* is a numerical identifier of a computer within a network, just like the phone number for a telephone. The *IP geolocation* is **not** using the GPS of a device and usually provides a precision in the order of a few hundred meters. This precision may vary based on many local parameters such as the density of the network grid or the terrain, this is why it is generally better not to use a zoom level higher than `14`.
# Easy to add controls
The term "control" is commonly used for all sorts of buttons and information displays that take place in one of the corners of the map area. The most well-known are probably the `[+]` and `[-]` zoom buttons as well as the attribution information. Plenty of others are possible and we have made a few easy to add and directly accessible from the `Map` constructor options:
Expand Down Expand Up @@ -367,7 +367,7 @@ Or simply disable it:
map.disableTerrain()
```

> 📣 *__Note:__* Keep in mind that setting an exaggeration factor at `0` will result in a the same result as disabling the elevation but that terrain RGB tiles will still be fetched in the background.
> 📣 *__Note:__* Keep in mind that setting an exaggeration factor at `0` will result in the same result as disabling the elevation but that terrain RGB tiles will still be fetched in the background.
> 📣 *__Note 2:__* please be aware that due to the volume and elevation of the map floor in 3D space, the navigation with the terrain enabled is slightly different than without.
Expand All @@ -389,7 +389,7 @@ map.on("terrain", (e) => {
})
```

Since MapTiler SDK adds animation and the terrain data is necessary all along, the `"terrain"` event will be called at the very begining of the terrain animation when enabling and at the very end when disabling.
Since MapTiler SDK adds animation and the terrain data is necessary all along, the `"terrain"` event will be called at the very beginning of the terrain animation when enabling and at the very end when disabling.

- `"terrainAnimationStart"` and `"terrainAnimationStop"` events

Expand All @@ -405,7 +405,7 @@ map.on("terrainAnimationStop", (event) => {
});
```

The `event` argument is an object that contains (amond other things) a `terrain` attribute. In the case of `"terrainAnimationStop"`, this terrain attribute is `null` if the animation was about disabling the terrain, otherwise, this is just a propagation of `map.terrain`.
The `event` argument is an object that contains (among other things) a `terrain` attribute. In the case of `"terrainAnimationStop"`, this terrain attribute is `null` if the animation was about disabling the terrain, otherwise, this is just a propagation of `map.terrain`.

In the following example, we decide to associate the terrain animation with a change of camera, e.g. from clicking on the terrain control:
- when the terrain is enabled, it pops up with an animation and only **then** the camera is animated to take a lower point of view
Expand Down Expand Up @@ -541,7 +541,7 @@ function init() {
});

// We wait for the event.
// Once triggered, the callback is ranin it's own scope.
// Once triggered, the callback is ran in its own scope.
map.on("load", (evt) => {
// Adding a data source
map.addSource('my-gps-track-source', {
Expand Down Expand Up @@ -633,7 +633,7 @@ function init() {
});

// We wait for the event.
// Once triggered, the callback is ranin it's own scope.
// Once triggered, the callback is ran in its own scope.
map.on("ready", (evt) => {
// Adding a data source
map.addSource('my-gps-track-source', {
Expand Down Expand Up @@ -671,9 +671,9 @@ We believe that the *promise* approach is better because it does not nest scopes
> 📣 *__Note:__* Generally speaking, *promises* are not a go to replacement for all event+callback and are suitable only for events that are called only once in the lifecycle of a Map instance. This is the reason why we have decided to provide a *promise* equivalent only for the `load`, `ready` and `loadWithTerrain` events but not for events that may be called multiple time such as interaction events.
### The `webglContextLost` event
The maps is rendered with WebGL, that leverages the GPU to provide high-performance graphics. In some cases, the host machine, operating system or the graphics driver, can decide that continuing to run such high performance graphics is unsustainable, and will abort the process. This is called a "WebGL context loss". Such situation happens when the ressources are running low or when multiple browser tabs are competing to access graphics memory.
The map is rendered with WebGL, that leverages the GPU to provide high-performance graphics. In some cases, the host machine, operating system or the graphics driver, can decide that continuing to run such high performance graphics is unsustainable, and will abort the process. This is called a "WebGL context loss". Such situation happens when the resources are running low or when multiple browser tabs are competing to access graphics memory.

The best course of action in such situation varies from an app to another. Sometimes a page refresh is the best thing to do, in other cases, instantiating a new Map dynmicaly at application level is more appropriate because it hides a technical failure to the end user. The event `webglContextLost` is exposed so that the most appropriate scenario can be implemented at application level.
The best course of action in such situation varies from an app to another. Sometimes a page refresh is the best thing to do, in other cases, instantiating a new Map dynamically at application level is more appropriate because it hides a technical failure to the end user. The event `webglContextLost` is exposed so that the most appropriate scenario can be implemented at application level.

Here is how to respond to a WebGL context loss with a simple page refresh:
```ts
Expand Down Expand Up @@ -764,13 +764,13 @@ maptilersdk.helpers

**Example:** we have a *geoJSON* file that contains both *polygons* and *point* and we use it as the `data` property on the `helpers.addPoint(map, { options })`, this will only add the *points*.

In addition to easy styling, the helper's datasource can be:
In addition to easy styling, the helpers' datasource can be:
- a URL to a geoJSON file or its string content
- a URL to a GPX or KML file (only for the polyline helper) or its string content
- a UUID of a MapTiler Cloud dataset

### Multiple Layers
The key design principle of these vector layer helpers is **it's easy to make what you want**, which is very different from **making MapLibre easier to use**.
The key design principle of these vector layer helpers is **it's easy to make what you want**, which is very different from **making MapLibre easier to use**.

> For example, to create a road with an outline, one must draw two layers: a wider base layer and a narrower top layer, fueled by the same polyline data. This requires ordering the layers properly and computing not the width of the outline, but rather the width of the polyline underneath so that it outgrows the top road layer of the desired number of pixels.
Expand Down Expand Up @@ -859,13 +859,12 @@ helpers.addPolyline(map, {

Endless possibilities, what about a glowing wire?
```ts
helpers.addPolyline(map, {
helpers.addPolyline(map, {
data: "74003ba7-215a-4b7e-8e26-5bbe3aa70b05",
lineColor: "#fff",
lineWidth: 1,
outline: true,
outlineColor: "#ca57ff",
outlineWidth: 2,
outlineWidth: 10,
outlineBlur: 10,
outlineOpacity: 0.5,
Expand All @@ -877,7 +876,7 @@ helpers.addPolyline(map, {
All the other options are documented on [our reference page](https://docs.maptiler.com/sdk-js/api/helpers/#polyline) and more examples are available [here](https://docs.maptiler.com/sdk-js/examples/?q=polyline+helper).

## Polygon Layer Helper
The polygon helper makes it easy to create vector layers that contain polygons, whether they are *multi*polylons, *holed*polygons or just simple polygons. Whenever it's possible and it makes sense, we use the same terminology across the different helpers.
The polygon helper makes it easy to create vector layers that contain polygons, whether they are *multi*polygons, *holed*polygons or just simple polygons. Whenever it's possible and it makes sense, we use the same terminology across the different helpers.

Here is a minimalist example, with a half-transparent polygon of Switzerland, from a local file:

Expand Down
78 changes: 65 additions & 13 deletions src/Map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,15 @@ import type { ReferenceMapStyle, MapStyleVariant } from "@maptiler/client";
import { config, MAPTILER_SESSION_ID, type SdkConfig } from "./config";
import { defaults } from "./defaults";
import { MaptilerLogoControl } from "./MaptilerLogoControl";
import { combineTransformRequest, displayNoWebGlWarning, displayWebGLContextLostWarning } from "./tools";
import {
changeFirstLanguage,
checkNamePattern,
combineTransformRequest,
computeLabelsLocalizationMetrics,
displayNoWebGlWarning,
displayWebGLContextLostWarning,
replaceLanguage,
} from "./tools";
import { getBrowserLanguage, Language, type LanguageInfo } from "./language";
import { styleToStyle } from "./mapstyle";
import { MaptilerTerrainControl } from "./MaptilerTerrainControl";
Expand Down Expand Up @@ -190,6 +198,8 @@ export class Map extends maplibregl.Map {
private terrainAnimationDuration = 1000;
private monitoredStyleUrls!: Set<string>;
private styleInProcess = false;
private originalLabelStyle = new window.Map<string, ExpressionSpecification | string>();
private isStyleLocalized = false;

constructor(options: MapOptions) {
displayNoWebGlWarning(options.container);
Expand Down Expand Up @@ -730,6 +740,7 @@ export class Map extends maplibregl.Map {
style: null | ReferenceMapStyle | MapStyleVariant | StyleSpecification | string,
options?: StyleSwapOptions & StyleOptions,
): this {
this.originalLabelStyle.clear();
this.minimap?.setStyle(style);
this.forceLanguageUpdate = true;

Expand Down Expand Up @@ -1003,7 +1014,7 @@ export class Map extends maplibregl.Map {
let langStr = Language.LOCAL.flag;

// will be overwritten below
let replacer: ExpressionSpecification | string = `{${langStr}}`;
let replacer: ExpressionSpecification = ["get", langStr];

if (languageNonStyle.flag === Language.VISITOR.flag) {
langStr = getBrowserLanguage().flag;
Expand Down Expand Up @@ -1049,23 +1060,32 @@ export class Map extends maplibregl.Map {
];
} else if (languageNonStyle.flag === Language.AUTO.flag) {
langStr = getBrowserLanguage().flag;
replacer = ["case", ["has", langStr], ["get", langStr], ["get", Language.LOCAL.flag]];
replacer = ["coalesce", ["get", langStr], ["get", Language.LOCAL.flag]];
}

// This is for using the regular names as {name}
else if (languageNonStyle === Language.LOCAL) {
langStr = Language.LOCAL.flag;
replacer = `{${langStr}}`;
replacer = ["get", langStr];
}

// This section is for the regular language ISO codes
else {
langStr = languageNonStyle.flag;
replacer = ["case", ["has", langStr], ["get", langStr], ["get", Language.LOCAL.flag]];
replacer = ["coalesce", ["get", langStr], ["get", Language.LOCAL.flag]];
}

const { layers } = this.getStyle();

// True if it's the first time the language is updated for the current style
const firstPassOnStyle = this.originalLabelStyle.size === 0;

// Analisis on all the label layers to check the languages being used
if (firstPassOnStyle) {
const labelsLocalizationMetrics = computeLabelsLocalizationMetrics(layers, this);
this.isStyleLocalized = Object.keys(labelsLocalizationMetrics.localized).length > 0;
}

for (const genericLayer of layers) {
// Only symbole layer can have a layout with text-field
if (genericLayer.type !== "symbol") {
Expand Down Expand Up @@ -1102,17 +1122,49 @@ export class Map extends maplibregl.Map {
continue;
}

const textFieldLayoutProp = this.getLayoutProperty(id, "text-field");
let textFieldLayoutProp: string | maplibregl.ExpressionSpecification;

// If the label is not about a name, then we don't translate it
if (
typeof textFieldLayoutProp === "string" &&
(textFieldLayoutProp.toLowerCase().includes("ref") || textFieldLayoutProp.toLowerCase().includes("housenumber"))
) {
continue;
// Keeping a copy of the text-field sub-object as it is in the original style
if (firstPassOnStyle) {
textFieldLayoutProp = this.getLayoutProperty(id, "text-field");
this.originalLabelStyle.set(id, textFieldLayoutProp);
} else {
textFieldLayoutProp = this.originalLabelStyle.get(id) as string | maplibregl.ExpressionSpecification;
}

// From this point, the value of textFieldLayoutProp is as in the original version of the style
// and never a mofified version

// Testing the different case where the text-field property should NOT be updated:
if (typeof textFieldLayoutProp === "string") {
// When the original style is localized (this.isStyleLocalized is true), we do not modify the {name} because they are
// very likely to be only fallbacks.
// When the original style is not localized (this.isStyleLocalized is false), the occurences of "{name}"
// should be replaced by localized versions with fallback to local language.

const { contains, exactMatch } = checkNamePattern(textFieldLayoutProp, this.isStyleLocalized);

// If the current text-fiels does not contain any "{name:xx}" pattern
if (!contains) continue;

// In case of an exact match, we replace by an object representation of the label
if (exactMatch) {
this.setLayoutProperty(id, "text-field", replacer);
} else {
// In case of a non-exact match (such as "foo {name:xx} bar" or "foo {name} bar", depending on localization)
// we create a "concat" object expresion composed of the original elements with new replacer
// in-betweem
const newReplacer = replaceLanguage(textFieldLayoutProp, replacer, this.isStyleLocalized);

this.setLayoutProperty(id, "text-field", newReplacer);
}
}

this.setLayoutProperty(id, "text-field", replacer);
// The value of text-field is an object
else {
const newReplacer = changeFirstLanguage(textFieldLayoutProp, replacer, this.isStyleLocalized);
this.setLayoutProperty(id, "text-field", newReplacer);
}
}
}

Expand Down
Loading

0 comments on commit e93fca1

Please sign in to comment.