Skip to content

Commit

Permalink
Upload GeoJSON - Added CRS check (#2064)
Browse files Browse the repository at this point in the history
  • Loading branch information
rfontanarosa authored Oct 16, 2024
1 parent a53f43b commit 2ee9157
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 31 deletions.
116 changes: 86 additions & 30 deletions functions/src/import-geojson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import JSONStream from 'jsonstream-ts';
import {canImport} from './common/auth';
import {DecodedIdToken} from 'firebase-admin/auth';
import {GroundProtos} from '@ground/proto';
import {toDocumentData, toGeometryPb} from '@ground/lib';
import {toDocumentData, toGeometryPb, isGeometryValid} from '@ground/lib';
import {Feature, GeoJsonProperties} from 'geojson';
import {ErrorHandler} from './handlers';

Expand Down Expand Up @@ -81,38 +81,13 @@ export function importGeoJsonCallback(
);
// Pipe file through JSON parser lib, inserting each row in the db as it is
// received.
let geoJsonType: any = null;
file.pipe(JSONStream.parse('type', undefined)).on('data', (data: any) => {
geoJsonType = data;
});
file.pipe(JSONStream.parse('type', undefined)).on('data', onGeoJsonType);

file.pipe(JSONStream.parse('crs', undefined)).on('data', onGeoJsonCrs);

file
.pipe(JSONStream.parse(['features', true], undefined))
.on('data', (geoJsonLoi: any) => {
try {
if (geoJsonType !== 'FeatureCollection') {
return error(
HttpStatus.BAD_REQUEST,
`Expected 'FeatureCollection', got '${geoJsonType}'`
);
}
if (geoJsonLoi.type !== 'Feature') {
console.debug(`Skipping LOI with invalid type ${geoJsonLoi.type}`);
return;
}
try {
const loi = toDocumentData(
toLoiPb(geoJsonLoi as Feature, jobId, ownerId)
);
inserts.push(db.insertLocationOfInterest(surveyId, loi));
} catch (loiErr) {
console.debug('Skipping LOI', loiErr);
}
} catch (err) {
req.unpipe(busboy);
return error(HttpStatus.BAD_REQUEST, (err as Error).message);
}
});
.on('data', (data: any) => onGeoJsonFeature(data, surveyId, jobId));
});

// Handle non-file fields in the task. survey and job must appear
Expand Down Expand Up @@ -145,6 +120,87 @@ export function importGeoJsonCallback(
// Use this for Cloud Functions rather than `req.pipe(busboy)`:
// https://github.com/mscdex/busboy/issues/229#issuecomment-648303108
busboy.end(req.rawBody);

/**
* This function is called by Busboy during file parsing to ensure that the GeoJSON
* data being processed is valid. It checks for the presence of the required 'type'
* property and verifies that its value is 'FeatureCollection'.
*/
function onGeoJsonType(geoJsonType: string | undefined) {
if (!geoJsonType) {
return error(
HttpStatus.BAD_REQUEST,
'Invalid GeoJSON: Missing "type" property'
);
}
if (geoJsonType !== 'FeatureCollection') {
return error(
HttpStatus.BAD_REQUEST,
`Unsupported GeoJSON Type: Expected 'FeatureCollection', got '${geoJsonType}'`
);
}
}

/**
* This function is called by Busboy during file parsing to ensure that the GeoJSON
* data uses the 'CRS84' coordinate reference system.
*/
function onGeoJsonCrs(
geoJsonCrs: {type: string; properties: {name?: string}} | undefined
) {
let crs = 'CRS84';
if (geoJsonCrs) {
const {type, properties} = geoJsonCrs;
switch (type) {
case 'name':
crs = properties?.name ?? 'CRS84';
break;
}
}
if (!crs.endsWith('CRS84')) {
return error(
HttpStatus.BAD_REQUEST,
`Unsupported GeoJSON CRS: Expected 'CRS84', got '${JSON.stringify(
geoJsonCrs
)}'`
);
}
}

/**
* This function is called by Busboy during file parsing to validate and process
* GeoJSON Feature objects within the file. It checks the feature type, geometry
* validity, and converts the feature to a document data format for insertion.
*/
function onGeoJsonFeature(
geoJsonFeature: any,
surveyId: string,
jobId: string
) {
try {
if (geoJsonFeature.type !== 'Feature') {
console.debug(`Skipping LOI with invalid type ${geoJsonFeature.type}`);
return;
}
if (!isGeometryValid(geoJsonFeature.geometry)) {
return error(
HttpStatus.BAD_REQUEST,
'Unsupported Feature coordinates format'
);
}
try {
const loi = toDocumentData(
toLoiPb(geoJsonFeature as Feature, jobId, ownerId)
);
inserts.push(db.insertLocationOfInterest(surveyId, loi));
} catch (loiErr) {
console.debug('Skipping LOI', loiErr);
}
} catch (err) {
req.unpipe(busboy);
return error(HttpStatus.BAD_REQUEST, (err as Error).message);
}
}
}

/**
Expand Down
27 changes: 27 additions & 0 deletions lib/src/geo-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,30 @@ function toMultiPolygonGeometryPb(positions: Position[][][]): Pb.Geometry {
const multiPolygon = new Pb.MultiPolygon({polygons});
return new Pb.Geometry({multiPolygon});
}

export function isGeometryValid(geometry: Geometry): boolean {
switch (geometry.type) {
case 'Point':
return isPositionValid(geometry.coordinates);
case 'Polygon':
for (const ring of geometry.coordinates) {
for (const position of ring) {
if (!isPositionValid(position)) return false;
}
}
break;
case 'MultiPolygon':
for (const polygon of geometry.coordinates) {
for (const ring of polygon) {
for (const position of ring) {
if (!isPositionValid(position)) return false;
}
}
}
}
return true;
}

function isPositionValid([lng, lat]: Position) {
return lng >= -180 && lng <= 180 && lat >= -90 && lat <= 90;
}
2 changes: 1 addition & 1 deletion lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@
export {toDocumentData} from './proto-to-firestore';
export {toMessage} from './firestore-to-proto';
export {deleteEmpty, isEmpty} from './obj-util';
export {toGeoJsonGeometry, toGeometryPb} from './geo-json';
export {toGeoJsonGeometry, toGeometryPb, isGeometryValid} from './geo-json';
export {registry} from './message-registry';

0 comments on commit 2ee9157

Please sign in to comment.