From f841ad369b7688eac679a328c2ab39ecdf699d91 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 4 Mar 2021 11:01:33 -0700 Subject: [PATCH] [Maps] Support GeometryCollections in GeoJson upload (#93507) --- .../geojson_importer/geojson_importer.test.js | 71 +++++++++- .../geojson_importer/geojson_importer.ts | 32 +---- .../maps/common/get_centroid_features.test.ts | 127 ++++++++++++++++++ .../maps/common/get_centroid_features.ts | 101 ++++++++------ 4 files changed, 267 insertions(+), 64 deletions(-) diff --git a/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.test.js b/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.test.js index 98f14f3e58166..3542b38aac793 100644 --- a/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.test.js +++ b/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.test.js @@ -25,6 +25,29 @@ const FEATURE_COLLECTION = { ], }; +const GEOMETRY_COLLECTION_FEATURE = { + type: 'Feature', + properties: { + population: 200, + }, + geometry: { + type: 'GeometryCollection', + geometries: [ + { + type: 'Point', + coordinates: [100.0, 0.0], + }, + { + type: 'LineString', + coordinates: [ + [101.0, 0.0], + [102.0, 1.0], + ], + }, + ], + }, +}; + describe('previewFile', () => { const FILE_WITH_FEATURE_COLLECTION = new File( [JSON.stringify(FEATURE_COLLECTION)], @@ -60,6 +83,27 @@ describe('previewFile', () => { }); }); + test('should read GeometryCollection feature', async () => { + const fileWithGeometryCollectionFeature = new File( + [ + JSON.stringify({ + type: 'FeatureCollection', + features: [GEOMETRY_COLLECTION_FEATURE], + }), + ], + 'testfile.json', + { type: 'text/json' } + ); + const importer = new GeoJsonImporter(fileWithGeometryCollectionFeature); + const results = await importer.previewFile(); + expect(results).toEqual({ + previewCoverage: 100, + hasPoints: false, + hasShapes: true, + features: [GEOMETRY_COLLECTION_FEATURE], + }); + }); + test('should remove features without geometry', async () => { const fileWithFeaturesWithoutGeometry = new File( [ @@ -193,11 +237,36 @@ describe('toEsDocs', () => { expect(esDocs).toEqual([ { coordinates: { - type: 'point', + type: 'Point', coordinates: [-112.0372, 46.608058], }, population: 200, }, ]); }); + + test('should convert GeometryCollection feature to geo_shape ES documents', () => { + const esDocs = toEsDocs([GEOMETRY_COLLECTION_FEATURE], ES_FIELD_TYPES.GEO_SHAPE); + expect(esDocs).toEqual([ + { + coordinates: { + type: 'GeometryCollection', + geometries: [ + { + type: 'Point', + coordinates: [100.0, 0.0], + }, + { + type: 'LineString', + coordinates: [ + [101.0, 0.0], + [102.0, 1.0], + ], + }, + ], + }, + population: 200, + }, + ]); + }); }); diff --git a/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.ts b/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.ts index 3294655916b5b..80ff9325b4fe0 100644 --- a/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.ts +++ b/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.ts @@ -5,15 +5,7 @@ * 2.0. */ -import { - Feature, - Point, - MultiPoint, - LineString, - MultiLineString, - Polygon, - MultiPolygon, -} from 'geojson'; +import { Feature, Point } from 'geojson'; import { i18n } from '@kbn/i18n'; // @ts-expect-error import { JSONLoader, loadInBatches } from './loaders'; @@ -71,7 +63,8 @@ export class GeoJsonImporter extends Importer { this._geometryTypesMap.has('LineString') || this._geometryTypesMap.has('MultiLineString') || this._geometryTypesMap.has('Polygon') || - this._geometryTypesMap.has('MultiPolygon'), + this._geometryTypesMap.has('MultiPolygon') || + this._geometryTypesMap.has('GeometryCollection'), }; } @@ -266,23 +259,12 @@ export function toEsDocs( const esDocs = []; for (let i = 0; i < features.length; i++) { const feature = features[i]; - const geometry = feature.geometry as - | Point - | MultiPoint - | LineString - | MultiLineString - | Polygon - | MultiPolygon; - const coordinates = - geoFieldType === ES_FIELD_TYPES.GEO_SHAPE - ? { - type: geometry.type.toLowerCase(), - coordinates: geometry.coordinates, - } - : geometry.coordinates; const properties = feature.properties ? feature.properties : {}; esDocs.push({ - coordinates, + coordinates: + geoFieldType === ES_FIELD_TYPES.GEO_SHAPE + ? feature.geometry + : (feature.geometry as Point).coordinates, ...properties, }); } diff --git a/x-pack/plugins/maps/common/get_centroid_features.test.ts b/x-pack/plugins/maps/common/get_centroid_features.test.ts index d409c9b71a59a..57879989644f1 100644 --- a/x-pack/plugins/maps/common/get_centroid_features.test.ts +++ b/x-pack/plugins/maps/common/get_centroid_features.test.ts @@ -281,3 +281,130 @@ test('should create centroid feature for multi polygon', () => { }, }); }); + +test('should create centroid feature for GeometryCollection with Point', () => { + const featureCollection: FeatureCollection = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'GeometryCollection', + geometries: [ + { + type: 'Point', + coordinates: [100.0, 0.0], + }, + ], + }, + properties: { + prop0: 'value0', + prop1: 0.0, + }, + }, + ], + }; + const centroidFeatures = getCentroidFeatures(featureCollection); + expect(centroidFeatures.length).toBe(1); + expect(centroidFeatures[0]).toEqual({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [100.0, 0.0], + }, + properties: { + __kbn_is_centroid_feature__: true, + prop0: 'value0', + prop1: 0.0, + }, + }); +}); + +test('should create centroid feature for GeometryCollection with MultiPoint', () => { + const featureCollection: FeatureCollection = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'GeometryCollection', + geometries: [ + { + type: 'MultiPoint', + coordinates: [ + [10, 40], + [40, 30], + [20, 20], + [30, 10], + ], + }, + ], + }, + properties: { + prop0: 'value0', + prop1: 0.0, + }, + }, + ], + }; + const centroidFeatures = getCentroidFeatures(featureCollection); + expect(centroidFeatures.length).toBe(1); + expect(centroidFeatures[0]).toEqual({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [10, 40], + }, + properties: { + __kbn_is_centroid_feature__: true, + prop0: 'value0', + prop1: 0.0, + }, + }); +}); + +test('should create centroid feature for GeometryCollection with Polygon', () => { + const featureCollection: FeatureCollection = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'GeometryCollection', + geometries: [ + { + type: 'Polygon', + coordinates: [ + [ + [35, 10], + [45, 45], + [15, 40], + [10, 20], + [35, 10], + ], + ], + }, + ], + }, + properties: { + prop0: 'value0', + prop1: 0.0, + }, + }, + ], + }; + const centroidFeatures = getCentroidFeatures(featureCollection); + expect(centroidFeatures.length).toBe(1); + expect(centroidFeatures[0]).toEqual({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [27.526881720430108, 28.70967741935484], + }, + properties: { + __kbn_is_centroid_feature__: true, + prop0: 'value0', + prop1: 0.0, + }, + }); +}); diff --git a/x-pack/plugins/maps/common/get_centroid_features.ts b/x-pack/plugins/maps/common/get_centroid_features.ts index f8dd897d24e02..ce82b7d875fbd 100644 --- a/x-pack/plugins/maps/common/get_centroid_features.ts +++ b/x-pack/plugins/maps/common/get_centroid_features.ts @@ -9,8 +9,10 @@ import { Feature, FeatureCollection, Geometry, + GeometryCollection, LineString, MultiLineString, + MultiPoint, MultiPolygon, } from 'geojson'; import turfAlong from '@turf/along'; @@ -26,7 +28,7 @@ import { } from './constants'; export function getCentroidFeatures(featureCollection: FeatureCollection): Feature[] { - const centroidFeatures = []; + const centroids = []; for (let i = 0; i < featureCollection.features.length; i++) { const feature = featureCollection.features[i]; @@ -35,43 +37,68 @@ export function getCentroidFeatures(featureCollection: FeatureCollection): Featu continue; } - let centroidGeometry: Geometry | null = null; - if (feature.geometry.type === GEO_JSON_TYPE.LINE_STRING) { - centroidGeometry = getLineCentroid(feature); - } else if (feature.geometry.type === GEO_JSON_TYPE.MULTI_LINE_STRING) { - const coordinates = (feature.geometry as MultiLineString).coordinates; - let longestLine = coordinates[0]; - let longestLength = turfLength(lineString(longestLine)); - for (let j = 1; j < coordinates.length; j++) { - const nextLine = coordinates[j]; - const nextLength = turfLength(lineString(nextLine)); - if (nextLength > longestLength) { - longestLine = nextLine; - longestLength = nextLength; - } + const centroid = getCentroid(feature); + if (centroid) { + centroids.push(centroid); + } + } + return centroids; +} + +export function getCentroid(feature: Feature): Feature | null { + let centroidGeometry: Geometry | null = null; + if (feature.geometry.type === GEO_JSON_TYPE.LINE_STRING) { + centroidGeometry = getLineCentroid(feature); + } else if (feature.geometry.type === GEO_JSON_TYPE.MULTI_LINE_STRING) { + const coordinates = (feature.geometry as MultiLineString).coordinates; + let longestLine = coordinates[0]; + let longestLength = turfLength(lineString(longestLine)); + for (let j = 1; j < coordinates.length; j++) { + const nextLine = coordinates[j]; + const nextLength = turfLength(lineString(nextLine)); + if (nextLength > longestLength) { + longestLine = nextLine; + longestLength = nextLength; } - centroidGeometry = getLineCentroid(lineString(longestLine) as Feature); - } else if (feature.geometry.type === GEO_JSON_TYPE.POLYGON) { - centroidGeometry = turfCenterOfMass(feature).geometry; - } else if (feature.geometry.type === GEO_JSON_TYPE.MULTI_POLYGON) { - const coordinates = (feature.geometry as MultiPolygon).coordinates; - let largestPolygon = coordinates[0]; - let largestArea = turfArea(polygon(largestPolygon)); - for (let j = 1; j < coordinates.length; j++) { - const nextPolygon = coordinates[j]; - const nextArea = turfArea(polygon(nextPolygon)); - if (nextArea > largestArea) { - largestPolygon = nextPolygon; - largestArea = nextArea; - } + } + centroidGeometry = getLineCentroid(lineString(longestLine) as Feature); + } else if (feature.geometry.type === GEO_JSON_TYPE.POLYGON) { + centroidGeometry = turfCenterOfMass(feature).geometry; + } else if (feature.geometry.type === GEO_JSON_TYPE.MULTI_POLYGON) { + const coordinates = (feature.geometry as MultiPolygon).coordinates; + let largestPolygon = coordinates[0]; + let largestArea = turfArea(polygon(largestPolygon)); + for (let j = 1; j < coordinates.length; j++) { + const nextPolygon = coordinates[j]; + const nextArea = turfArea(polygon(nextPolygon)); + if (nextArea > largestArea) { + largestPolygon = nextPolygon; + largestArea = nextArea; } - centroidGeometry = turfCenterOfMass(polygon(largestPolygon)).geometry; - } else if (feature.geometry.type === GEO_JSON_TYPE.GEOMETRY_COLLECTION) { - throw new Error('Should not have features with geometrycollection'); } + centroidGeometry = turfCenterOfMass(polygon(largestPolygon)).geometry; + } else if ( + feature.geometry.type === GEO_JSON_TYPE.GEOMETRY_COLLECTION && + (feature.geometry as GeometryCollection).geometries.length + ) { + const firstGeometry = (feature.geometry as GeometryCollection).geometries[0]; + if (firstGeometry.type === GEO_JSON_TYPE.POINT) { + centroidGeometry = firstGeometry; + } else if (firstGeometry.type === GEO_JSON_TYPE.MULTI_POINT) { + centroidGeometry = { + type: 'Point', + coordinates: (firstGeometry as MultiPoint).coordinates[0], + }; + } else { + return getCentroid({ + ...feature, + geometry: firstGeometry, + }); + } + } - if (centroidGeometry) { - centroidFeatures.push({ + return centroidGeometry + ? ({ type: 'Feature', id: feature.id, properties: { @@ -79,10 +106,8 @@ export function getCentroidFeatures(featureCollection: FeatureCollection): Featu [KBN_IS_CENTROID_FEATURE]: true, }, geometry: centroidGeometry, - } as Feature); - } - } - return centroidFeatures; + } as Feature) + : null; } function getLineCentroid(feature: Feature): Geometry {