Skip to content

Commit 40cf8d3

Browse files
authored
feat: trim footprints to 2D coordinates (#30)
* feat: trim footprints to 2D coordinates * chore: update fixpatchMetadataPayload --------- Co-authored-by: Asaf Masa <>
1 parent 54980d1 commit 40cf8d3

File tree

8 files changed

+168
-5
lines changed

8 files changed

+168
-5
lines changed

src/metadata/models/metadataManager.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { AppError } from '../../common/appError';
1010
import { CatalogCall } from '../../externalServices/catalog/catalogCall';
1111
import { LogContext, UpdatePayload, UpdateStatusPayload } from '../../common/interfaces';
1212
import { Record3D } from '../../externalServices/catalog/interfaces';
13+
import { convertPolygonTo2DPolygon, convertStringToGeojson } from '../../model/models/utilities';
1314

1415
@injectable()
1516
export class MetadataManager {
@@ -70,6 +71,10 @@ export class MetadataManager {
7071
throw new AppError('error', StatusCodes.INTERNAL_SERVER_ERROR, String(err), true);
7172
}
7273
try {
74+
if (payload.footprint != undefined) {
75+
const footprint3DOr2D = convertStringToGeojson(JSON.stringify(payload.footprint));
76+
payload.footprint = convertPolygonTo2DPolygon(footprint3DOr2D);
77+
}
7378
const response = await this.catalog.patchMetadata(identifier, payload);
7479
return response;
7580
} catch (err) {

src/model/models/modelManager.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ import { FILE_ENCODING, SERVICES } from '../../common/constants';
1313
import { FailedReason, ValidationManager } from '../../validator/validationManager';
1414
import { AppError } from '../../common/appError';
1515
import { IConfig, IngestionPayload, IngestionValidatePayload, LogContext, ValidationResponse } from '../../common/interfaces';
16-
import { convertStringToGeojson, changeBasePathToPVPath, replaceBackQuotesWithQuotes, removePvPathFromModelPath } from './utilities';
16+
import {
17+
convertStringToGeojson,
18+
changeBasePathToPVPath,
19+
replaceBackQuotesWithQuotes,
20+
removePvPathFromModelPath,
21+
convertPolygonTo2DPolygon,
22+
} from './utilities';
1723

1824
export const ERROR_STORE_TRIGGER_ERROR: string = 'store-trigger service is not available';
1925

@@ -141,8 +147,8 @@ export class ModelManager {
141147
spanActive?.setAttributes({
142148
[THREE_D_CONVENTIONS.three_d.catalogManager.catalogId]: modelId,
143149
});
144-
145150
const adjustedModelPath = this.getAdjustedModelPath(payload.modelPath);
151+
payload.metadata.footprint = convertPolygonTo2DPolygon(payload.metadata.footprint);
146152
const request: StoreTriggerPayload = {
147153
modelId: modelId,
148154
pathToTileset: removePvPathFromModelPath(adjustedModelPath),

src/model/models/utilities.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { sep, relative } from 'node:path';
2-
import { Polygon } from 'geojson';
2+
import { Polygon, Position } from 'geojson';
33
import config from 'config';
44

55
export const changeBasePathToPVPath = (modelPath: string): string => {
@@ -22,3 +22,11 @@ export const replaceBackQuotesWithQuotes = (path: string): string => {
2222
export const convertStringToGeojson = (geojson: string): Polygon => {
2323
return JSON.parse(geojson) as Polygon;
2424
};
25+
26+
export const convertPolygonTo2DPolygon = (polygon: Polygon): Polygon => {
27+
polygon.coordinates = polygon.coordinates.map((polygonInPolygon: Position[]) => {
28+
/* eslint-disable-next-line @typescript-eslint/no-magic-numbers */
29+
return polygonInPolygon.map((coordinate: Position) => coordinate.slice(0, 2));
30+
});
31+
return polygon;
32+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"type": "Polygon",
3+
"coordinates": [
4+
[
5+
[35.2670012825, 32.5856881598, 5],
6+
[35.2670012825, 32.6300363309, 9],
7+
[35.3105702702, 32.6300363309, 9],
8+
[35.3105702702, 32.5856881598, 9],
9+
[35.2670012825, 32.5856881598, 5]
10+
]
11+
]
12+
}

tests/helpers/helpers.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,9 @@ export const createTilesetFileName = (): string => {
9999
return 'tileset.json';
100100
};
101101

102-
export const createFootprint = (modelName = 'Sphere'): Polygon => {
103-
const jsonString: string = readFileSync(`${pvPath}/${modelName}/footprint.json`, 'utf8');
102+
export const createFootprint = (modelName = 'Sphere', is3D = false): Polygon => {
103+
const fileName = !is3D ? 'footprint' : 'footprint3D';
104+
const jsonString: string = readFileSync(`${pvPath}/${modelName}/${fileName}.json`, 'utf8');
104105
return JSON.parse(jsonString) as Polygon;
105106
};
106107

tests/integration/metadata/metadataController.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ import {
1212
createWrongFootprintSchema,
1313
createRecord,
1414
createWrongFootprintMixed2D3D,
15+
createFootprint,
1516
} from '../../helpers/helpers';
1617
import { getApp } from '../../../src/app';
1718
import { SERVICES } from '../../../src/common/constants';
1819
import { S3Helper } from '../../helpers/s3Helper';
1920
import { S3Config } from '../../../src/common/interfaces';
2021
import { extractLink } from '../../../src/validator/extractPathFromLink';
22+
import { CatalogCall } from '../../../src/externalServices/catalog/catalogCall';
2123
import { MetadataRequestSender } from './helpers/requestSender';
2224

2325
describe('MetadataController', function () {
@@ -66,6 +68,53 @@ describe('MetadataController', function () {
6668
expect(response.status).toBe(StatusCodes.OK);
6769
expect(response).toSatisfyApiSpec();
6870
});
71+
72+
it(`Should return 200 status code and metadata if payload is valid and footprint is 3D and pass footprint 2D to catalog`, async function () {
73+
const identifier = faker.string.uuid();
74+
const payload = createUpdatePayload('Sphere');
75+
payload.footprint = createFootprint('Sphere', true);
76+
const expectedFootprint = createFootprint('Sphere', false);
77+
const expected = createRecord();
78+
const record = createRecord();
79+
const linkUrl = extractLink(record.links);
80+
await s3Helper.createFile(linkUrl, true);
81+
mockAxios.get.mockResolvedValueOnce({ status: StatusCodes.OK, data: record });
82+
mockAxios.get.mockResolvedValueOnce({ data: [{ value: payload.classification }] as ILookupOption[] });
83+
mockAxios.patch.mockResolvedValueOnce({ status: StatusCodes.OK, data: expected });
84+
85+
const catalogCallPatchPayloadSpy = jest.spyOn(CatalogCall.prototype, 'patchMetadata');
86+
const patchMetadataPayload = {
87+
productName: payload.productName,
88+
sourceDateStart: payload.sourceDateStart?.toISOString(),
89+
sourceDateEnd: payload.sourceDateEnd?.toISOString(),
90+
footprint: expectedFootprint,
91+
description: payload.description,
92+
creationDate: payload.creationDate?.toISOString(),
93+
minResolutionMeter: payload.minResolutionMeter,
94+
maxResolutionMeter: payload.maxResolutionMeter,
95+
maxAccuracyCE90: payload.maxAccuracyCE90,
96+
absoluteAccuracyLE90: payload.absoluteAccuracyLE90,
97+
accuracySE90: payload.accuracySE90,
98+
relativeAccuracySE90: payload.relativeAccuracySE90,
99+
visualAccuracy: payload.visualAccuracy,
100+
heightRangeFrom: payload.heightRangeFrom,
101+
heightRangeTo: payload.heightRangeTo,
102+
classification: payload.classification,
103+
producerName: payload.producerName,
104+
maxFlightAlt: payload.maxFlightAlt,
105+
minFlightAlt: payload.minFlightAlt,
106+
geographicArea: payload.geographicArea,
107+
};
108+
109+
const response = await requestSender.updateMetadata(identifier, payload);
110+
111+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
112+
expect(catalogCallPatchPayloadSpy).toHaveBeenCalledTimes(1);
113+
expect(catalogCallPatchPayloadSpy).toHaveBeenCalledWith(expect.any(String), patchMetadataPayload);
114+
115+
expect(response.status).toBe(StatusCodes.OK);
116+
expect(response).toSatisfyApiSpec();
117+
});
69118
});
70119

71120
describe('Bad Path 😡', function () {

tests/integration/model/modelController.spec.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
getBasePath,
2121
getModelNameByPath,
2222
createWrongFootprintMixed2D3D,
23+
createFootprint,
2324
} from '../../helpers/helpers';
2425
import { getApp } from '../../../src/app';
2526
import { SERVICES } from '../../../src/common/constants';
@@ -127,6 +128,75 @@ describe('ModelController', function () {
127128
expect(response.status).toBe(StatusCodes.CREATED);
128129
expect(response).toSatisfyApiSpec();
129130
});
131+
132+
it('should return 201 status code if footprint has 3D coordinates and pass footprint 2D to storeTrigger', async function () {
133+
const payload = createIngestionPayload('Sphere');
134+
payload.metadata.minResolutionMeter = 11;
135+
payload.metadata.producerName = 'aa';
136+
payload.metadata.footprint = createFootprint('Sphere', true);
137+
const expectedFootprint = createFootprint('Sphere', false);
138+
139+
const storeTriggerResult: StoreTriggerResponse = {
140+
jobId: faker.string.uuid(),
141+
status: OperationStatus.IN_PROGRESS,
142+
};
143+
mockAxios.get.mockResolvedValueOnce({ status: StatusCodes.OK });
144+
mockAxios.get.mockResolvedValueOnce({ data: [{ value: payload.metadata.classification }] as ILookupOption[] });
145+
mockAxios.post.mockResolvedValueOnce({ data: storeTriggerResult });
146+
147+
const storeTriggerCallPostPayloadSpy = jest.spyOn(StoreTriggerCall.prototype, 'postPayload');
148+
149+
const response = await requestSender.createModel(payload);
150+
151+
expect(storeTriggerCallPostPayloadSpy).toHaveBeenCalledTimes(1);
152+
153+
const subsetPostPayloadMetadata = {
154+
absoluteAccuracyLE90: payload.metadata.absoluteAccuracyLE90,
155+
accuracySE90: payload.metadata.accuracySE90,
156+
classification: payload.metadata.classification,
157+
creationDate: payload.metadata.creationDate?.toISOString(),
158+
description: payload.metadata.description,
159+
footprint: expectedFootprint,
160+
geographicArea: payload.metadata.geographicArea,
161+
heightRangeFrom: payload.metadata.heightRangeFrom,
162+
heightRangeTo: payload.metadata.heightRangeTo,
163+
maxAccuracyCE90: payload.metadata.maxAccuracyCE90,
164+
maxFlightAlt: payload.metadata.maxFlightAlt,
165+
maxResolutionMeter: payload.metadata.maxResolutionMeter,
166+
minFlightAlt: payload.metadata.minFlightAlt,
167+
minResolutionMeter: payload.metadata.minResolutionMeter,
168+
producerName: payload.metadata.producerName,
169+
productId: payload.metadata.productId,
170+
productName: payload.metadata.productName,
171+
productSource: '\\\\tmp\\tilesets\\models\\Sphere',
172+
productStatus: 'UNPUBLISHED',
173+
productType: '3DPhotoRealistic',
174+
productionSystem: payload.metadata.productionSystem,
175+
productionSystemVer: payload.metadata.productionSystemVer,
176+
region: payload.metadata.region,
177+
relativeAccuracySE90: payload.metadata.relativeAccuracySE90,
178+
sensors: payload.metadata.sensors,
179+
sourceDateEnd: `${payload.metadata.sourceDateEnd?.toISOString()}`,
180+
sourceDateStart: `${payload.metadata.sourceDateStart?.toISOString()}`,
181+
srsId: payload.metadata.srsId,
182+
srsName: payload.metadata.srsName,
183+
type: 'RECORD_3D',
184+
visualAccuracy: payload.metadata.visualAccuracy,
185+
};
186+
187+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
188+
expect(storeTriggerCallPostPayloadSpy).toHaveBeenCalledWith(
189+
expect.objectContaining({
190+
modelId: expect.any(String),
191+
pathToTileset: 'Sphere',
192+
tilesetFilename: 'tileset.json',
193+
metadata: subsetPostPayloadMetadata,
194+
})
195+
);
196+
197+
expect(response.status).toBe(StatusCodes.CREATED);
198+
expect(response).toSatisfyApiSpec();
199+
});
130200
});
131201

132202
describe('Sad Path 😥, createModel', function () {

tests/unit/model/models/utilities.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {
55
changeBasePathToPVPath,
66
removePvPathFromModelPath,
77
replaceBackQuotesWithQuotes,
8+
convertPolygonTo2DPolygon,
89
} from '../../../../src/model/models/utilities';
10+
import { createFootprint } from '../../../helpers/helpers';
911

1012
describe('utilities tests', () => {
1113
describe('removeFootprintSpaces tests', () => {
@@ -19,6 +21,16 @@ describe('utilities tests', () => {
1921
});
2022
});
2123

24+
describe('convertPolygonTo2DPolygon tests', () => {
25+
it('Should return footprint 2D from 3D', () => {
26+
const footprint3D = createFootprint('Sphere', true);
27+
const expectedFootprint2D = createFootprint('Sphere', false);
28+
29+
const result = convertPolygonTo2DPolygon(footprint3D);
30+
expect(result).toStrictEqual(expectedFootprint2D);
31+
});
32+
});
33+
2234
describe('changeBasePathToPVPath tests', () => {
2335
it('Should return mounted path', () => {
2436
const basePath: string = config.get<string>('paths.basePath') + '\\model\\path';

0 commit comments

Comments
 (0)