mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Spacetime] [Maps] Localized basemaps (#130930)
This commit is contained in:
parent
3effa893da
commit
0c2d06dd81
13 changed files with 156 additions and 10 deletions
|
@ -108,7 +108,7 @@
|
|||
"@elastic/charts": "46.0.1",
|
||||
"@elastic/datemath": "5.0.3",
|
||||
"@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.2.0-canary.2",
|
||||
"@elastic/ems-client": "8.3.0",
|
||||
"@elastic/ems-client": "8.3.2",
|
||||
"@elastic/eui": "55.1.2",
|
||||
"@elastic/filesaver": "1.1.2",
|
||||
"@elastic/node-crypto": "1.2.1",
|
||||
|
|
|
@ -76,7 +76,7 @@ export const DEV_ONLY_LICENSE_ALLOWED = ['MPL-2.0'];
|
|||
export const LICENSE_OVERRIDES = {
|
||||
'jsts@1.6.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts
|
||||
'@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], // license in readme https://github.com/tmcw/jsonlint
|
||||
'@elastic/ems-client@8.3.0': ['Elastic License 2.0'],
|
||||
'@elastic/ems-client@8.3.2': ['Elastic License 2.0'],
|
||||
'@elastic/eui@55.1.2': ['SSPL-1.0 OR Elastic License 2.0'],
|
||||
'language-subtag-registry@0.3.21': ['CC-BY-4.0'], // retired ODC‑By license https://github.com/mattcg/language-subtag-registry
|
||||
};
|
||||
|
|
|
@ -298,6 +298,8 @@ export const MAPS_NEW_VECTOR_LAYER_META_CREATED_BY = 'maps-new-vector-layer';
|
|||
|
||||
export const MAX_DRAWING_SIZE_BYTES = 10485760; // 10MB
|
||||
|
||||
export const NO_EMS_LOCALE = 'none';
|
||||
export const AUTOSELECT_EMS_LOCALE = 'autoselect';
|
||||
export const emsWorldLayerId = 'world_countries';
|
||||
|
||||
export enum WIZARD_ID {
|
||||
|
|
|
@ -61,6 +61,7 @@ export type LayerDescriptor = {
|
|||
attribution?: Attribution;
|
||||
id: string;
|
||||
label?: string | null;
|
||||
locale?: string | null;
|
||||
areLabelsOnTop?: boolean;
|
||||
minZoom?: number;
|
||||
maxZoom?: number;
|
||||
|
|
|
@ -472,6 +472,15 @@ export function updateLayerLabel(id: string, newLabel: string) {
|
|||
};
|
||||
}
|
||||
|
||||
export function updateLayerLocale(id: string, locale: string) {
|
||||
return {
|
||||
type: UPDATE_LAYER_PROP,
|
||||
id,
|
||||
propName: 'locale',
|
||||
newValue: locale,
|
||||
};
|
||||
}
|
||||
|
||||
export function setLayerAttribution(id: string, attribution: Attribution) {
|
||||
return {
|
||||
type: UPDATE_LAYER_PROP,
|
||||
|
|
|
@ -80,10 +80,11 @@ describe('EMS is enabled', () => {
|
|||
id: '12345',
|
||||
includeInFitToBounds: true,
|
||||
label: null,
|
||||
locale: 'autoselect',
|
||||
maxZoom: 24,
|
||||
minZoom: 0,
|
||||
source: undefined,
|
||||
sourceDescriptor: {
|
||||
id: undefined,
|
||||
isAutoSelect: true,
|
||||
lightModeDefault: 'road_map_desaturated',
|
||||
type: 'EMS_TMS',
|
||||
|
|
|
@ -14,6 +14,7 @@ import { KibanaTilemapSource } from '../sources/kibana_tilemap_source';
|
|||
import { RasterTileLayer } from './raster_tile_layer/raster_tile_layer';
|
||||
import { EmsVectorTileLayer } from './ems_vector_tile_layer/ems_vector_tile_layer';
|
||||
import { EMSTMSSource } from '../sources/ems_tms_source';
|
||||
import { AUTOSELECT_EMS_LOCALE } from '../../../common/constants';
|
||||
|
||||
export function createBasemapLayerDescriptor(): LayerDescriptor | null {
|
||||
const tilemapSourceFromKibana = getKibanaTileMap();
|
||||
|
@ -27,6 +28,7 @@ export function createBasemapLayerDescriptor(): LayerDescriptor | null {
|
|||
const isEmsEnabled = getEMSSettings()!.isEMSEnabled();
|
||||
if (isEmsEnabled) {
|
||||
const layerDescriptor = EmsVectorTileLayer.createDescriptor({
|
||||
locale: AUTOSELECT_EMS_LOCALE,
|
||||
sourceDescriptor: EMSTMSSource.createDescriptor({ isAutoSelect: true }),
|
||||
});
|
||||
return layerDescriptor;
|
||||
|
|
|
@ -55,6 +55,26 @@ describe('EmsVectorTileLayer', () => {
|
|||
expect(actualErrorMessage).toStrictEqual('network error');
|
||||
});
|
||||
|
||||
describe('getLocale', () => {
|
||||
test('should set locale to none for existing layers where locale is not defined', () => {
|
||||
const layer = new EmsVectorTileLayer({
|
||||
source: {} as unknown as EMSTMSSource,
|
||||
layerDescriptor: {} as unknown as LayerDescriptor,
|
||||
});
|
||||
expect(layer.getLocale()).toBe('none');
|
||||
});
|
||||
|
||||
test('should set locale for new layers', () => {
|
||||
const layer = new EmsVectorTileLayer({
|
||||
source: {} as unknown as EMSTMSSource,
|
||||
layerDescriptor: {
|
||||
locale: 'xx',
|
||||
} as unknown as LayerDescriptor,
|
||||
});
|
||||
expect(layer.getLocale()).toBe('xx');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isInitialDataLoadComplete', () => {
|
||||
test('should return false when tile loading has not started', () => {
|
||||
const layer = new EmsVectorTileLayer({
|
||||
|
|
|
@ -6,11 +6,19 @@
|
|||
*/
|
||||
|
||||
import type { Map as MbMap, LayerSpecification, StyleSpecification } from '@kbn/mapbox-gl';
|
||||
import { TMSService } from '@elastic/ems-client';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import _ from 'lodash';
|
||||
// @ts-expect-error
|
||||
import { RGBAImage } from './image_utils';
|
||||
import { AbstractLayer } from '../layer';
|
||||
import { SOURCE_DATA_REQUEST_ID, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../../../common/constants';
|
||||
import {
|
||||
AUTOSELECT_EMS_LOCALE,
|
||||
NO_EMS_LOCALE,
|
||||
SOURCE_DATA_REQUEST_ID,
|
||||
LAYER_TYPE,
|
||||
LAYER_STYLE_TYPE,
|
||||
} from '../../../../common/constants';
|
||||
import { LayerDescriptor } from '../../../../common/descriptor_types';
|
||||
import { DataRequest } from '../../util/data_request';
|
||||
import { isRetina } from '../../../util';
|
||||
|
@ -50,6 +58,7 @@ export class EmsVectorTileLayer extends AbstractLayer {
|
|||
const tileLayerDescriptor = super.createDescriptor(options);
|
||||
tileLayerDescriptor.type = LAYER_TYPE.EMS_VECTOR_TILE;
|
||||
tileLayerDescriptor.alpha = _.get(options, 'alpha', 1);
|
||||
tileLayerDescriptor.locale = _.get(options, 'locale', AUTOSELECT_EMS_LOCALE);
|
||||
tileLayerDescriptor.style = { type: LAYER_STYLE_TYPE.TILE };
|
||||
return tileLayerDescriptor;
|
||||
}
|
||||
|
@ -87,6 +96,10 @@ export class EmsVectorTileLayer extends AbstractLayer {
|
|||
return this._style;
|
||||
}
|
||||
|
||||
getLocale() {
|
||||
return this._descriptor.locale ?? NO_EMS_LOCALE;
|
||||
}
|
||||
|
||||
_canSkipSync({
|
||||
prevDataRequest,
|
||||
nextMeta,
|
||||
|
@ -309,7 +322,6 @@ export class EmsVectorTileLayer extends AbstractLayer {
|
|||
return;
|
||||
}
|
||||
this._addSpriteSheetToMapFromImageData(newJson, imageData, mbMap);
|
||||
|
||||
// sync layers
|
||||
const layers = vectorStyle.layers ? vectorStyle.layers : [];
|
||||
layers.forEach((layer) => {
|
||||
|
@ -391,6 +403,27 @@ export class EmsVectorTileLayer extends AbstractLayer {
|
|||
});
|
||||
}
|
||||
|
||||
_setLanguage(mbMap: MbMap, mbLayer: LayerSpecification, mbLayerId: string) {
|
||||
const locale = this.getLocale();
|
||||
if (locale === null || locale === NO_EMS_LOCALE) {
|
||||
if (mbLayer.type !== 'symbol') return;
|
||||
|
||||
const textProperty = mbLayer.layout?.['text-field'];
|
||||
if (mbLayer.layout && textProperty) {
|
||||
mbMap.setLayoutProperty(mbLayerId, 'text-field', textProperty);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const textProperty =
|
||||
locale === AUTOSELECT_EMS_LOCALE
|
||||
? TMSService.transformLanguageProperty(mbLayer, i18n.getLocale())
|
||||
: TMSService.transformLanguageProperty(mbLayer, locale);
|
||||
if (textProperty !== undefined) {
|
||||
mbMap.setLayoutProperty(mbLayerId, 'text-field', textProperty);
|
||||
}
|
||||
}
|
||||
|
||||
_setLayerZoomRange(mbMap: MbMap, mbLayer: LayerSpecification, mbLayerId: string) {
|
||||
let minZoom = this.getMinZoom();
|
||||
if (typeof mbLayer.minzoom === 'number') {
|
||||
|
@ -414,6 +447,7 @@ export class EmsVectorTileLayer extends AbstractLayer {
|
|||
this.syncVisibilityWithMb(mbMap, mbLayerId);
|
||||
this._setLayerZoomRange(mbMap, mbLayer, mbLayerId);
|
||||
this._setOpacityForType(mbMap, mbLayer, mbLayerId);
|
||||
this._setLanguage(mbMap, mbLayer, mbLayerId);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -425,6 +459,10 @@ export class EmsVectorTileLayer extends AbstractLayer {
|
|||
return true;
|
||||
}
|
||||
|
||||
supportsLabelLocales(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
async getLicensedFeatures() {
|
||||
return this._source.getLicensedFeatures();
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ export interface ILayer {
|
|||
supportsFitToBounds(): Promise<boolean>;
|
||||
getAttributions(): Promise<Attribution[]>;
|
||||
getLabel(): string;
|
||||
getLocale(): string | null;
|
||||
hasLegendDetails(): Promise<boolean>;
|
||||
renderLegendDetails(): ReactElement<any> | null;
|
||||
showAtZoomLevel(zoom: number): boolean;
|
||||
|
@ -101,6 +102,7 @@ export interface ILayer {
|
|||
isPreviewLayer: () => boolean;
|
||||
areLabelsOnTop: () => boolean;
|
||||
supportsLabelsOnTop: () => boolean;
|
||||
supportsLabelLocales: () => boolean;
|
||||
isFittable(): Promise<boolean>;
|
||||
isIncludeInFitToBounds(): boolean;
|
||||
getLicensedFeatures(): Promise<LICENSED_FEATURES[]>;
|
||||
|
@ -250,6 +252,10 @@ export class AbstractLayer implements ILayer {
|
|||
return this._descriptor.label ? this._descriptor.label : '';
|
||||
}
|
||||
|
||||
getLocale(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
getLayerIcon(isTocIcon: boolean): LayerIcon {
|
||||
return {
|
||||
icon: <EuiIcon size="m" type={this.getLayerTypeIconName()} />,
|
||||
|
@ -461,6 +467,10 @@ export class AbstractLayer implements ILayer {
|
|||
return false;
|
||||
}
|
||||
|
||||
supportsLabelLocales(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
async getLicensedFeatures(): Promise<LICENSED_FEATURES[]> {
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
clearLayerAttribution,
|
||||
setLayerAttribution,
|
||||
updateLayerLabel,
|
||||
updateLayerLocale,
|
||||
updateLayerMaxZoom,
|
||||
updateLayerMinZoom,
|
||||
updateLayerAlpha,
|
||||
|
@ -26,6 +27,7 @@ function mapDispatchToProps(dispatch: Dispatch<AnyAction>) {
|
|||
setLayerAttribution: (id: string, attribution: Attribution) =>
|
||||
dispatch(setLayerAttribution(id, attribution)),
|
||||
updateLabel: (id: string, label: string) => dispatch(updateLayerLabel(id, label)),
|
||||
updateLocale: (id: string, locale: string) => dispatch(updateLayerLocale(id, locale)),
|
||||
updateMinZoom: (id: string, minZoom: number) => dispatch(updateLayerMinZoom(id, minZoom)),
|
||||
updateMaxZoom: (id: string, maxZoom: number) => dispatch(updateLayerMaxZoom(id, maxZoom)),
|
||||
updateAlpha: (id: string, alpha: number) => dispatch(updateLayerAlpha(id, alpha)),
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
EuiPanel,
|
||||
EuiFormRow,
|
||||
EuiFieldText,
|
||||
EuiSelect,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiSwitchEvent,
|
||||
|
@ -20,7 +21,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { ValidatedDualRange } from '@kbn/kibana-react-plugin/public';
|
||||
import { Attribution } from '../../../../common/descriptor_types';
|
||||
import { MAX_ZOOM } from '../../../../common/constants';
|
||||
import { AUTOSELECT_EMS_LOCALE, NO_EMS_LOCALE, MAX_ZOOM } from '../../../../common/constants';
|
||||
import { AlphaSlider } from '../../../components/alpha_slider';
|
||||
import { ILayer } from '../../../classes/layers/layer';
|
||||
import { AttributionFormRow } from './attribution_form_row';
|
||||
|
@ -30,6 +31,7 @@ export interface Props {
|
|||
clearLayerAttribution: (layerId: string) => void;
|
||||
setLayerAttribution: (id: string, attribution: Attribution) => void;
|
||||
updateLabel: (layerId: string, label: string) => void;
|
||||
updateLocale: (layerId: string, locale: string) => void;
|
||||
updateMinZoom: (layerId: string, minZoom: number) => void;
|
||||
updateMaxZoom: (layerId: string, maxZoom: number) => void;
|
||||
updateAlpha: (layerId: string, alpha: number) => void;
|
||||
|
@ -48,6 +50,11 @@ export function LayerSettings(props: Props) {
|
|||
props.updateLabel(layerId, label);
|
||||
};
|
||||
|
||||
const onLocaleChange = (event: ChangeEvent<HTMLSelectElement>) => {
|
||||
const { value } = event.target;
|
||||
if (value) props.updateLocale(layerId, value);
|
||||
};
|
||||
|
||||
const onZoomChange = (value: [string, string]) => {
|
||||
props.updateMinZoom(layerId, Math.max(minVisibilityZoom, parseInt(value[0], 10)));
|
||||
props.updateMaxZoom(layerId, Math.min(maxVisibilityZoom, parseInt(value[1], 10)));
|
||||
|
@ -155,6 +162,58 @@ export function LayerSettings(props: Props) {
|
|||
);
|
||||
};
|
||||
|
||||
const renderShowLocaleSelector = () => {
|
||||
if (!props.layer.supportsLabelLocales()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const options = [
|
||||
{
|
||||
text: i18n.translate(
|
||||
'xpack.maps.layerPanel.settingsPanel.labelLanguageAutoselectDropDown',
|
||||
{
|
||||
defaultMessage: 'Autoselect based on Kibana locale',
|
||||
}
|
||||
),
|
||||
value: AUTOSELECT_EMS_LOCALE,
|
||||
},
|
||||
{ value: 'ar', text: 'العربية' },
|
||||
{ value: 'de', text: 'Deutsch' },
|
||||
{ value: 'en', text: 'English' },
|
||||
{ value: 'es', text: 'Español' },
|
||||
{ value: 'fr-fr', text: 'Français' },
|
||||
{ value: 'hi-in', text: 'हिन्दी' },
|
||||
{ value: 'it', text: 'Italiano' },
|
||||
{ value: 'ja-jp', text: '日本語' },
|
||||
{ value: 'ko', text: '한국어' },
|
||||
{ value: 'pt-pt', text: 'Português' },
|
||||
{ value: 'ru-ru', text: 'русский' },
|
||||
{ value: 'zh-cn', text: '简体中文' },
|
||||
{
|
||||
text: i18n.translate('xpack.maps.layerPanel.settingsPanel.labelLanguageNoneDropDown', {
|
||||
defaultMessage: 'None',
|
||||
}),
|
||||
value: NO_EMS_LOCALE,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
label={i18n.translate('xpack.maps.layerPanel.settingsPanel.labelLanguageLabel', {
|
||||
defaultMessage: 'Label language',
|
||||
})}
|
||||
>
|
||||
<EuiSelect
|
||||
options={options}
|
||||
value={props.layer.getLocale() ?? NO_EMS_LOCALE}
|
||||
onChange={onLocaleChange}
|
||||
compressed
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiPanel>
|
||||
|
@ -172,6 +231,7 @@ export function LayerSettings(props: Props) {
|
|||
{renderZoomSliders()}
|
||||
<AlphaSlider alpha={props.layer.getAlpha()} onChange={onAlphaChange} />
|
||||
{renderShowLabelsOnTop()}
|
||||
{renderShowLocaleSelector()}
|
||||
<AttributionFormRow layer={props.layer} onChange={onAttributionChange} />
|
||||
{renderIncludeInFitToBounds()}
|
||||
</EuiPanel>
|
||||
|
|
|
@ -1483,15 +1483,16 @@
|
|||
"@elastic/transport" "^8.0.2"
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@elastic/ems-client@8.3.0":
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@elastic/ems-client/-/ems-client-8.3.0.tgz#9d40c02e33c407d433b8e509d83c5edec24c4902"
|
||||
integrity sha512-DlJDyUQzNrxGbS0AWxGiBNfq1hPQUP3Ib/Zyotgv7+VGGklb0mBwppde7WLVvuj0E+CYc6E63TJsoD8KNUO0MQ==
|
||||
"@elastic/ems-client@8.3.2":
|
||||
version "8.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@elastic/ems-client/-/ems-client-8.3.2.tgz#a12eafcfd9ac8d3068da78a5a77503ea8a89f67c"
|
||||
integrity sha512-81u+Z7+4Y2Fu+sTl9QOKdG3SVeCzzpfyCsHFR8X0V2WFCpQa+SU4sSN9WhdLHz/pe9oi6Gtt5eFMF90TOO/ckg==
|
||||
dependencies:
|
||||
"@types/geojson" "^7946.0.7"
|
||||
"@types/lru-cache" "^5.1.0"
|
||||
"@types/topojson-client" "^3.0.0"
|
||||
"@types/topojson-specification" "^1.0.1"
|
||||
chroma-js "^2.1.0"
|
||||
lodash "^4.17.15"
|
||||
lru-cache "^6.0.0"
|
||||
semver "^7.3.2"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue