@qn-pandora/pandora-visualization
Version:
Pandora 通用可视化库
376 lines (340 loc) • 12.9 kB
JavaScript
/**
* 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.
* @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.
* @param {string} [options.languageChangePattern] - replace: [name_en, name] => [name_zh-Hans, name] | insert: [name_en, name] => [name_zh-Hans, name_en, name]
*/
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._languageChangePattern = options.languageChangePattern || 'insert';
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 || function (style, language) {
if (language === 'ar') {
return noSpacing(style);
} else {
return standardSpacing(style);
}
};
this._excludedLayerIds = options.excludedLayerIds || [];
this.supportedLanguages = options.supportedLanguages || ['ar', 'en', 'es', 'fr', 'de', 'ja', 'ko', 'mul', 'pt', 'ru', 'zh', 'zh-Hans', 'zh-Hant'];
}
function standardSpacing(style) {
var changedLayers = style.layers.map(function (layer) {
if (!(layer.layout || {})['text-field']) return layer;
var spacing = 0;
if (layer['source-layer'] === 'state_label') {
spacing = 0.15;
}
if (layer['source-layer'] === 'marine_label') {
if (/-lg/.test(layer.id)) {
spacing = 0.25;
}
if (/-md/.test(layer.id)) {
spacing = 0.15;
}
if (/-sm/.test(layer.id)) {
spacing = 0.1;
}
}
if (layer['source-layer'] === 'place_label') {
if (/-suburb/.test(layer.id)) {
spacing = 0.15;
}
if (/-neighbour/.test(layer.id)) {
spacing = 0.1;
}
if (/-islet/.test(layer.id)) {
spacing = 0.01;
}
}
if (layer['source-layer'] === 'airport_label') {
spacing = 0.01;
}
if (layer['source-layer'] === 'rail_station_label') {
spacing = 0.01;
}
if (layer['source-layer'] === 'poi_label') {
if (/-scalerank/.test(layer.id)) {
spacing = 0.01;
}
}
if (layer['source-layer'] === 'road_label') {
if (/-label-/.test(layer.id)) {
spacing = 0.01;
}
if (/-shields/.test(layer.id)) {
spacing = 0.05;
}
}
return Object.assign({}, layer, {
layout: Object.assign({}, layer.layout, {
'text-letter-spacing': spacing
})
});
});
return Object.assign({}, style, {
layers: changedLayers
});
}
function noSpacing(style) {
var changedLayers = style.layers.map(function (layer) {
if (!(layer.layout || {})['text-field']) return layer;
var spacing = 0;
return Object.assign({}, layer, {
layout: Object.assign({}, layer.layout, {
'text-letter-spacing': spacing
})
});
});
return Object.assign({}, style, {
layers: changedLayers
});
}
function isNameStringField(isLangField, property) {
return typeof property === 'string' && isLangField.test(property);
}
function isNameFunctionField(isLangField, property) {
return property.stops && property.stops.filter(function (stop) {
return isLangField.test(stop[1]);
}).length > 0;
}
function isArray(value) {
if (Array.isArray) return Array.isArray(value);
return Object.prototype.toString.call(value) === '[object Array]';
}
function isCoalesceArrayField(property) {
return isArray(property) && property.length > 1 && property[0] === 'coalesce';
}
function isCoalesceGetField(property) {
return isArray(property) && property.length === 2 && property[0] === 'get' && /^name_/.test(property[1]);
}
function getLanguage(languageFieldName) {
var reg = /^\{name_(.*)\}$/;
var match = reg.exec(languageFieldName);
if (match) return match[1];
return languageFieldName;
}
function adaptArrayPropertyLanguage(property, languageFieldName, languageChangePattern) {
if (isCoalesceArrayField(property)) {
if (languageChangePattern === 'insert') {
var newProperty = property.slice();
var languageIndex = newProperty.findIndex(isCoalesceGetField);
if (languageIndex === -1) return property;
// 采用 insert 的方式,能在没有当前语言的文本时,沿用原先的文本(比如英语),而不是各个地区显示当地各自的官方语言(比如泰国显示泰语)
newProperty.splice(languageIndex, 0, ['get', 'name_' + getLanguage(languageFieldName)]);
return newProperty;
}
return property.map(item => {
if (isCoalesceGetField(item)) {
return ['get', 'name_' + getLanguage(languageFieldName)];
}
return item;
});
}
return property.map(item => {
if (!isArray(item)) return item;
return adaptArrayPropertyLanguage(item, languageFieldName, languageChangePattern);
});
}
function adaptPropertyLanguage(isLangField, property, languageFieldName, languageChangePattern) {
// 版本不同,新版的 resource text-field 采用 array 形式来描述
if (isArray(property)) return adaptArrayPropertyLanguage(property, languageFieldName, languageChangePattern);
if (isNameStringField(isLangField, property)) return languageFieldName;
if (isNameFunctionField(isLangField, property)) {
var newStops = property.stops.map(function (stop) {
if (isLangField.test(stop[1])) {
return [stop[0], languageFieldName];
}
return stop;
});
return Object.assign({}, property, {
stops: newStops
});
}
return property;
}
function changeLayerTextProperty(isLangField, layer, languageFieldName, excludedLayerIds, languageChangePattern, originStyle) {
if (layer.layout && layer.layout['text-field'] && excludedLayerIds.indexOf(layer.id) === -1) {
var originLayer = originStyle.layers[originStyle.layers.findIndex(function(item) {
return item.id === layer.id;
})];
// insert 模式需要拿原始的值,否则多次调用时会导致不断往值里面插入新值
var textField = languageChangePattern === 'insert' ? originLayer.layout['text-field'] : layer.layout['text-field'];
return Object.assign({}, layer, {
layout: Object.assign({}, layer.layout, {
'text-field': adaptPropertyLanguage(isLangField, textField, languageFieldName, languageChangePattern)
})
});
}
return layer;
}
function findStreetsSource(style) {
// var sources = Object.keys(style.sources).filter(function (sourceName) {
// var source = style.sources[sourceName];
// return /mapbox-streets-v\d/.test(source.url);
// });
// return sources[0];
// 本地化不存在url,这里直接写死 source name
return 'composite';
}
/**
* 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');
if (!this.originStyle) this.originStyle = style
var streetsSource = this._languageSource || findStreetsSource(style);
if (!streetsSource) return style;
var field = this._getLanguageField(language);
var isLangField = this._isLanguageField;
var excludedLayerIds = this._excludedLayerIds;
var languageChangePattern = this._languageChangePattern;
var originStyle = this.originStyle;
var changedLayers = style.layers.map(function (layer) {
if (layer.source === streetsSource) return changeLayerTextProperty(isLangField, layer, field, excludedLayerIds, languageChangePattern, originStyle);
return layer;
});
var languageStyle = Object.assign({}, style, {
layers: changedLayers
});
return this._languageTransform(languageStyle, language);
};
MapboxLanguage.prototype._initialStyleUpdate = function () {
var style = this._map.getStyle();
// cache origin style
this.originStyle = style;
var language = this._defaultLanguage || browserLanguage(this.supportedLanguages);
// We only update the style once
this._map.off('styledata', this._initialStyleUpdate);
const changeStyle = this.setLanguage(style, language);
this._map.setStyle(changeStyle);
};
function browserLanguage(supportedLanguages) {
var language = navigator.languages ? navigator.languages[0] : (navigator.language || navigator.userLanguage);
var parts = language.split('-');
var 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('styledata', this._initialStyleUpdate);
this._container = document.createElement('div');
return this._container;
};
MapboxLanguage.prototype.onRemove = function () {
this._map.off('styledata', this._initialStyleUpdate);
this._map = undefined;
};
function ie11Polyfill() {
if (typeof Object.assign != 'function') {
// Must be writable: true, enumerable: false, configurable: true
Object.defineProperty(Object, 'assign', {
// eslint-disable-next-line no-unused-vars
value: function assign(target, varArgs) { // .length of function is 2
// eslint-disable-next-line strict
'use strict';
if (target === null) { // TypeError if undefined or null
throw new TypeError('Cannot convert undefined or null to object');
}
var to = Object(target);
for (var index = 1; index < arguments.length; index++) {
var nextSource = arguments[index];
if (nextSource !== null) { // Skip over if undefined or null
for (var nextKey in nextSource) {
// Avoid bugs when hasOwnProperty is shadowed
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
},
writable: true,
configurable: true
});
}
if (!Array.prototype.map) {
Object.defineProperty(Array.prototype, 'map', {
value: function (callback, thisArg) {
var A, k;
if (this == null) {
throw new TypeError('this is null or not defined');
}
var O = Object(this);
var len = O.length >>> 0;
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
A = new Array(len);
k = 0;
while (k < len) {
var kValue, mappedValue;
if (k in O) {
kValue = O[k];
mappedValue = callback.call(thisArg, kValue, k, O);
A[k] = mappedValue;
}
k++;
}
return A;
}
});
}
if (!Array.prototype.findIndex) {
Object.defineProperty(Array.prototype, 'findIndex', {
value: function (predicate, thisArg) {
if (this == null) {
throw new TypeError('this is null or not defined');
}
if (typeof predicate !== 'function') {
throw new TypeError('predicate must be a function');
}
var O = Object(this);
var len = O.length >>> 0;
var k = 0;
while (k < len) {
var kValue = this[k];
if (predicate.call(thisArg, kValue, k, o)) {
return k;
}
k++;
}
return -1;
}
});
}
}
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = MapboxLanguage;
} else {
ie11Polyfill();
window.MapboxLanguage = MapboxLanguage;
}