-
Notifications
You must be signed in to change notification settings - Fork 51
/
index.js
173 lines (152 loc) · 7.17 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
/**
* Create a new [Mapbox GL JS plugin](https://www.mapbox.com/blog/build-mapbox-gl-js-plugins/) that
* modifies the layers of the map style to use the `text-field` that matches the browser language.
* As of Mapbox GL Language v1.0.0, this plugin no longer supports token values (e.g. `{name}`). v1.0+ expects the `text-field`
* property of a style to use an [expression](https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/) of the form `['get', 'name_en']` or `['get', 'name']`; these expressions can be nested. Note that `get` expressions used as inputs to other expressions may not be handled by this plugin. For example:
* ```
* ["match",
* ["get", "name"],
* "California",
* "Golden State",
* ["coalesce",
* ["get", "name_en"],
* ["get", "name"]
* ]
* ]
* ```
* Only styles based on [Mapbox v8 styles](https://docs.mapbox.com/help/troubleshooting/streets-v8-migration-guide/) are supported.
*
* @constructor
* @param {object} options - Options to configure the plugin.
* @param {string[]} [options.supportedLanguages] - List of supported languages
* @param {Function} [options.languageTransform] - Custom style transformation to apply
* @param {RegExp} [options.languageField=/^name_/] - RegExp to match if a text-field is a language field
* @param {Function} [options.getLanguageField] - Given a language choose the field in the vector tiles
* @param {string} [options.languageSource] - Name of the source that contains the different languages.
* @param {string} [options.defaultLanguage] - Name of the default language to initialize style after loading.
* @param {string[]} [options.excludedLayerIds] - Name of the layers that should be excluded from translation.
*/
function MapboxLanguage(options) {
options = Object.assign({}, options);
if (!(this instanceof MapboxLanguage)) {
throw new Error('MapboxLanguage needs to be called with the new keyword');
}
this.setLanguage = this.setLanguage.bind(this);
this._initialStyleUpdate = this._initialStyleUpdate.bind(this);
this._defaultLanguage = options.defaultLanguage;
this._isLanguageField = options.languageField || /^name_/;
this._getLanguageField = options.getLanguageField || function nameField(language) {
return language === 'mul' ? 'name' : `name_${language}`;
};
this._languageSource = options.languageSource || null;
this._languageTransform = options.languageTransform;
this._excludedLayerIds = options.excludedLayerIds || [];
this.supportedLanguages = options.supportedLanguages || ['ar', 'de', 'en', 'es', 'fr', 'it', 'ja', 'ko', 'mul', 'pt', 'ru', 'vi', 'zh-Hans', 'zh-Hant'];
}
const isTokenField = /^\{name/;
function isFlatExpressionField(isLangField, property) {
const isGetExpression = Array.isArray(property) && property[0] === 'get';
if (isGetExpression && isTokenField.test(property[1])) {
console.warn('This plugin no longer supports the use of token syntax (e.g. {name}). Please use a get expression. See https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/ for more details.');
}
return isGetExpression && isLangField.test(property[1]);
}
function adaptNestedExpressionField(isLangField, property, languageFieldName) {
if (Array.isArray(property)) {
for (let i = 1; i < property.length; i++) {
if (Array.isArray(property[i])) {
if (isFlatExpressionField(isLangField, property[i])) {
property[i][1] = languageFieldName;
}
adaptNestedExpressionField(isLangField, property[i], languageFieldName);
}
}
}
}
function adaptPropertyLanguage(isLangField, property, languageFieldName) {
if (isFlatExpressionField(isLangField, property)) {
property[1] = languageFieldName;
}
adaptNestedExpressionField(isLangField, property, languageFieldName);
// handle special case of bare ['get', 'name'] expression by wrapping it in a coalesce statement
if (property[0] === 'get' && property[1] === 'name') {
const defaultProp = property.slice();
const adaptedProp = ['get', languageFieldName];
property = ['coalesce', adaptedProp, defaultProp];
}
return property;
}
function changeLayerTextProperty(isLangField, layer, languageFieldName, excludedLayerIds) {
if (layer.layout && layer.layout['text-field'] && excludedLayerIds.indexOf(layer.id) === -1) {
return Object.assign({}, layer, {
layout: Object.assign({}, layer.layout, {
'text-field': adaptPropertyLanguage(isLangField, layer.layout['text-field'], languageFieldName)
})
});
}
return layer;
}
function findStreetsSource(style) {
const sources = Object.keys(style.sources).filter((sourceName) => {
const url = style.sources[sourceName].url;
// the source URL can reference the source version or the style version
// this check and the error forces users to migrate to styles using source version 8
return url && url.indexOf('mapbox.mapbox-streets-v8') > -1 || /mapbox-streets-v[1-9][1-9]/.test(url);
});
if (!sources.length) throw new Error('If using MapboxLanguage with a Mapbox style, the style must be based on vector tile version 8, e.g. "streets-v11"');
return sources[0];
}
/**
* Explicitly change the language for a style.
* @param {object} style - Mapbox GL style to modify
* @param {string} language - The language iso code
* @returns {object} the modified style
*/
MapboxLanguage.prototype.setLanguage = function (style, language) {
if (this.supportedLanguages.indexOf(language) < 0) throw new Error(`Language ${ language } is not supported`);
const streetsSource = this._languageSource || findStreetsSource(style);
if (!streetsSource) return style;
const field = this._getLanguageField(language);
const isLangField = this._isLanguageField;
const excludedLayerIds = this._excludedLayerIds;
const changedLayers = style.layers.map((layer) => {
if (layer.source === streetsSource) return changeLayerTextProperty(isLangField, layer, field, excludedLayerIds);
return layer;
});
const languageStyle = Object.assign({}, style, {
layers: changedLayers
});
return this._languageTransform ? this._languageTransform(languageStyle, language) : languageStyle;
};
MapboxLanguage.prototype._initialStyleUpdate = function () {
const style = this._map.getStyle();
const language = this._defaultLanguage || browserLanguage(this.supportedLanguages);
this._map.setStyle(this.setLanguage(style, language));
};
function browserLanguage(supportedLanguages) {
const language = navigator.languages ? navigator.languages[0] : (navigator.language || navigator.userLanguage);
const parts = language && language.split('-');
let languageCode = language;
if (parts.length > 1) {
languageCode = parts[0];
}
if (supportedLanguages.indexOf(languageCode) > -1) {
return languageCode;
}
return null;
}
MapboxLanguage.prototype.onAdd = function (map) {
this._map = map;
this._map.on('style.load', this._initialStyleUpdate);
this._container = document.createElement('div');
return this._container;
};
MapboxLanguage.prototype.onRemove = function () {
this._map.off('style.load', this._initialStyleUpdate);
this._map = undefined;
};
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = MapboxLanguage;
} else {
window.MapboxLanguage = MapboxLanguage;
}