Skip to content

Commit 1e29d02

Browse files
mtrezzaflovilmart
authored andcommitted
add support for geoWithin.centerSphere queries via withJSON (#4825)
* add support for geoWithin.centerSphere queries via withJSON * added test for passing array of lat, lng instead of Parse.GeoPoint * added postgres support * added more tests * improved tests and validation * added more tests
1 parent 260c466 commit 1e29d02

File tree

4 files changed

+197
-34
lines changed

4 files changed

+197
-34
lines changed

spec/ParseQuery.spec.js

+102
Original file line numberDiff line numberDiff line change
@@ -3985,4 +3985,106 @@ describe('Parse.Query testing', () => {
39853985
})
39863986
});
39873987

3988+
it('withJSON supports geoWithin.centerSphere', (done) => {
3989+
const inbound = new Parse.GeoPoint(1.5, 1.5);
3990+
const onbound = new Parse.GeoPoint(10, 10);
3991+
const outbound = new Parse.GeoPoint(20, 20);
3992+
const obj1 = new Parse.Object('TestObject', {location: inbound});
3993+
const obj2 = new Parse.Object('TestObject', {location: onbound});
3994+
const obj3 = new Parse.Object('TestObject', {location: outbound});
3995+
const center = new Parse.GeoPoint(0, 0);
3996+
const distanceInKilometers = 1569 + 1; // 1569km is the approximate distance between {0, 0} and {10, 10}.
3997+
Parse.Object.saveAll([obj1, obj2, obj3]).then(() => {
3998+
const q = new Parse.Query(TestObject);
3999+
const jsonQ = q.toJSON();
4000+
jsonQ.where.location = {
4001+
'$geoWithin': {
4002+
'$centerSphere': [
4003+
center,
4004+
distanceInKilometers / 6371.0
4005+
]
4006+
}
4007+
};
4008+
q.withJSON(jsonQ);
4009+
return q.find();
4010+
}).then(results => {
4011+
equal(results.length, 2);
4012+
const q = new Parse.Query(TestObject);
4013+
const jsonQ = q.toJSON();
4014+
jsonQ.where.location = {
4015+
'$geoWithin': {
4016+
'$centerSphere': [
4017+
[0, 0],
4018+
distanceInKilometers / 6371.0
4019+
]
4020+
}
4021+
};
4022+
q.withJSON(jsonQ);
4023+
return q.find();
4024+
}).then(results => {
4025+
equal(results.length, 2);
4026+
done();
4027+
}).catch(error => {
4028+
fail(error);
4029+
done();
4030+
});
4031+
});
4032+
4033+
it('withJSON with geoWithin.centerSphere fails without parameters', (done) => {
4034+
const q = new Parse.Query(TestObject);
4035+
const jsonQ = q.toJSON();
4036+
jsonQ.where.location = {
4037+
'$geoWithin': {
4038+
'$centerSphere': [
4039+
]
4040+
}
4041+
};
4042+
q.withJSON(jsonQ);
4043+
q.find(expectError(Parse.Error.INVALID_JSON, done));
4044+
});
4045+
4046+
it('withJSON with geoWithin.centerSphere fails with invalid distance', (done) => {
4047+
const q = new Parse.Query(TestObject);
4048+
const jsonQ = q.toJSON();
4049+
jsonQ.where.location = {
4050+
'$geoWithin': {
4051+
'$centerSphere': [
4052+
[0, 0],
4053+
'invalid_distance'
4054+
]
4055+
}
4056+
};
4057+
q.withJSON(jsonQ);
4058+
q.find(expectError(Parse.Error.INVALID_JSON, done));
4059+
});
4060+
4061+
it('withJSON with geoWithin.centerSphere fails with invalid coordinate', (done) => {
4062+
const q = new Parse.Query(TestObject);
4063+
const jsonQ = q.toJSON();
4064+
jsonQ.where.location = {
4065+
'$geoWithin': {
4066+
'$centerSphere': [
4067+
[-190,-190],
4068+
1
4069+
]
4070+
}
4071+
};
4072+
q.withJSON(jsonQ);
4073+
q.find(expectError(undefined, done));
4074+
});
4075+
4076+
it('withJSON with geoWithin.centerSphere fails with invalid geo point', (done) => {
4077+
const q = new Parse.Query(TestObject);
4078+
const jsonQ = q.toJSON();
4079+
jsonQ.where.location = {
4080+
'$geoWithin': {
4081+
'$centerSphere': [
4082+
{'longitude': 0, 'dummytude': 0},
4083+
1
4084+
]
4085+
}
4086+
};
4087+
q.withJSON(jsonQ);
4088+
q.find(expectError(undefined, done));
4089+
});
39884090
});

spec/helper.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -294,11 +294,13 @@ function expectError(errorCode, callback) {
294294
error: function(obj, e) {
295295
// Some methods provide 2 parameters.
296296
e = e || obj;
297-
if (!e) {
298-
fail('expected a specific error but got a blank error');
299-
return;
297+
if (errorCode !== undefined) {
298+
if (!e) {
299+
fail('expected a specific error but got a blank error');
300+
return;
301+
}
302+
expect(e.code).toEqual(errorCode, e.message);
300303
}
301-
expect(e.code).toEqual(errorCode, e.message);
302304
if (callback) {
303305
callback(e);
304306
}

src/Adapters/Storage/Mongo/MongoTransform.js

+56-30
Original file line numberDiff line numberDiff line change
@@ -905,44 +905,70 @@ function transformConstraint(constraint, field) {
905905

906906
case '$geoWithin': {
907907
const polygon = constraint[key]['$polygon'];
908-
let points;
909-
if (typeof polygon === 'object' && polygon.__type === 'Polygon') {
910-
if (!polygon.coordinates || polygon.coordinates.length < 3) {
908+
const centerSphere = constraint[key]['$centerSphere'];
909+
if (polygon !== undefined) {
910+
let points;
911+
if (typeof polygon === 'object' && polygon.__type === 'Polygon') {
912+
if (!polygon.coordinates || polygon.coordinates.length < 3) {
913+
throw new Parse.Error(
914+
Parse.Error.INVALID_JSON,
915+
'bad $geoWithin value; Polygon.coordinates should contain at least 3 lon/lat pairs'
916+
);
917+
}
918+
points = polygon.coordinates;
919+
} else if (polygon instanceof Array) {
920+
if (polygon.length < 3) {
921+
throw new Parse.Error(
922+
Parse.Error.INVALID_JSON,
923+
'bad $geoWithin value; $polygon should contain at least 3 GeoPoints'
924+
);
925+
}
926+
points = polygon;
927+
} else {
911928
throw new Parse.Error(
912929
Parse.Error.INVALID_JSON,
913-
'bad $geoWithin value; Polygon.coordinates should contain at least 3 lon/lat pairs'
930+
'bad $geoWithin value; $polygon should be Polygon object or Array of Parse.GeoPoint\'s'
914931
);
915932
}
916-
points = polygon.coordinates;
917-
} else if (polygon instanceof Array) {
918-
if (polygon.length < 3) {
919-
throw new Parse.Error(
920-
Parse.Error.INVALID_JSON,
921-
'bad $geoWithin value; $polygon should contain at least 3 GeoPoints'
922-
);
933+
points = points.map((point) => {
934+
if (point instanceof Array && point.length === 2) {
935+
Parse.GeoPoint._validate(point[1], point[0]);
936+
return point;
937+
}
938+
if (!GeoPointCoder.isValidJSON(point)) {
939+
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value');
940+
} else {
941+
Parse.GeoPoint._validate(point.latitude, point.longitude);
942+
}
943+
return [point.longitude, point.latitude];
944+
});
945+
answer[key] = {
946+
'$polygon': points
947+
};
948+
} else if (centerSphere !== undefined) {
949+
if (!(centerSphere instanceof Array) || centerSphere.length < 2) {
950+
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere should be an array of Parse.GeoPoint and distance');
923951
}
924-
points = polygon;
925-
} else {
926-
throw new Parse.Error(
927-
Parse.Error.INVALID_JSON,
928-
'bad $geoWithin value; $polygon should be Polygon object or Array of Parse.GeoPoint\'s'
929-
);
930-
}
931-
points = points.map((point) => {
952+
// Get point, convert to geo point if necessary and validate
953+
let point = centerSphere[0];
932954
if (point instanceof Array && point.length === 2) {
933-
Parse.GeoPoint._validate(point[1], point[0]);
934-
return point;
955+
point = new Parse.GeoPoint(point[1], point[0]);
956+
} else if (!GeoPointCoder.isValidJSON(point)) {
957+
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere geo point invalid');
935958
}
936-
if (!GeoPointCoder.isValidJSON(point)) {
937-
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value');
938-
} else {
939-
Parse.GeoPoint._validate(point.latitude, point.longitude);
959+
Parse.GeoPoint._validate(point.latitude, point.longitude);
960+
// Get distance and validate
961+
const distance = centerSphere[1];
962+
if(isNaN(distance) || distance < 0) {
963+
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere distance invalid');
940964
}
941-
return [point.longitude, point.latitude];
942-
});
943-
answer[key] = {
944-
'$polygon': points
945-
};
965+
answer[key] = {
966+
'$centerSphere': [
967+
[point.longitude, point.latitude],
968+
distance
969+
]
970+
};
971+
}
946972
break;
947973
}
948974
case '$geoIntersects': {

src/Adapters/Storage/Postgres/PostgresStorageAdapter.js

+33
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,30 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => {
535535
index += 2;
536536
}
537537

538+
if (fieldValue.$geoWithin && fieldValue.$geoWithin.$centerSphere) {
539+
const centerSphere = fieldValue.$geoWithin.$centerSphere;
540+
if (!(centerSphere instanceof Array) || centerSphere.length < 2) {
541+
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere should be an array of Parse.GeoPoint and distance');
542+
}
543+
// Get point, convert to geo point if necessary and validate
544+
let point = centerSphere[0];
545+
if (point instanceof Array && point.length === 2) {
546+
point = new Parse.GeoPoint(point[1], point[0]);
547+
} else if (!GeoPointCoder.isValidJSON(point)) {
548+
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere geo point invalid');
549+
}
550+
Parse.GeoPoint._validate(point.latitude, point.longitude);
551+
// Get distance and validate
552+
const distance = centerSphere[1];
553+
if(isNaN(distance) || distance < 0) {
554+
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere distance invalid');
555+
}
556+
const distanceInKM = distance * 6371 * 1000;
557+
patterns.push(`ST_distance_sphere($${index}:name::geometry, POINT($${index + 1}, $${index + 2})::geometry) <= $${index + 3}`);
558+
values.push(fieldName, point.longitude, point.latitude, distanceInKM);
559+
index += 4;
560+
}
561+
538562
if (fieldValue.$geoWithin && fieldValue.$geoWithin.$polygon) {
539563
const polygon = fieldValue.$geoWithin.$polygon;
540564
let points;
@@ -1986,4 +2010,13 @@ function literalizeRegexPart(s: string) {
19862010
);
19872011
}
19882012

2013+
var GeoPointCoder = {
2014+
isValidJSON(value) {
2015+
return (typeof value === 'object' &&
2016+
value !== null &&
2017+
value.__type === 'GeoPoint'
2018+
);
2019+
}
2020+
};
2021+
19892022
export default PostgresStorageAdapter;

0 commit comments

Comments
 (0)