diff --git a/x-pack/plugins/maps/common/elasticsearch_geo_utils.d.ts b/x-pack/plugins/maps/common/elasticsearch_geo_utils.d.ts index 44250360e9d00..f15eb6d8a4f71 100644 --- a/x-pack/plugins/maps/common/elasticsearch_geo_utils.d.ts +++ b/x-pack/plugins/maps/common/elasticsearch_geo_utils.d.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { FeatureCollection, GeoJsonProperties } from 'geojson'; import { MapExtent } from './descriptor_types'; +import { ES_GEO_FIELD_TYPE } from './constants'; export function scaleBounds(bounds: MapExtent, scaleFactor: number): MapExtent; @@ -13,3 +15,11 @@ export function turfBboxToBounds(turfBbox: unknown): MapExtent; export function clampToLatBounds(lat: number): number; export function clampToLonBounds(lon: number): number; + +export function hitsToGeoJson( + hits: any[], + flattenHit: (geojsonProperties: GeoJsonProperties) => GeoJsonProperties, + geoFieldName: string, + geoFieldType: ES_GEO_FIELD_TYPE, + epochMillisFields: string[] +): FeatureCollection; diff --git a/x-pack/plugins/maps/common/elasticsearch_geo_utils.js b/x-pack/plugins/maps/common/elasticsearch_geo_utils.js index f2bf83ae18bb0..4122537ef7297 100644 --- a/x-pack/plugins/maps/common/elasticsearch_geo_utils.js +++ b/x-pack/plugins/maps/common/elasticsearch_geo_utils.js @@ -97,6 +97,7 @@ export function hitsToGeoJson(hits, flattenHit, geoFieldName, geoFieldType, epoc delete properties[geoFieldName]; //create new geojson Feature for every individual geojson geometry. + //todo: Consider using GeometryCollection instead. for (let j = 0; j < tmpGeometriesAccumulator.length; j++) { features.push({ type: 'Feature', diff --git a/x-pack/plugins/maps/server/mvt/__tests__/pbf/0_0_0.pbf b/x-pack/plugins/maps/server/mvt/__tests__/pbf/0_0_0.pbf index 386769f4f2420..d8e5ae193319b 100644 Binary files a/x-pack/plugins/maps/server/mvt/__tests__/pbf/0_0_0.pbf and b/x-pack/plugins/maps/server/mvt/__tests__/pbf/0_0_0.pbf differ diff --git a/x-pack/plugins/maps/server/mvt/__tests__/tile_searches.ts b/x-pack/plugins/maps/server/mvt/__tests__/tile_searches.ts index 5d762b8ed517c..7ae9c0c499976 100644 --- a/x-pack/plugins/maps/server/mvt/__tests__/tile_searches.ts +++ b/x-pack/plugins/maps/server/mvt/__tests__/tile_searches.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-expect-error -const search000json = await import('./json/0_0_0_search.json'); // Prefer require() over setting the compiler options, which affect production modules as well +const search000json = require('./json/0_0_0_search.json'); // Prefer require() over setting the compiler options, which affect production modules as well export const TILE_SEARCHES = { '0.0.0': { diff --git a/x-pack/plugins/maps/server/mvt/get_tile.ts b/x-pack/plugins/maps/server/mvt/get_tile.ts index 764e7f93351f0..eebd4f0fb4f2a 100644 --- a/x-pack/plugins/maps/server/mvt/get_tile.ts +++ b/x-pack/plugins/maps/server/mvt/get_tile.ts @@ -9,13 +9,17 @@ import geojsonvt from 'geojson-vt'; // @ts-expect-error import vtpbf from 'vt-pbf'; import { Logger } from 'src/core/server'; -import { Feature, FeatureCollection, GeoJsonProperties, Geometry, Polygon } from 'geojson'; +import { Feature, FeatureCollection, GeoJsonProperties, Polygon } from 'geojson'; import { + ES_GEO_FIELD_TYPE, FEATURE_ID_PROPERTY_NAME, - MVT_SOURCE_LAYER_NAME, KBN_TOO_MANY_FEATURES_PROPERTY, + MVT_SOURCE_LAYER_NAME, } from '../../common/constants'; +import { hitsToGeoJson } from '../../common/elasticsearch_geo_utils'; +import { flattenHit } from './util'; + interface ESBounds { top_left: { lon: number; @@ -116,70 +120,28 @@ export async function getTile({ // Perform actual search result = await callElasticsearch('search', esSearchQuery); - // @ts-expect-error - const hitsFeatures: Array = result.hits.hits.map( - (hit: any): Feature | null => { - let geomType: - | 'Point' - | 'MultiPoint' - | 'LineString' - | 'MultiLineString' - | 'Polygon' - | 'MultiPolygon'; - const geometry = hit._source[geometryFieldName]; - if (geometry.type === 'polygon' || geometry.type === 'Polygon') { - geomType = 'Polygon'; - } else if (geometry.type === 'multipolygon' || geometry.type === 'MultiPolygon') { - geomType = 'MultiPolygon'; - } else if (geometry.type === 'linestring' || geometry.type === 'LineString') { - geomType = 'LineString'; - } else if (geometry.type === 'multilinestring' || geometry.type === 'MultiLineString') { - geomType = 'MultiLineString'; - } else if (geometry.type === 'point' || geometry.type === 'Point') { - geomType = 'Point'; - } else if (geometry.type === 'MultiPoint' || geometry.type === 'multipoint') { - geomType = 'MultiPoint'; - } else { - return null; - } - const geometryGeoJson: Geometry = { - type: geomType, - coordinates: geometry.coordinates, - }; - - const firstFields: GeoJsonProperties = {}; - if (hit.fields) { - const fields = hit.fields; - Object.keys(fields).forEach((key) => { - const value = fields[key]; - if (Array.isArray(value)) { - firstFields[key] = value[0]; - } else { - firstFields[key] = value; - } - }); - } + // Todo: pass in epochMillies-fields + const featureCollection = hitsToGeoJson( + // @ts-expect-error + result.hits.hits, + (hit: GeoJsonProperties) => { + return flattenHit(geometryFieldName, hit); + }, + geometryFieldName, + ES_GEO_FIELD_TYPE.GEO_SHAPE, + [] + ); - const properties = { - ...hit._source, - ...firstFields, - _id: hit._id, - _index: hit._index, - [FEATURE_ID_PROPERTY_NAME]: hit._id, - [KBN_TOO_MANY_FEATURES_PROPERTY]: false, - }; - delete properties[geometryFieldName]; + resultFeatures = featureCollection.features; - return { - type: 'Feature', - id: hit._id, - geometry: geometryGeoJson, - properties, - }; + // Correct system-fields. + for (let i = 0; i < resultFeatures.length; i++) { + const props = resultFeatures[i].properties; + if (props !== null) { + props[FEATURE_ID_PROPERTY_NAME] = resultFeatures[i].id; + props[KBN_TOO_MANY_FEATURES_PROPERTY] = false; } - ); - - resultFeatures = hitsFeatures.filter((f) => !!f) as Feature[]; + } } } catch (e) { logger.warn(e.message); diff --git a/x-pack/plugins/maps/server/mvt/util.ts b/x-pack/plugins/maps/server/mvt/util.ts new file mode 100644 index 0000000000000..336dad953b446 --- /dev/null +++ b/x-pack/plugins/maps/server/mvt/util.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// This implementation: +// - does not include meta-fields +// - does not validate the schema against the index-pattern (e.g. nested fields) +// In the context of .mvt this is sufficient: +// - only fields from the response are packed in the tile (more efficient) +// - query-dsl submitted from the client, which was generated by the IndexPattern +// todo: Ideally, this should adapt/reuse from https://github.com/elastic/kibana/blob/52b42a81faa9dd5c102b9fbb9a645748c3623121/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts#L26 +import { GeoJsonProperties } from 'geojson'; + +export function flattenHit(geometryField: string, hit: GeoJsonProperties): GeoJsonProperties { + const flat: GeoJsonProperties = {}; + if (hit) { + flattenSource(flat, '', hit._source, geometryField); + if (hit.fields) { + flattenFields(flat, hit.fields); + } + + // Attach meta fields + flat._index = hit._index; + flat._id = hit._id; + } + return flat; +} + +function flattenSource( + accum: GeoJsonProperties, + path: string, + properties: GeoJsonProperties = {}, + geometryField: string +): GeoJsonProperties { + accum = accum || {}; + for (const key in properties) { + if (properties.hasOwnProperty(key)) { + const newKey = path ? path + '.' + key : key; + let value; + if (geometryField === newKey) { + value = properties[key]; // do not deep-copy the geometry + } else if (properties[key] !== null && typeof value === 'object' && !Array.isArray(value)) { + value = flattenSource(accum, newKey, properties[key], geometryField); + } else { + value = properties[key]; + } + accum[newKey] = value; + } + } + return accum; +} + +function flattenFields(accum: GeoJsonProperties = {}, fields: GeoJsonProperties[]) { + accum = accum || {}; + for (const key in fields) { + if (fields.hasOwnProperty(key)) { + const value = fields[key]; + if (Array.isArray(value)) { + accum[key] = value[0]; + } else { + accum[key] = value; + } + } + } +}