Skip to content

Commit 293e2e1

Browse files
dplewisflovilmart
authored andcommitted
Add Type Polygon to Schema and PolygonContains to query (#455)
* Add Type Polygon to Schema and PolygonContains to query * Corrects Guide Link in README.md (#457) Just a small fix of `https` to `http` for the JS guide link, which is only available over `http`. * Addresses flaky test (#458) * Update ParseGeoPointTest.js * Limit request attempts to 1 * explore * Marks flaky test as pending * Add methods `addAll`, `addAllUnique` and `removeAll` (#459) * Add methods `addAll`, `addAllUnique` and `removeAll` * Add methods `addAll`, `addAllUnique` and `removeAll` * Add test for `addAll`, `addAllUnique` and `removeAll` * Revert test case * containsPoint uses bounded box first then ray casting * update parse-server to 2.6.0 * Add Type Polygon to Schema and PolygonContains to query * containsPoint uses bounded box first then ray casting * update parse-server to 2.6.0
1 parent af28f27 commit 293e2e1

File tree

8 files changed

+330
-2
lines changed

8 files changed

+330
-2
lines changed

integration/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"dependencies": {
44
"express": "^4.13.4",
55
"mocha": "^2.4.5",
6-
"parse-server": "^2.4.2"
6+
"parse-server": "^2.6.0"
77
},
88
"scripts": {
99
"test": "mocha --reporter dot -t 5000"

integration/test/ParseGeoPointTest.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ describe('Geo Point', () => {
291291
const query = new Parse.Query(TestPoint);
292292
query.withinPolygon('location', []);
293293
query.find().then(done.fail, (err) => {
294-
assert.equal(err.code, Parse.Error.INTERNAL_SERVER_ERROR);
294+
assert.equal(err.code, Parse.Error.INVALID_JSON);
295295
done();
296296
})
297297
.fail(done.fail);

integration/test/ParsePolygonTest.js

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
const assert = require('assert');
2+
const clear = require('./clear');
3+
const Parse = require('../../node');
4+
5+
const TestObject = Parse.Object.extend('TestObject');
6+
7+
describe('Polygon', () => {
8+
before(() => {
9+
Parse.initialize('integration');
10+
Parse.CoreManager.set('SERVER_URL', 'http://localhost:1337/parse');
11+
Parse.Storage._clear();
12+
});
13+
14+
beforeEach((done) => {
15+
clear().then(() => {
16+
done();
17+
});
18+
});
19+
20+
it('can save polygon with points', (done) => {
21+
const openPoints = [[0,0], [0,1], [1,1], [1,0]];
22+
const closedPoints = [[0,0], [0,1], [1,1], [1,0], [0,0]];
23+
const polygon = new Parse.Polygon(openPoints);
24+
const obj = new TestObject({ polygon });
25+
obj.save().then(() => {
26+
const query = new Parse.Query(TestObject);
27+
query.equalTo('polygon', polygon);
28+
return query.find();
29+
}).then((results) => {
30+
assert.equal(results.length, 1);
31+
assert.deepEqual(results[0].get('polygon').coordinates, closedPoints);
32+
const closedPolygon = new Parse.Polygon(closedPoints);
33+
const query = new Parse.Query(TestObject);
34+
query.equalTo('polygon', closedPolygon);
35+
return query.find();
36+
}).then((results) => {
37+
assert.equal(results.length, 1);
38+
assert.deepEqual(results[0].get('polygon').coordinates, closedPoints);
39+
done();
40+
}, done.fail);
41+
});
42+
43+
it('can save polygon with GeoPoints', (done) => {
44+
const p1 = new Parse.GeoPoint(0, 0);
45+
const p2 = new Parse.GeoPoint(0, 1);
46+
const p3 = new Parse.GeoPoint(1, 1);
47+
const p4 = new Parse.GeoPoint(1, 0);
48+
const p5 = new Parse.GeoPoint(0, 0);
49+
const closedPoints = [[0,0], [0,1], [1,1], [1,0], [0,0]];
50+
const polygon = new Parse.Polygon([p1, p2, p3, p4, p5]);
51+
const obj = new TestObject({ polygon });
52+
obj.save().then(() => {
53+
const query = new Parse.Query(TestObject);
54+
query.equalTo('polygon', polygon);
55+
return query.find();
56+
}).then((results) => {
57+
assert.equal(results.length, 1);
58+
assert.deepEqual(results[0].get('polygon').coordinates, closedPoints);
59+
const closedPolygon = new Parse.Polygon(closedPoints);
60+
const query = new Parse.Query(TestObject);
61+
query.equalTo('polygon', closedPolygon);
62+
return query.find();
63+
}).then((results) => {
64+
assert.deepEqual(results[0].get('polygon').coordinates, closedPoints);
65+
done();
66+
}, done.fail);
67+
});
68+
69+
it('fail save with 3 point minumum', (done) => {
70+
try {
71+
const polygon = new Parse.Polygon([[0, 0]]);
72+
} catch (e) {
73+
done();
74+
}
75+
});
76+
77+
it('fail save with non array', (done) => {
78+
try {
79+
const polygon = new Parse.Polygon(123);
80+
} catch (e) {
81+
done();
82+
}
83+
});
84+
85+
it('fail save with invalid array', (done) => {
86+
try {
87+
const polygon = new Parse.Polygon([['str1'], ['str2'], ['str3']]);
88+
} catch (e) {
89+
done();
90+
}
91+
});
92+
93+
it('containsPoint', (done) => {
94+
const points = [[0,0], [0,1], [1,1], [1,0]];
95+
const inside = new Parse.GeoPoint(0.5, 0.5);
96+
const outside = new Parse.GeoPoint(10, 10);
97+
const polygon = new Parse.Polygon(points);
98+
99+
assert.equal(polygon.containsPoint(inside), true);
100+
assert.equal(polygon.containsPoint(outside), false);
101+
done();
102+
});
103+
104+
it('equality', (done) => {
105+
const points = [[0,0], [0,1], [1,1], [1,0]];
106+
const diff = [[0,0], [0,2], [2,2], [2,0]];
107+
108+
const polygonA = new Parse.Polygon(points);
109+
const polygonB = new Parse.Polygon(points);
110+
const polygonC = new Parse.Polygon(diff);
111+
112+
assert.equal(polygonA.equals(polygonA), true);
113+
assert.equal(polygonA.equals(polygonB), true);
114+
assert.equal(polygonB.equals(polygonA), true);
115+
116+
assert.equal(polygonA.equals(true), false);
117+
assert.equal(polygonA.equals(polygonC), false);
118+
119+
done();
120+
});
121+
122+
it('supports polygonContains', (done) => {
123+
const p1 = [[0,0], [0,1], [1,1], [1,0]];
124+
const p2 = [[0,0], [0,2], [2,2], [2,0]];
125+
const p3 = [[10,10], [10,15], [15,15], [15,10], [10,10]];
126+
127+
const polygon1 = new Parse.Polygon(p1);
128+
const polygon2 = new Parse.Polygon(p2);
129+
const polygon3 = new Parse.Polygon(p3);
130+
131+
const obj1 = new TestObject({ polygon: polygon1 });
132+
const obj2 = new TestObject({ polygon: polygon2 });
133+
const obj3 = new TestObject({ polygon: polygon3 });
134+
135+
Parse.Object.saveAll([obj1, obj2, obj3]).then(() => {
136+
const point = new Parse.GeoPoint(0.5, 0.5);
137+
const query = new Parse.Query(TestObject);
138+
query.polygonContains('polygon', point);
139+
return query.find();
140+
}).then((results) => {
141+
assert.equal(results.length, 2);
142+
done();
143+
}, done.fail);
144+
});
145+
146+
it('polygonContains invalid input', (done) => {
147+
const points = [[0,0], [0,1], [1,1], [1,0]];
148+
const polygon = new Parse.Polygon(points);
149+
const obj = new TestObject({ polygon });
150+
obj.save().then(() => {
151+
const query = new Parse.Query(TestObject);
152+
query.polygonContains('polygon', 1234);
153+
return query.find();
154+
}).then(() => {
155+
fail();
156+
}).catch(() => {
157+
done();
158+
});
159+
});
160+
});

src/Parse.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ Parse.Error = require('./ParseError').default;
9999
Parse.FacebookUtils = require('./FacebookUtils').default;
100100
Parse.File = require('./ParseFile').default;
101101
Parse.GeoPoint = require('./ParseGeoPoint').default;
102+
Parse.Polygon = require('./ParsePolygon').default;
102103
Parse.Installation = require('./ParseInstallation').default;
103104
Parse.Object = require('./ParseObject').default;
104105
Parse.Op = {

src/ParsePolygon.js

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* Copyright (c) 2015-present, Parse, LLC.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @flow
10+
*/
11+
12+
import ParseGeoPoint from './ParseGeoPoint';
13+
14+
/**
15+
* Creates a new Polygon with any of the following forms:<br>
16+
* <pre>
17+
* new Polygon([[0,0],[0,1],[1,1],[1,0]])
18+
* new Polygon([GeoPoint, GeoPoint, GeoPoint])
19+
* </pre>
20+
* @class Parse.GeoPoint
21+
* @constructor
22+
*
23+
* <p>Represents a coordinates that may be associated
24+
* with a key in a ParseObject or used as a reference point for geo queries.
25+
* This allows proximity-based queries on the key.</p>
26+
*
27+
* <p>Example:<pre>
28+
* var polygon = new Parse.Polygon([[0,0],[0,1],[1,1],[1,0]]);
29+
* var object = new Parse.Object("PlaceObject");
30+
* object.set("area", polygon);
31+
* object.save();</pre></p>
32+
*/
33+
export default class ParsePolygon {
34+
_coordinates: Array;
35+
36+
constructor(
37+
arg1: Array,
38+
) {
39+
this._coordinates = ParsePolygon._validate(arg1);
40+
}
41+
42+
/**
43+
* Coordinates value for this Polygon.
44+
* Throws an exception if not valid type.
45+
* @property coordinates
46+
* @type Array
47+
*/
48+
get coordinates(): Array {
49+
return this._coordinates;
50+
}
51+
52+
set coordinates(coords: Array) {
53+
this._coordinates = ParsePolygon._validate(coords);
54+
}
55+
56+
/**
57+
* Returns a JSON representation of the GeoPoint, suitable for Parse.
58+
* @method toJSON
59+
* @return {Object}
60+
*/
61+
toJSON(): { __type: string; coordinates: Array;} {
62+
ParsePolygon._validate(this._coordinates);
63+
return {
64+
__type: 'Polygon',
65+
coordinates: this._coordinates,
66+
};
67+
}
68+
69+
equals(other: mixed): boolean {
70+
if (!(other instanceof ParsePolygon) || (this.coordinates.length !== other.coordinates.length)) {
71+
return false;
72+
}
73+
let isEqual = true;
74+
75+
for (let i = 1; i < this._coordinates.length; i += 1) {
76+
if (this._coordinates[i][0] != other.coordinates[i][0] ||
77+
this._coordinates[i][1] != other.coordinates[i][1]) {
78+
isEqual = false;
79+
break;
80+
}
81+
}
82+
return isEqual;
83+
}
84+
85+
containsPoint(point: ParseGeoPoint): boolean {
86+
let minX = this._coordinates[0][0];
87+
let maxX = this._coordinates[0][0];
88+
let minY = this._coordinates[0][1];
89+
let maxY = this._coordinates[0][1];
90+
91+
for (let i = 1; i < this._coordinates.length; i += 1) {
92+
const p = this._coordinates[i];
93+
minX = Math.min(p[0], minX);
94+
maxX = Math.max(p[0], maxX);
95+
minY = Math.min(p[1], minY);
96+
maxY = Math.max(p[1], maxY);
97+
}
98+
99+
const outside = (point.latitude < minX || point.latitude > maxX || point.longitude < minY || point.longitude > maxY);
100+
if (outside) {
101+
return false;
102+
}
103+
104+
let inside = false;
105+
for (let i = 0, j = this._coordinates.length - 1 ; i < this._coordinates.length; j = i++) {
106+
let startX = this._coordinates[i][0];
107+
let startY = this._coordinates[i][1];
108+
let endX = this._coordinates[j][0];
109+
let endY = this._coordinates[j][1];
110+
111+
const intersect = (( startY > point.longitude ) != ( endY > point.longitude ) &&
112+
point.latitude < ( endX - startX ) * ( point.longitude - startY ) / ( endY - startY ) + startX);
113+
114+
if (intersect) {
115+
inside = !inside;
116+
}
117+
}
118+
return inside;
119+
}
120+
121+
/**
122+
* Throws an exception if the given lat-long is out of bounds.
123+
* @return {Array}
124+
*/
125+
static _validate(coords: Array) {
126+
if (!Array.isArray(coords)) {
127+
throw new TypeError('Coordinates must be an Array');
128+
}
129+
if (coords.length < 3) {
130+
throw new TypeError('Polygon must have at least 3 GeoPoints or Points');
131+
}
132+
const points = [];
133+
for (let i = 0; i < coords.length; i += 1) {
134+
const coord = coords[i];
135+
let geoPoint;
136+
if (coord instanceof ParseGeoPoint) {
137+
geoPoint = coord;
138+
} else if (Array.isArray(coord) && coord.length === 2) {
139+
geoPoint = new ParseGeoPoint(coord[0], coord[1]);
140+
} else {
141+
throw new TypeError('Coordinates must be an Array of GeoPoints or Points');
142+
}
143+
points.push([geoPoint.latitude, geoPoint.longitude]);
144+
}
145+
return points;
146+
}
147+
}

src/ParseQuery.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import CoreManager from './CoreManager';
1313
import encode from './encode';
1414
import ParseError from './ParseError';
1515
import ParseGeoPoint from './ParseGeoPoint';
16+
import ParsePolygon from './ParsePolygon';
1617
import ParseObject from './ParseObject';
1718
import ParsePromise from './ParsePromise';
1819

@@ -1013,6 +1014,19 @@ export default class ParseQuery {
10131014
return this._addCondition(key, '$geoWithin', { '$polygon': points });
10141015
}
10151016

1017+
/**
1018+
* Add a constraint to the query that requires a particular key's
1019+
* coordinates that contains a ParseGeoPoint
1020+
*
1021+
* @method polygonContains
1022+
* @param {String} key The key to be constrained.
1023+
* @param {Parse.GeoPoint} GeoPoint
1024+
* @return {Parse.Query} Returns the query, so you can chain this call.
1025+
*/
1026+
polygonContains(key: string, point: ParseGeoPoint): ParseQuery {
1027+
return this._addCondition(key, '$geoIntersects', { '$point': point });
1028+
}
1029+
10161030
/** Query Orderings **/
10171031

10181032
/**

src/decode.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import ParseACL from './ParseACL';
1313
import ParseFile from './ParseFile';
1414
import ParseGeoPoint from './ParseGeoPoint';
15+
import ParsePolygon from './ParsePolygon';
1516
import ParseObject from './ParseObject';
1617
import { opFromJSON } from './ParseOp';
1718
import ParseRelation from './ParseRelation';
@@ -54,6 +55,9 @@ export default function decode(value: any): any {
5455
longitude: value.longitude
5556
});
5657
}
58+
if (value.__type === 'Polygon') {
59+
return new ParsePolygon(value.coordinates);
60+
}
5761
var copy = {};
5862
for (var k in value) {
5963
copy[k] = decode(value[k]);

src/encode.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import ParseACL from './ParseACL';
1313
import ParseFile from './ParseFile';
1414
import ParseGeoPoint from './ParseGeoPoint';
15+
import ParsePolygon from './ParsePolygon';
1516
import ParseObject from './ParseObject';
1617
import { Op } from './ParseOp';
1718
import ParseRelation from './ParseRelation';
@@ -38,6 +39,7 @@ function encode(value: mixed, disallowObjects: boolean, forcePointers: boolean,
3839
if (value instanceof Op ||
3940
value instanceof ParseACL ||
4041
value instanceof ParseGeoPoint ||
42+
value instanceof ParsePolygon ||
4143
value instanceof ParseRelation) {
4244
return value.toJSON();
4345
}

0 commit comments

Comments
 (0)