UNPKG

@qn-pandora/pandora-visualization

Version:

Pandora 通用可视化库

376 lines (340 loc) 12.9 kB
/** * 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; }