[Maps] Add suggest EMS layer utility (#94969)

This commit is contained in:
Thomas Neirynck 2021-04-01 12:21:29 -04:00 committed by GitHub
parent 34b58a4f6f
commit 35ba996e84
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 506 additions and 94 deletions

View file

@ -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';

View 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);
}

View file

@ -8,3 +8,4 @@
export { MapsStartApi } from './start_api';
export { createLayerDescriptors } from './create_layer_descriptors';
export { registerLayerWizard, registerSource } from './register';
export { suggestEMSTermJoinConfig } from './ems';

View file

@ -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> {

View file

@ -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>;
}

View file

@ -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,

View file

@ -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

View file

@ -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';

View file

@ -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',
},

View 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({

View file

@ -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,

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
jest.mock('../../../meta', () => {
jest.mock('../../../util', () => {
return {
getEmsTmsServices: () => {
class MockTMSService {

View file

@ -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';

View file

@ -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 }) {

View file

@ -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 = {

View file

@ -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: {},

View file

@ -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 }) {

View file

@ -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 = {

View file

@ -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';

View file

@ -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 [];
}

View file

@ -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 {

View file

@ -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';

View file

@ -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);
});
});
});

View 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;
}

View 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';

View file

@ -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;

View file

@ -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';

View file

@ -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,
};
}
}

View file

@ -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');

View file

@ -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 },
})
);
}

View file

@ -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,

View file

@ -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,

View file

@ -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": "負荷容量",

View file

@ -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": "加载功能",