Skip to content

Commit

Permalink
Fix ParsePolygon
Browse files Browse the repository at this point in the history
  • Loading branch information
cbaker6 committed Jul 10, 2021
1 parent 1d4372f commit f426531
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@ struct GameScore: ParseObject {

//: Your own properties.
var score: Int = 0
var bytes: ParseBytes?
var polygon: ParsePolygon?

//: Custom initializer.
init(score: Int) {
Expand All @@ -52,6 +50,25 @@ struct GameScore: ParseObject {
}
}

struct GameData: ParseObject {
//: Those are required for Object
var objectId: String?
var createdAt: Date?
var updatedAt: Date?
var ACL: ParseACL?

//: Your own properties.
var polygon: ParsePolygon?
//: `ParseBytes` needs to be a part of the original schema
//: or else you will need your masterKey to force an upgrade.
var bytes: ParseBytes?

init (bytes: ParseBytes?, polygon: ParsePolygon) {
self.bytes = bytes
self.polygon = polygon
}
}

//: Define initial GameScores.
let score = GameScore(score: 10)
let score2 = GameScore(score: 3)
Expand Down Expand Up @@ -166,15 +183,6 @@ guard var changedScore = savedScore else {
fatalError()
}
changedScore.score = 200
changedScore.bytes = ParseBytes(data: "hello world".data(using: .utf8)!)
let points = [
try ParseGeoPoint(latitude: 0, longitude: 0),
try ParseGeoPoint(latitude: 0, longitude: 1),
try ParseGeoPoint(latitude: 1, longitude: 1),
try ParseGeoPoint(latitude: 1, longitude: 0),
try ParseGeoPoint(latitude: 0, longitude: 0)
]
changedScore.polygon = ParsePolygon(points)

let savedChangedScore: GameScore?
do {
Expand Down Expand Up @@ -319,6 +327,24 @@ do {
assertionFailure("Error deleting: \(error)")
}*/

//: How to add `ParseBytes` and `ParsePolygon` to objects.
let points = [
try ParseGeoPoint(latitude: 0, longitude: 0),
try ParseGeoPoint(latitude: 0, longitude: 1),
try ParseGeoPoint(latitude: 1, longitude: 1),
try ParseGeoPoint(latitude: 1, longitude: 0),
try ParseGeoPoint(latitude: 0, longitude: 0)
]
do {
let polygon = try ParsePolygon(points)
let bytes = ParseBytes(data: "hello world".data(using: .utf8)!)
var gameData = GameData(bytes: bytes, polygon: polygon)
gameData = try gameData.save()
print("Successfully saved: \(gameData)")
} catch {
print("Error saving: \(error.localizedDescription)")
}

PlaygroundPage.current.finishExecution()

//: [Next](@next)
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,14 @@ score.save { result in
}

//: Now we will show how to query based on the `ParseGeoPoint`.
var query: Query<GameScore> //: Store query for later user
var constraints = [QueryConstraint]()

do {
let pointToFind = try ParseGeoPoint(latitude: 40.0, longitude: -30.0)
var constraints = [QueryConstraint]()
constraints.append(near(key: "location", geoPoint: pointToFind))

let query = GameScore.query(constraints)
query = GameScore.query(constraints)
query.find { results in
switch results {
case .success(let scores):
Expand Down
23 changes: 20 additions & 3 deletions Sources/ParseSwift/Types/ParsePolygon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,32 @@ public struct ParsePolygon: Codable, Hashable {
extension ParsePolygon {
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
coordinates = try values.decode([ParseGeoPoint].self, forKey: .coordinates)
var decodedCoordinates = [ParseGeoPoint]()
let points = try values.decode([[Double]].self, forKey: .coordinates)
try points.forEach {
if $0.count == 2 {
guard let latitude = $0.first,
let longitude = $0.last else {
throw ParseError(code: .unknownError, message: "Could not decode ParsePolygon: \(points)")
}
decodedCoordinates.append(try ParseGeoPoint(latitude: latitude,
longitude: longitude))
} else {
throw ParseError(code: .unknownError, message: "Could not decode ParsePolygon: \(points)")
}
}
coordinates = decodedCoordinates
try validate()
}

public func encode(to encoder: Encoder) throws {
try validate()
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(__type, forKey: .__type)
try container.encode(coordinates, forKey: .coordinates)
try validate()
var nestedUnkeyedContainer = container.nestedUnkeyedContainer(forKey: .coordinates)
try coordinates.forEach {
try nestedUnkeyedContainer.encode([$0.latitude, $0.longitude])
}
}
}

Expand Down
23 changes: 20 additions & 3 deletions Sources/ParseSwift/Types/Query.swift
Original file line number Diff line number Diff line change
Expand Up @@ -434,16 +434,33 @@ public func withinGeoBox(key: String, fromSouthWest southwest: ParseGeoPoint,
- returns: The same instance of `QueryConstraint` as the receiver.
*/
public func withinPolygon(key: String, points: [ParseGeoPoint]) -> QueryConstraint {
let dictionary = [QueryConstraint.Comparator.polygon.rawValue: points]
let polygon = points.flatMap { [[$0.latitude, $0.longitude]]}
let dictionary = [QueryConstraint.Comparator.polygon.rawValue: polygon]
return .init(key: key, value: dictionary, comparator: .geoWithin)
}

/**
Add a constraint to the query that requires a particular key's
coordinates that contains a `ParseGeoPoint`.
coordinates be contained within and on the bounds of a given polygon
Supports closed and open (last point is connected to first) paths.
- parameter key: The key to be constrained.
- parameter point: The point the polygon contains `ParseGeoPoint`.
- parameter polygon: The `ParsePolygon`.
- warning: Requires Parse Server 2.5.0+.
- returns: The same instance of `QueryConstraint` as the receiver.
*/
public func withinPolygon(key: String, polygon: ParsePolygon) -> QueryConstraint {
let polygon = polygon.coordinates.flatMap { [[$0.latitude, $0.longitude]]}
let dictionary = [QueryConstraint.Comparator.polygon.rawValue: polygon]
return .init(key: key, value: dictionary, comparator: .geoWithin)
}

/**
Add a constraint to the query that requires a particular key's
coordinates contains a `ParseGeoPoint`.
- parameter key: The key of the `ParsePolygon`.
- parameter point: The `ParseGeoPoint` to check for containment.
- warning: Requires Parse Server 2.6.0+.
- returns: The same instance of `QueryConstraint` as the receiver.
*/
Expand Down
7 changes: 7 additions & 0 deletions Tests/ParseSwiftTests/ParseBytesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ class ParseBytesTests: XCTestCase {
try ParseStorage.shared.deleteAll()
}

func testDecode() throws {
let bytes = ParseBytes(base64: "ZnJveW8=")
let encoded = try ParseCoding.jsonEncoder().encode(bytes)
let decoded = try ParseCoding.jsonDecoder().decode(ParseBytes.self, from: encoded)
XCTAssertEqual(decoded, bytes)
}

#if !os(Linux) && !os(Android)
func testDebugString() {
let bytes = ParseBytes(base64: "ZnJveW8=")
Expand Down
64 changes: 60 additions & 4 deletions Tests/ParseSwiftTests/ParsePolygonTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import XCTest
@testable import ParseSwift

class ParsePolygonTests: XCTestCase {

struct FakeParsePolygon: Encodable, Hashable {
private let __type: String = "Polygon" // swiftlint:disable:this identifier_name
public let coordinates: [[Double]]
}

override func setUpWithError() throws {
try super.setUpWithError()
guard let url = URL(string: "http://localhost:1337/1") else {
Expand Down Expand Up @@ -57,18 +63,68 @@ class ParsePolygonTests: XCTestCase {
XCTAssertThrowsError(try ParsePolygon(point, point))
}

func testDecode() throws {
let polygon = try ParsePolygon(points)
let encoded = try ParseCoding.jsonEncoder().encode(polygon)
let decoded = try ParseCoding.jsonDecoder().decode(ParsePolygon.self, from: encoded)
XCTAssertEqual(decoded, polygon)
}

func testDecodeFailNotEnoughPoints() throws {
let fakePolygon = FakeParsePolygon(coordinates: [[0.0, 0.0], [0.0, 1.0]])
let encoded = try ParseCoding.jsonEncoder().encode(fakePolygon)
do {
_ = try ParseCoding.jsonDecoder().decode(ParsePolygon.self, from: encoded)
XCTFail("Should have failed")
} catch {
guard let parseError = error as? ParseError else {
XCTFail("Should have unwrapped")
return
}
XCTAssertTrue(parseError.message.contains("3 ParseGeoPoint"))
}
}

func testDecodeFailWrongData() throws {
let fakePolygon = FakeParsePolygon(coordinates: [[0.0], [1.0]])
let encoded = try ParseCoding.jsonEncoder().encode(fakePolygon)
do {
_ = try ParseCoding.jsonDecoder().decode(ParsePolygon.self, from: encoded)
XCTFail("Should have failed")
} catch {
guard let parseError = error as? ParseError else {
XCTFail("Should have unwrapped")
return
}
XCTAssertTrue(parseError.message.contains("decode ParsePolygon"))
}
}

func testDecodeFailTooMuchCoordinates() throws {
let fakePolygon = FakeParsePolygon(coordinates: [[0.0, 0.0, 0.0], [0.0, 1.0, 1.0]])
let encoded = try ParseCoding.jsonEncoder().encode(fakePolygon)
do {
_ = try ParseCoding.jsonDecoder().decode(ParsePolygon.self, from: encoded)
XCTFail("Should have failed")
} catch {
guard let parseError = error as? ParseError else {
XCTFail("Should have unwrapped")
return
}
XCTAssertTrue(parseError.message.contains("decode ParsePolygon"))
}
}

#if !os(Linux) && !os(Android)
func testDebugString() throws {
let polygon = try ParsePolygon(points)
// swiftlint:disable:next line_length
let expected = "ParsePolygon ({\"__type\":\"Polygon\",\"coordinates\":[{\"__type\":\"GeoPoint\",\"longitude\":0,\"latitude\":0},{\"__type\":\"GeoPoint\",\"longitude\":1,\"latitude\":0},{\"__type\":\"GeoPoint\",\"longitude\":1,\"latitude\":1},{\"__type\":\"GeoPoint\",\"longitude\":0,\"latitude\":1},{\"__type\":\"GeoPoint\",\"longitude\":0,\"latitude\":0}]})"
let expected = "ParsePolygon ({\"__type\":\"Polygon\",\"coordinates\":[[0,0],[0,1],[1,1],[1,0],[0,0]]})"
XCTAssertEqual(polygon.debugDescription, expected)
}

func testDescription() throws {
let polygon = try ParsePolygon(points)
// swiftlint:disable:next line_length
let expected = "ParsePolygon ({\"__type\":\"Polygon\",\"coordinates\":[{\"__type\":\"GeoPoint\",\"longitude\":0,\"latitude\":0},{\"__type\":\"GeoPoint\",\"longitude\":1,\"latitude\":0},{\"__type\":\"GeoPoint\",\"longitude\":1,\"latitude\":1},{\"__type\":\"GeoPoint\",\"longitude\":0,\"latitude\":1},{\"__type\":\"GeoPoint\",\"longitude\":0,\"latitude\":0}]})"
let expected = "ParsePolygon ({\"__type\":\"Polygon\",\"coordinates\":[[0,0],[0,1],[1,1],[1,0],[0,0]]})"
XCTAssertEqual(polygon.description, expected)
}
#endif
Expand Down
93 changes: 55 additions & 38 deletions Tests/ParseSwiftTests/ParseQueryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2211,20 +2211,63 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
}
}

// swiftlint:disable:next function_body_length
func testWhereKeyWithinPolygonPoints() throws {
let expected: [String: AnyCodable] = [
"yolo": ["$geoWithin": ["$polygon": [
[10.1, 20.1],
[20.1, 30.1],
[30.1, 40.1]]
]
]
]
let geoPoint1 = try ParseGeoPoint(latitude: 10.1, longitude: 20.1)
let geoPoint2 = try ParseGeoPoint(latitude: 20.1, longitude: 30.1)
let geoPoint3 = try ParseGeoPoint(latitude: 30.1, longitude: 40.1)
let polygon = [geoPoint1, geoPoint2, geoPoint3]
let constraint = withinPolygon(key: "yolo", points: polygon)
let query = GameScore.query(constraint)
let queryWhere = query.`where`

do {
let encoded = try ParseCoding.jsonEncoder().encode(queryWhere)
let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded)
XCTAssertEqual(expected.keys, decodedDictionary.keys)

guard let expectedValues = expected.values.first?.value as? [String: [String: [[Double]]]],
let expectedBox = expectedValues["$geoWithin"]?["$polygon"] else {
XCTFail("Should have casted")
return
}

guard let decodedValues = decodedDictionary.values.first?.value as? [String: [String: [[Double]]]],
let decodedBox = decodedValues["$geoWithin"]?["$polygon"] else {
XCTFail("Should have casted")
return
}
XCTAssertEqual(expectedBox, decodedBox)

} catch {
XCTFail(error.localizedDescription)
return
}
}

// swiftlint:disable:next function_body_length
func testWhereKeyWithinPolygon() throws {
let expected: [String: AnyCodable] = [
"yolo": ["$geoWithin": ["$polygon": [
["latitude": 10, "longitude": 20, "__type": "GeoPoint"],
["latitude": 20, "longitude": 30, "__type": "GeoPoint"],
["latitude": 30, "longitude": 40, "__type": "GeoPoint"]]
[10.1, 20.1],
[20.1, 30.1],
[30.1, 40.1]]
]
]
]
let geoPoint1 = try ParseGeoPoint(latitude: 10, longitude: 20)
let geoPoint2 = try ParseGeoPoint(latitude: 20, longitude: 30)
let geoPoint3 = try ParseGeoPoint(latitude: 30, longitude: 40)
let constraint = withinPolygon(key: "yolo", points: [geoPoint1, geoPoint2, geoPoint3])
let geoPoint1 = try ParseGeoPoint(latitude: 10.1, longitude: 20.1)
let geoPoint2 = try ParseGeoPoint(latitude: 20.1, longitude: 30.1)
let geoPoint3 = try ParseGeoPoint(latitude: 30.1, longitude: 40.1)
let polygon = try ParsePolygon(geoPoint1, geoPoint2, geoPoint3)
let constraint = withinPolygon(key: "yolo", polygon: polygon)
let query = GameScore.query(constraint)
let queryWhere = query.`where`

Expand All @@ -2233,44 +2276,18 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded)
XCTAssertEqual(expected.keys, decodedDictionary.keys)

guard let expectedValues = expected.values.first?.value as? [String: [String: [[String: Any]]]],
let expectedBox = expectedValues["$geoWithin"]?["$polygon"],
let expectedLongitude = expectedBox.first?["longitude"] as? Int,
let expectedLatitude = expectedBox.first?["latitude"] as? Int,
let expectedType = expectedBox.first?["__type"] as? String,
let expectedLongitude2 = expectedBox[1]["longitude"] as? Int,
let expectedLatitude2 = expectedBox[1]["latitude"] as? Int,
let expectedType2 = expectedBox[1]["__type"] as? String,
let expectedLongitude3 = expectedBox.last?["longitude"] as? Int,
let expectedLatitude3 = expectedBox.last?["latitude"] as? Int,
let expectedType3 = expectedBox.last?["__type"] as? String else {
guard let expectedValues = expected.values.first?.value as? [String: [String: [[Double]]]],
let expectedBox = expectedValues["$geoWithin"]?["$polygon"] else {
XCTFail("Should have casted")
return
}

guard let decodedValues = decodedDictionary.values.first?.value as? [String: [String: [[String: Any]]]],
let decodedBox = decodedValues["$geoWithin"]?["$polygon"],
let decodedLongitude = decodedBox.first?["longitude"] as? Int,
let decodedLatitude = decodedBox.first?["latitude"] as? Int,
let decodedType = decodedBox.first?["__type"] as? String,
let decodedLongitude2 = decodedBox[1]["longitude"] as? Int,
let decodedLatitude2 = decodedBox[1]["latitude"] as? Int,
let decodedType2 = decodedBox[1]["__type"] as? String,
let decodedLongitude3 = decodedBox.last?["longitude"] as? Int,
let decodedLatitude3 = decodedBox.last?["latitude"] as? Int,
let decodedType3 = decodedBox.last?["__type"] as? String else {
guard let decodedValues = decodedDictionary.values.first?.value as? [String: [String: [[Double]]]],
let decodedBox = decodedValues["$geoWithin"]?["$polygon"] else {
XCTFail("Should have casted")
return
}
XCTAssertEqual(expectedLongitude, decodedLongitude)
XCTAssertEqual(expectedLatitude, decodedLatitude)
XCTAssertEqual(expectedType, decodedType)
XCTAssertEqual(expectedLongitude2, decodedLongitude2)
XCTAssertEqual(expectedLatitude2, decodedLatitude2)
XCTAssertEqual(expectedType2, decodedType2)
XCTAssertEqual(expectedLongitude3, decodedLongitude3)
XCTAssertEqual(expectedLatitude3, decodedLatitude3)
XCTAssertEqual(expectedType3, decodedType3)
XCTAssertEqual(expectedBox, decodedBox)

} catch {
XCTFail(error.localizedDescription)
Expand Down

0 comments on commit f426531

Please sign in to comment.