mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Maps] Add suggest EMS layer utility (#94969)
This commit is contained in:
parent
34b58a4f6f
commit
35ba996e84
37 changed files with 506 additions and 94 deletions
|
@ -300,3 +300,7 @@ export type FieldFormatter = (value: RawValue) => string | number;
|
|||
export const INDEX_META_DATA_CREATED_BY = 'maps-drawing-data-ingest';
|
||||
|
||||
export const MAX_DRAWING_SIZE_BYTES = 10485760; // 10MB
|
||||
|
||||
export const emsWorldLayerId = 'world_countries';
|
||||
export const emsRegionLayerId = 'administrative_regions_lvl2';
|
||||
export const emsUsaZipLayerId = 'usa_zip_codes';
|
||||
|
|
16
x-pack/plugins/maps/public/api/ems.ts
Normal file
16
x-pack/plugins/maps/public/api/ems.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EMSTermJoinConfig, SampleValuesConfig } from '../ems_autosuggest';
|
||||
import { lazyLoadMapModules } from '../lazy_load_bundle';
|
||||
|
||||
export async function suggestEMSTermJoinConfig(
|
||||
sampleValuesConfig: SampleValuesConfig
|
||||
): Promise<EMSTermJoinConfig | null> {
|
||||
const mapModules = await lazyLoadMapModules();
|
||||
return await mapModules.suggestEMSTermJoinConfig(sampleValuesConfig);
|
||||
}
|
|
@ -8,3 +8,4 @@
|
|||
export { MapsStartApi } from './start_api';
|
||||
export { createLayerDescriptors } from './create_layer_descriptors';
|
||||
export { registerLayerWizard, registerSource } from './register';
|
||||
export { suggestEMSTermJoinConfig } from './ems';
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SourceRegistryEntry } from '../classes/sources/source_registry';
|
||||
import { LayerWizard } from '../classes/layers/layer_wizard_registry';
|
||||
import type { SourceRegistryEntry } from '../classes/sources/source_registry';
|
||||
import type { LayerWizard } from '../classes/layers/layer_wizard_registry';
|
||||
import { lazyLoadMapModules } from '../lazy_load_bundle';
|
||||
|
||||
export async function registerLayerWizard(layerWizard: LayerWizard): Promise<void> {
|
||||
|
|
|
@ -5,10 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { LayerDescriptor } from '../../common/descriptor_types';
|
||||
import { SourceRegistryEntry } from '../classes/sources/source_registry';
|
||||
import { LayerWizard } from '../classes/layers/layer_wizard_registry';
|
||||
import type { LayerDescriptor } from '../../common/descriptor_types';
|
||||
import type { SourceRegistryEntry } from '../classes/sources/source_registry';
|
||||
import type { LayerWizard } from '../classes/layers/layer_wizard_registry';
|
||||
import type { CreateLayerDescriptorParams } from '../classes/sources/es_search_source';
|
||||
import type { SampleValuesConfig, EMSTermJoinConfig } from '../ems_autosuggest';
|
||||
|
||||
export interface MapsStartApi {
|
||||
createLayerDescriptors: {
|
||||
|
@ -23,4 +24,5 @@ export interface MapsStartApi {
|
|||
};
|
||||
registerLayerWizard(layerWizard: LayerWizard): Promise<void>;
|
||||
registerSource(entry: SourceRegistryEntry): Promise<void>;
|
||||
suggestEMSTermJoinConfig(config: SampleValuesConfig): Promise<EMSTermJoinConfig | null>;
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ import { EMSFileSelect } from '../../../components/ems_file_select';
|
|||
import { GeoIndexPatternSelect } from '../../../components/geo_index_pattern_select';
|
||||
import { SingleFieldSelect } from '../../../components/single_field_select';
|
||||
import { getGeoFields, getSourceFields, getTermsFields } from '../../../index_pattern_util';
|
||||
import { getEmsFileLayers } from '../../../meta';
|
||||
import { getEmsFileLayers } from '../../../util';
|
||||
import { getIndexPatternSelectComponent, getIndexPatternService } from '../../../kibana_services';
|
||||
import {
|
||||
createEmsChoroplethLayerDescriptor,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
jest.mock('../../meta', () => {
|
||||
jest.mock('../../util', () => {
|
||||
return {};
|
||||
});
|
||||
jest.mock('../../kibana_services', () => {
|
||||
|
@ -33,7 +33,7 @@ import { createBasemapLayerDescriptor } from './create_basemap_layer_descriptor'
|
|||
describe('kibana.yml configured with map.tilemap.url', () => {
|
||||
beforeAll(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
require('../../meta').getKibanaTileMap = () => {
|
||||
require('../../util').getKibanaTileMap = () => {
|
||||
return {
|
||||
url: 'myTileUrl',
|
||||
};
|
||||
|
@ -61,7 +61,7 @@ describe('kibana.yml configured with map.tilemap.url', () => {
|
|||
describe('EMS is enabled', () => {
|
||||
beforeAll(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
require('../../meta').getKibanaTileMap = () => {
|
||||
require('../../util').getKibanaTileMap = () => {
|
||||
return null;
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
|
@ -95,7 +95,7 @@ describe('EMS is enabled', () => {
|
|||
describe('EMS is not enabled', () => {
|
||||
beforeAll(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
require('../../meta').getKibanaTileMap = () => {
|
||||
require('../../util').getKibanaTileMap = () => {
|
||||
return null;
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import _ from 'lodash';
|
||||
import { LayerDescriptor } from '../../../common/descriptor_types';
|
||||
import { getKibanaTileMap } from '../../meta';
|
||||
import { getKibanaTileMap } from '../../util';
|
||||
import { getEMSSettings } from '../../kibana_services';
|
||||
// @ts-expect-error
|
||||
import { KibanaTilemapSource } from '../sources/kibana_tilemap_source';
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { emsWorldLayerId } from '../../../../../common';
|
||||
|
||||
jest.mock('../../../../kibana_services', () => {
|
||||
return {
|
||||
getIsDarkMode() {
|
||||
|
@ -71,7 +73,7 @@ describe('createLayerDescriptor', () => {
|
|||
maxZoom: 24,
|
||||
minZoom: 0,
|
||||
sourceDescriptor: {
|
||||
id: 'world_countries',
|
||||
id: emsWorldLayerId,
|
||||
tooltipProperties: ['name', 'iso2'],
|
||||
type: 'EMS_FILE',
|
||||
},
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
import {
|
||||
AGG_TYPE,
|
||||
COLOR_MAP_TYPE,
|
||||
emsWorldLayerId,
|
||||
FIELD_ORIGIN,
|
||||
GRID_RESOLUTION,
|
||||
RENDER_AS,
|
||||
|
@ -182,7 +183,7 @@ export function createLayerDescriptor({
|
|||
},
|
||||
],
|
||||
sourceDescriptor: EMSFileSource.createDescriptor({
|
||||
id: 'world_countries',
|
||||
id: emsWorldLayerId,
|
||||
tooltipProperties: ['name', 'iso2'],
|
||||
}),
|
||||
style: VectorStyle.createDescriptor({
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { TileLayer } from '../tile_layer/tile_layer';
|
||||
import _ from 'lodash';
|
||||
import { SOURCE_DATA_REQUEST_ID, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../../../common/constants';
|
||||
import { isRetina } from '../../../meta';
|
||||
import { isRetina } from '../../../util';
|
||||
import {
|
||||
addSpriteSheetToMapFromImageData,
|
||||
loadSpriteSheetImageData,
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
VECTOR_SHAPE_TYPE,
|
||||
FORMAT_TYPE,
|
||||
} from '../../../../common/constants';
|
||||
import { getEmsFileLayers } from '../../../meta';
|
||||
import { fetchGeoJson, getEmsFileLayers } from '../../../util';
|
||||
import { getDataSourceLabel } from '../../../../common/i18n_getters';
|
||||
import { UpdateSourceEditor } from './update_source_editor';
|
||||
import { EMSFileField } from '../../fields/ems_file_field';
|
||||
|
@ -123,12 +123,11 @@ export class EMSFileSource extends AbstractVectorSource implements IEmsFileSourc
|
|||
|
||||
async getGeoJsonWithMeta(): Promise<GeoJsonWithMeta> {
|
||||
const emsFileLayer = await this.getEMSFileLayer();
|
||||
// @ts-ignore
|
||||
const featureCollection = await AbstractVectorSource.getGeoJson({
|
||||
format: emsFileLayer.getDefaultFormatType() as FORMAT_TYPE,
|
||||
featureCollectionPath: 'data',
|
||||
fetchUrl: emsFileLayer.getDefaultFormatUrl(),
|
||||
});
|
||||
const featureCollection = await fetchGeoJson(
|
||||
emsFileLayer.getDefaultFormatUrl(),
|
||||
emsFileLayer.getDefaultFormatType() as FORMAT_TYPE,
|
||||
'data'
|
||||
);
|
||||
|
||||
const emsIdField = emsFileLayer.getFields().find((field) => {
|
||||
return field.type === 'id';
|
||||
|
|
|
@ -9,7 +9,7 @@ import React, { Component, Fragment } from 'react';
|
|||
import { EuiTitle, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { TooltipSelector } from '../../../components/tooltip_selector';
|
||||
import { getEmsFileLayers } from '../../../meta';
|
||||
import { getEmsFileLayers } from '../../../util';
|
||||
import { IEmsFileSource } from './ems_file_source';
|
||||
import { IField } from '../../fields/field';
|
||||
import { OnSourceChangeArgs } from '../../../connected_components/layer_panel/view';
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { AbstractTMSSource } from '../tms_source';
|
||||
import { getEmsTmsServices } from '../../../meta';
|
||||
import { getEmsTmsServices } from '../../../util';
|
||||
import { UpdateSourceEditor } from './update_source_editor';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getDataSourceLabel } from '../../../../common/i18n_getters';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
jest.mock('../../../meta', () => {
|
||||
jest.mock('../../../util', () => {
|
||||
return {
|
||||
getEmsTmsServices: () => {
|
||||
class MockTMSService {
|
||||
|
|
|
@ -9,7 +9,7 @@ import React, { ChangeEvent, Component } from 'react';
|
|||
import { EuiSelect, EuiSelectOption, EuiFormRow } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getEmsTmsServices } from '../../../meta';
|
||||
import { getEmsTmsServices } from '../../../util';
|
||||
import { getEmsUnavailableMessage } from '../../../components/ems_unavailable_message';
|
||||
|
||||
export const AUTO_SELECT = 'auto_select';
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { EuiSelect, EuiFormRow, EuiPanel } from '@elastic/eui';
|
||||
import { getKibanaRegionList } from '../../../meta';
|
||||
import { getKibanaRegionList } from '../../../util';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export function CreateSourceEditor({ onSourceConfigChange }) {
|
||||
|
|
|
@ -13,7 +13,7 @@ import { KibanaRegionmapSource, sourceTitle } from './kibana_regionmap_source';
|
|||
import { VectorLayer } from '../../layers/vector_layer';
|
||||
// @ts-ignore
|
||||
import { CreateSourceEditor } from './create_source_editor';
|
||||
import { getKibanaRegionList } from '../../../meta';
|
||||
import { getKibanaRegionList } from '../../../util';
|
||||
import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants';
|
||||
|
||||
export const kibanaRegionMapLayerWizardConfig: LayerWizard = {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AbstractVectorSource, GeoJsonWithMeta } from '../vector_source';
|
||||
import { getKibanaRegionList } from '../../../meta';
|
||||
import { fetchGeoJson, getKibanaRegionList } from '../../../util';
|
||||
import { getDataSourceLabel } from '../../../../common/i18n_getters';
|
||||
import { FIELD_ORIGIN, FORMAT_TYPE, SOURCE_TYPES } from '../../../../common/constants';
|
||||
import { KibanaRegionField } from '../../fields/kibana_region_field';
|
||||
|
@ -79,11 +79,12 @@ export class KibanaRegionmapSource extends AbstractVectorSource {
|
|||
|
||||
async getGeoJsonWithMeta(): Promise<GeoJsonWithMeta> {
|
||||
const vectorFileMeta = await this.getVectorFileMeta();
|
||||
const featureCollection = await AbstractVectorSource.getGeoJson({
|
||||
format: vectorFileMeta.format.type as FORMAT_TYPE,
|
||||
featureCollectionPath: vectorFileMeta.meta.feature_collection_path,
|
||||
fetchUrl: vectorFileMeta.url,
|
||||
});
|
||||
const featureCollection = await fetchGeoJson(
|
||||
vectorFileMeta.url,
|
||||
vectorFileMeta.format.type as FORMAT_TYPE,
|
||||
vectorFileMeta.meta.feature_collection_path
|
||||
);
|
||||
|
||||
return {
|
||||
data: featureCollection,
|
||||
meta: {},
|
||||
|
|
|
@ -9,7 +9,7 @@ import React, { useEffect } from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { EuiFieldText, EuiFormRow, EuiPanel } from '@elastic/eui';
|
||||
|
||||
import { getKibanaTileMap } from '../../../meta';
|
||||
import { getKibanaTileMap } from '../../../util';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export function CreateSourceEditor({ onSourceConfigChange }) {
|
||||
|
|
|
@ -13,7 +13,7 @@ import { CreateSourceEditor } from './create_source_editor';
|
|||
// @ts-ignore
|
||||
import { KibanaTilemapSource, sourceTitle } from './kibana_tilemap_source';
|
||||
import { TileLayer } from '../../layers/tile_layer/tile_layer';
|
||||
import { getKibanaTileMap } from '../../../meta';
|
||||
import { getKibanaTileMap } from '../../../util';
|
||||
import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants';
|
||||
|
||||
export const kibanaBasemapLayerWizardConfig: LayerWizard = {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { AbstractTMSSource } from '../tms_source';
|
||||
import { getKibanaTileMap } from '../../../meta';
|
||||
import { getKibanaTileMap } from '../../../util';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getDataSourceLabel } from '../../../../common/i18n_getters';
|
||||
import _ from 'lodash';
|
||||
|
|
|
@ -5,13 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
// @ts-expect-error
|
||||
import * as topojson from 'topojson-client';
|
||||
import _ from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FeatureCollection, GeoJsonProperties } from 'geojson';
|
||||
import { Filter, TimeRange } from 'src/plugins/data/public';
|
||||
import { FORMAT_TYPE, VECTOR_SHAPE_TYPE } from '../../../../common/constants';
|
||||
import { VECTOR_SHAPE_TYPE } from '../../../../common/constants';
|
||||
import { TooltipProperty, ITooltipProperty } from '../../tooltips/tooltip_property';
|
||||
import { AbstractSource, ISource } from '../source';
|
||||
import { IField } from '../../fields/field';
|
||||
|
@ -85,48 +81,6 @@ export interface ITiledSingleLayerVectorSource extends IVectorSource {
|
|||
}
|
||||
|
||||
export class AbstractVectorSource extends AbstractSource implements IVectorSource {
|
||||
static async getGeoJson({
|
||||
format,
|
||||
featureCollectionPath,
|
||||
fetchUrl,
|
||||
}: {
|
||||
format: FORMAT_TYPE;
|
||||
featureCollectionPath: string;
|
||||
fetchUrl: string;
|
||||
}) {
|
||||
let fetchedJson;
|
||||
try {
|
||||
const response = await fetch(fetchUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error('Request failed');
|
||||
}
|
||||
fetchedJson = await response.json();
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.maps.source.vetorSource.requestFailedErrorMessage', {
|
||||
defaultMessage: `Unable to fetch vector shapes from url: {fetchUrl}`,
|
||||
values: { fetchUrl },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (format === FORMAT_TYPE.GEOJSON) {
|
||||
return fetchedJson;
|
||||
}
|
||||
|
||||
if (format === FORMAT_TYPE.TOPOJSON) {
|
||||
const features = _.get(fetchedJson, `objects.${featureCollectionPath}`);
|
||||
return topojson.feature(fetchedJson, features);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
i18n.translate('xpack.maps.source.vetorSource.formatErrorMessage', {
|
||||
defaultMessage: `Unable to fetch vector shapes from url: {format}`,
|
||||
values: { format },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getFieldNames(): string[] {
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow, EuiSelect } from '@el
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FileLayer } from '@elastic/ems-client';
|
||||
import { getEmsFileLayers } from '../meta';
|
||||
import { getEmsFileLayers } from '../util';
|
||||
import { getEmsUnavailableMessage } from './ems_unavailable_message';
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -33,7 +33,7 @@ import {
|
|||
RawValue,
|
||||
ZOOM_PRECISION,
|
||||
} from '../../../common/constants';
|
||||
import { getGlyphUrl, isRetina } from '../../meta';
|
||||
import { getGlyphUrl, isRetina } from '../../util';
|
||||
import { syncLayerOrder } from './sort_layers';
|
||||
// @ts-expect-error
|
||||
import { removeOrphanedSourcesAndLayers, addSpritesheetToMap } from './utils';
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { suggestEMSTermJoinConfig } from './ems_autosuggest';
|
||||
import { FORMAT_TYPE } from '../../common';
|
||||
import { FeatureCollection } from 'geojson';
|
||||
|
||||
class MockFileLayer {
|
||||
private readonly _url: string;
|
||||
private readonly _id: string;
|
||||
private readonly _fields: Array<{ id: string }>;
|
||||
|
||||
constructor(url: string, fields: Array<{ id: string }>) {
|
||||
this._url = url;
|
||||
this._id = url;
|
||||
this._fields = fields;
|
||||
}
|
||||
getDefaultFormatUrl() {
|
||||
return this._url;
|
||||
}
|
||||
|
||||
getFields() {
|
||||
return this._fields;
|
||||
}
|
||||
|
||||
getDefaultFormatType() {
|
||||
return FORMAT_TYPE.GEOJSON;
|
||||
}
|
||||
|
||||
hasId(id: string) {
|
||||
return id === this._id;
|
||||
}
|
||||
}
|
||||
|
||||
jest.mock('../util', () => {
|
||||
return {
|
||||
async getEmsFileLayers() {
|
||||
return [
|
||||
new MockFileLayer('world_countries', [{ id: 'iso2' }, { id: 'iso3' }]),
|
||||
new MockFileLayer('zips', [{ id: 'zip' }]),
|
||||
];
|
||||
},
|
||||
async fetchGeoJson(url: string): Promise<FeatureCollection> {
|
||||
if (url === 'world_countries') {
|
||||
return ({
|
||||
type: 'FeatureCollection',
|
||||
features: [
|
||||
{ properties: { iso2: 'CA', iso3: 'CAN' } },
|
||||
{ properties: { iso2: 'US', iso3: 'USA' } },
|
||||
],
|
||||
} as unknown) as FeatureCollection;
|
||||
} else if (url === 'zips') {
|
||||
return ({
|
||||
type: 'FeatureCollection',
|
||||
features: [{ properties: { zip: '40204' } }, { properties: { zip: '40205' } }],
|
||||
} as unknown) as FeatureCollection;
|
||||
} else {
|
||||
throw new Error(`unrecognized mock url ${url}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('suggestEMSTermJoinConfig', () => {
|
||||
test('no info provided', async () => {
|
||||
const termJoinConfig = await suggestEMSTermJoinConfig({});
|
||||
expect(termJoinConfig).toBe(null);
|
||||
});
|
||||
|
||||
describe('validate common column names', () => {
|
||||
test('ecs region', async () => {
|
||||
const termJoinConfig = await suggestEMSTermJoinConfig({
|
||||
sampleValuesColumnName: 'destination.geo.region_iso_code',
|
||||
});
|
||||
expect(termJoinConfig).toEqual({
|
||||
layerId: 'administrative_regions_lvl2',
|
||||
field: 'region_iso_code',
|
||||
});
|
||||
});
|
||||
|
||||
test('ecs country', async () => {
|
||||
const termJoinConfig = await suggestEMSTermJoinConfig({
|
||||
sampleValuesColumnName: 'country_iso_code',
|
||||
});
|
||||
expect(termJoinConfig).toEqual({
|
||||
layerId: 'world_countries',
|
||||
field: 'iso2',
|
||||
});
|
||||
});
|
||||
|
||||
test('country', async () => {
|
||||
const termJoinConfig = await suggestEMSTermJoinConfig({
|
||||
sampleValuesColumnName: 'Country_name',
|
||||
});
|
||||
expect(termJoinConfig).toEqual({
|
||||
layerId: 'world_countries',
|
||||
field: 'name',
|
||||
});
|
||||
});
|
||||
|
||||
test('unknown name', async () => {
|
||||
const termJoinConfig = await suggestEMSTermJoinConfig({
|
||||
sampleValuesColumnName: 'cntry',
|
||||
});
|
||||
expect(termJoinConfig).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validate well known formats', () => {
|
||||
test('5-digit zip code', async () => {
|
||||
const termJoinConfig = await suggestEMSTermJoinConfig({
|
||||
sampleValues: ['90201', 40204],
|
||||
});
|
||||
expect(termJoinConfig).toEqual({
|
||||
layerId: 'usa_zip_codes',
|
||||
field: 'zip',
|
||||
});
|
||||
});
|
||||
|
||||
test('mismatch', async () => {
|
||||
const termJoinConfig = await suggestEMSTermJoinConfig({
|
||||
sampleValues: ['90201', 'foobar'],
|
||||
});
|
||||
expect(termJoinConfig).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validate based on EMS data', () => {
|
||||
test('Should validate with zip codes layer', async () => {
|
||||
const termJoinConfig = await suggestEMSTermJoinConfig({
|
||||
sampleValues: ['40204', 40205],
|
||||
emsLayerIds: ['world_countries', 'zips'],
|
||||
});
|
||||
expect(termJoinConfig).toEqual({
|
||||
layerId: 'zips',
|
||||
field: 'zip',
|
||||
});
|
||||
});
|
||||
|
||||
test('Should not validate with faulty zip codes', async () => {
|
||||
const termJoinConfig = await suggestEMSTermJoinConfig({
|
||||
sampleValues: ['40204', '00000'],
|
||||
emsLayerIds: ['world_countries', 'zips'],
|
||||
});
|
||||
expect(termJoinConfig).toEqual(null);
|
||||
});
|
||||
|
||||
test('Should validate against countries', async () => {
|
||||
const termJoinConfig = await suggestEMSTermJoinConfig({
|
||||
sampleValues: ['USA', 'USA', 'CAN'],
|
||||
emsLayerIds: ['world_countries', 'zips'],
|
||||
});
|
||||
expect(termJoinConfig).toEqual({
|
||||
layerId: 'world_countries',
|
||||
field: 'iso3',
|
||||
});
|
||||
});
|
||||
|
||||
test('Should not validate against missing countries', async () => {
|
||||
const termJoinConfig = await suggestEMSTermJoinConfig({
|
||||
sampleValues: ['USA', 'BEL', 'CAN'],
|
||||
emsLayerIds: ['world_countries', 'zips'],
|
||||
});
|
||||
expect(termJoinConfig).toEqual(null);
|
||||
});
|
||||
});
|
||||
});
|
201
x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts
Normal file
201
x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts
Normal file
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { FileLayer } from '@elastic/ems-client';
|
||||
import { getEmsFileLayers, fetchGeoJson } from '../util';
|
||||
import { FORMAT_TYPE, emsWorldLayerId, emsRegionLayerId, emsUsaZipLayerId } from '../../common';
|
||||
|
||||
export interface SampleValuesConfig {
|
||||
emsLayerIds?: string[];
|
||||
sampleValues?: Array<string | number>;
|
||||
sampleValuesColumnName?: string;
|
||||
}
|
||||
|
||||
export interface EMSTermJoinConfig {
|
||||
layerId: string;
|
||||
field: string;
|
||||
}
|
||||
|
||||
const wellKnownColumnNames = [
|
||||
{
|
||||
regex: /(geo\.){0,}country_iso_code$/i, // ECS postfix for country
|
||||
emsConfig: {
|
||||
layerId: emsWorldLayerId,
|
||||
field: 'iso2',
|
||||
},
|
||||
},
|
||||
{
|
||||
regex: /(geo\.){0,}region_iso_code$/i, // ECS postfixn for region
|
||||
emsConfig: {
|
||||
layerId: emsRegionLayerId,
|
||||
field: 'region_iso_code',
|
||||
},
|
||||
},
|
||||
{
|
||||
regex: /^country/i, // anything starting with country
|
||||
emsConfig: {
|
||||
layerId: emsWorldLayerId,
|
||||
field: 'name',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const wellKnownColumnFormats = [
|
||||
{
|
||||
regex: /(^\d{5}$)/i, // 5-digit zipcode
|
||||
emsConfig: {
|
||||
layerId: emsUsaZipLayerId,
|
||||
field: 'zip',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
interface UniqueMatch {
|
||||
config: { layerId: string; field: string };
|
||||
count: number;
|
||||
}
|
||||
|
||||
export async function suggestEMSTermJoinConfig(
|
||||
sampleValuesConfig: SampleValuesConfig
|
||||
): Promise<EMSTermJoinConfig | null> {
|
||||
const matches: EMSTermJoinConfig[] = [];
|
||||
|
||||
if (sampleValuesConfig.sampleValuesColumnName) {
|
||||
matches.push(...suggestByName(sampleValuesConfig.sampleValuesColumnName));
|
||||
}
|
||||
|
||||
if (sampleValuesConfig.sampleValues && sampleValuesConfig.sampleValues.length) {
|
||||
if (sampleValuesConfig.emsLayerIds && sampleValuesConfig.emsLayerIds.length) {
|
||||
matches.push(
|
||||
...(await suggestByEMSLayerIds(
|
||||
sampleValuesConfig.emsLayerIds,
|
||||
sampleValuesConfig.sampleValues
|
||||
))
|
||||
);
|
||||
} else {
|
||||
matches.push(...suggestByValues(sampleValuesConfig.sampleValues));
|
||||
}
|
||||
}
|
||||
|
||||
const uniqMatches: UniqueMatch[] = matches.reduce((accum: UniqueMatch[], match) => {
|
||||
const found = accum.find((m) => {
|
||||
return m.config.layerId === match.layerId && m.config.field === match.layerId;
|
||||
});
|
||||
|
||||
if (found) {
|
||||
found.count += 1;
|
||||
} else {
|
||||
accum.push({
|
||||
config: match,
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
|
||||
return accum;
|
||||
}, []);
|
||||
|
||||
uniqMatches.sort((a, b) => {
|
||||
return b.count - a.count;
|
||||
});
|
||||
|
||||
return uniqMatches.length ? uniqMatches[0].config : null;
|
||||
}
|
||||
|
||||
function suggestByName(columnName: string): EMSTermJoinConfig[] {
|
||||
const matches = wellKnownColumnNames.filter((wellknown) => {
|
||||
return columnName.match(wellknown.regex);
|
||||
});
|
||||
|
||||
return matches.map((m) => {
|
||||
return m.emsConfig;
|
||||
});
|
||||
}
|
||||
|
||||
function suggestByValues(values: Array<string | number>): EMSTermJoinConfig[] {
|
||||
const matches = wellKnownColumnFormats.filter((wellknown) => {
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const value = values[i].toString();
|
||||
if (!value.match(wellknown.regex)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return matches.map((m) => {
|
||||
return m.emsConfig;
|
||||
});
|
||||
}
|
||||
|
||||
function existsInEMS(emsJson: any, emsFieldId: string, sampleValue: string): boolean {
|
||||
for (let i = 0; i < emsJson.features.length; i++) {
|
||||
const emsFieldValue = emsJson.features[i].properties[emsFieldId].toString();
|
||||
if (emsFieldValue.toString() === sampleValue) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function matchesEmsField(emsJson: any, emsFieldId: string, sampleValues: Array<string | number>) {
|
||||
for (let j = 0; j < sampleValues.length; j++) {
|
||||
const sampleValue = sampleValues[j].toString();
|
||||
if (!existsInEMS(emsJson, emsFieldId, sampleValue)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function getMatchesForEMSLayer(
|
||||
emsLayerId: string,
|
||||
sampleValues: Array<string | number>
|
||||
): Promise<EMSTermJoinConfig[]> {
|
||||
const fileLayers: FileLayer[] = await getEmsFileLayers();
|
||||
const emsFileLayer: FileLayer | undefined = fileLayers.find((fl: FileLayer) =>
|
||||
fl.hasId(emsLayerId)
|
||||
);
|
||||
|
||||
if (!emsFileLayer) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const emsFields = emsFileLayer.getFields();
|
||||
const url = emsFileLayer.getDefaultFormatUrl();
|
||||
|
||||
try {
|
||||
const emsJson = await fetchGeoJson(
|
||||
url,
|
||||
emsFileLayer.getDefaultFormatType() as FORMAT_TYPE,
|
||||
'data'
|
||||
);
|
||||
const matches: EMSTermJoinConfig[] = [];
|
||||
for (let f = 0; f < emsFields.length; f++) {
|
||||
if (matchesEmsField(emsJson, emsFields[f].id, sampleValues)) {
|
||||
matches.push({
|
||||
layerId: emsLayerId,
|
||||
field: emsFields[f].id,
|
||||
});
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function suggestByEMSLayerIds(
|
||||
emsLayerIds: string[],
|
||||
values: Array<string | number>
|
||||
): Promise<EMSTermJoinConfig[]> {
|
||||
const matches = [];
|
||||
for (const emsLayerId of emsLayerIds) {
|
||||
const layerIdMathes = await getMatchesForEMSLayer(emsLayerId, values);
|
||||
matches.push(...layerIdMathes);
|
||||
}
|
||||
return matches;
|
||||
}
|
8
x-pack/plugins/maps/public/ems_autosuggest/index.ts
Normal file
8
x-pack/plugins/maps/public/ems_autosuggest/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './ems_autosuggest';
|
|
@ -14,6 +14,7 @@ import { MapEmbeddableConfig, MapEmbeddableInput, MapEmbeddableOutput } from '..
|
|||
import { SourceRegistryEntry } from '../classes/sources/source_registry';
|
||||
import { LayerWizard } from '../classes/layers/layer_wizard_registry';
|
||||
import type { CreateLayerDescriptorParams } from '../classes/sources/es_search_source';
|
||||
import type { EMSTermJoinConfig, SampleValuesConfig } from '../ems_autosuggest';
|
||||
|
||||
let loadModulesPromise: Promise<LazyLoadedMapModules>;
|
||||
|
||||
|
@ -74,6 +75,7 @@ interface LazyLoadedMapModules {
|
|||
}) => LayerDescriptor | null;
|
||||
createBasemapLayerDescriptor: () => LayerDescriptor | null;
|
||||
createESSearchSourceLayerDescriptor: (params: CreateLayerDescriptorParams) => LayerDescriptor;
|
||||
suggestEMSTermJoinConfig: (config: SampleValuesConfig) => Promise<EMSTermJoinConfig | null>;
|
||||
}
|
||||
|
||||
export async function lazyLoadMapModules(): Promise<LazyLoadedMapModules> {
|
||||
|
@ -94,6 +96,7 @@ export async function lazyLoadMapModules(): Promise<LazyLoadedMapModules> {
|
|||
createRegionMapLayerDescriptor,
|
||||
createBasemapLayerDescriptor,
|
||||
createESSearchSourceLayerDescriptor,
|
||||
suggestEMSTermJoinConfig,
|
||||
} = await import('./lazy');
|
||||
|
||||
resolve({
|
||||
|
@ -108,6 +111,7 @@ export async function lazyLoadMapModules(): Promise<LazyLoadedMapModules> {
|
|||
createRegionMapLayerDescriptor,
|
||||
createBasemapLayerDescriptor,
|
||||
createESSearchSourceLayerDescriptor,
|
||||
suggestEMSTermJoinConfig,
|
||||
});
|
||||
});
|
||||
return loadModulesPromise;
|
||||
|
|
|
@ -16,3 +16,4 @@ export { createTileMapLayerDescriptor } from '../../classes/layers/create_tile_m
|
|||
export { createRegionMapLayerDescriptor } from '../../classes/layers/create_region_map_layer_descriptor';
|
||||
export { createBasemapLayerDescriptor } from '../../classes/layers/create_basemap_layer_descriptor';
|
||||
export { createLayerDescriptor as createESSearchSourceLayerDescriptor } from '../../classes/sources/es_search_source';
|
||||
export { suggestEMSTermJoinConfig } from '../../ems_autosuggest';
|
||||
|
|
|
@ -47,8 +47,13 @@ import type { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/e
|
|||
import { MapsXPackConfig, MapsConfigType } from '../config';
|
||||
import { getAppTitle } from '../common/i18n_getters';
|
||||
import { lazyLoadMapModules } from './lazy_load_bundle';
|
||||
import { MapsStartApi } from './api';
|
||||
import { createLayerDescriptors, registerLayerWizard, registerSource } from './api';
|
||||
import {
|
||||
createLayerDescriptors,
|
||||
registerLayerWizard,
|
||||
registerSource,
|
||||
MapsStartApi,
|
||||
suggestEMSTermJoinConfig,
|
||||
} from './api';
|
||||
import type { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public';
|
||||
import type { MapsEmsPluginSetup } from '../../../../src/plugins/maps_ems/public';
|
||||
import type { DataPublicPluginStart } from '../../../../src/plugins/data/public';
|
||||
|
@ -177,6 +182,7 @@ export class MapsPlugin
|
|||
createLayerDescriptors,
|
||||
registerLayerWizard,
|
||||
registerSource,
|
||||
suggestEMSTermJoinConfig,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { EMSClient } from '@elastic/ems-client';
|
||||
import { getEMSClient, getGlyphUrl } from './meta';
|
||||
import { getEMSClient, getGlyphUrl } from './util';
|
||||
|
||||
jest.mock('@elastic/ems-client');
|
||||
|
|
@ -7,6 +7,10 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EMSClient, FileLayer, TMSService } from '@elastic/ems-client';
|
||||
import { FeatureCollection } from 'geojson';
|
||||
// @ts-expect-error
|
||||
import * as topojson from 'topojson-client';
|
||||
import _ from 'lodash';
|
||||
|
||||
import fetch from 'node-fetch';
|
||||
import {
|
||||
|
@ -16,6 +20,7 @@ import {
|
|||
EMS_GLYPHS_PATH,
|
||||
EMS_APP_NAME,
|
||||
FONTS_API_PATH,
|
||||
FORMAT_TYPE,
|
||||
} from '../common/constants';
|
||||
import {
|
||||
getHttp,
|
||||
|
@ -113,3 +118,41 @@ export function getGlyphUrl(): string {
|
|||
export function isRetina(): boolean {
|
||||
return window.devicePixelRatio === 2;
|
||||
}
|
||||
|
||||
export async function fetchGeoJson(
|
||||
fetchUrl: string,
|
||||
format: FORMAT_TYPE,
|
||||
featureCollectionPath: string
|
||||
): Promise<FeatureCollection> {
|
||||
let fetchedJson;
|
||||
try {
|
||||
const response = await fetch(fetchUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error('Request failed');
|
||||
}
|
||||
fetchedJson = await response.json();
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.maps.util.requestFailedErrorMessage', {
|
||||
defaultMessage: `Unable to fetch vector shapes from url: {fetchUrl}`,
|
||||
values: { fetchUrl },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (format === FORMAT_TYPE.GEOJSON) {
|
||||
return fetchedJson;
|
||||
}
|
||||
|
||||
if (format === FORMAT_TYPE.TOPOJSON) {
|
||||
const features = _.get(fetchedJson, `objects.${featureCollectionPath}`);
|
||||
return topojson.feature(fetchedJson, features);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
i18n.translate('xpack.maps.util.formatErrorMessage', {
|
||||
defaultMessage: `Unable to fetch vector shapes from url: {format}`,
|
||||
values: { format },
|
||||
})
|
||||
);
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { emsWorldLayerId } from '../../common';
|
||||
|
||||
const layerList = [
|
||||
{
|
||||
|
@ -29,7 +30,7 @@ const layerList = [
|
|||
alpha: 1,
|
||||
sourceDescriptor: {
|
||||
type: 'EMS_FILE',
|
||||
id: 'world_countries',
|
||||
id: emsWorldLayerId,
|
||||
tooltipProperties: ['name', 'iso2'],
|
||||
},
|
||||
visible: true,
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { emsWorldLayerId } from '../../common';
|
||||
|
||||
const layerList = [
|
||||
{
|
||||
|
@ -29,7 +30,7 @@ const layerList = [
|
|||
alpha: 0.5,
|
||||
sourceDescriptor: {
|
||||
type: 'EMS_FILE',
|
||||
id: 'world_countries',
|
||||
id: emsWorldLayerId,
|
||||
tooltipProperties: ['name', 'iso2'],
|
||||
},
|
||||
visible: true,
|
||||
|
|
|
@ -12806,8 +12806,6 @@
|
|||
"xpack.maps.source.pewPewDescription": "ソースとデスティネーションの間の集約データパスです。",
|
||||
"xpack.maps.source.pewPewTitle": "ソースとデスティネーションの接続",
|
||||
"xpack.maps.source.urlLabel": "Url",
|
||||
"xpack.maps.source.vetorSource.formatErrorMessage": "URL からベクターシェイプを取得できません:{format}",
|
||||
"xpack.maps.source.vetorSource.requestFailedErrorMessage": "URL からベクターシェイプを取得できません:{fetchUrl}",
|
||||
"xpack.maps.source.wms.attributionLink": "属性テキストにはリンクが必要です",
|
||||
"xpack.maps.source.wms.attributionText": "属性 URL にはテキストが必要です",
|
||||
"xpack.maps.source.wms.getCapabilitiesButtonText": "負荷容量",
|
||||
|
|
|
@ -12974,8 +12974,6 @@
|
|||
"xpack.maps.source.pewPewDescription": "源和目标之间的聚合数据路径",
|
||||
"xpack.maps.source.pewPewTitle": "源-目标连接",
|
||||
"xpack.maps.source.urlLabel": "URL",
|
||||
"xpack.maps.source.vetorSource.formatErrorMessage": "无法从以下 URL 获取矢量形状:{format}",
|
||||
"xpack.maps.source.vetorSource.requestFailedErrorMessage": "无法从以下 URL 获取矢量形状:{fetchUrl}",
|
||||
"xpack.maps.source.wms.attributionLink": "属性文本必须附带链接",
|
||||
"xpack.maps.source.wms.attributionText": "属性 url 必须附带文本",
|
||||
"xpack.maps.source.wms.getCapabilitiesButtonText": "加载功能",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue