Skip to content

Commit

Permalink
fix: Save and decode ParsePolygon correctly (#118)
Browse files Browse the repository at this point in the history
* fix: Save and decode ParsePolygon correctly

* add changelog

* refactor
  • Loading branch information
cbaker6 committed Jun 13, 2023
1 parent 7f07262 commit 47548b3
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 33 deletions.
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@
# Parse-Swift Changelog

### main
[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.7.1...main), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/main/documentation/parseswift)
[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.7.2...main), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/main/documentation/parseswift)
* _Contributing to this repo? Add info about your change here to be included in the next release_

### 5.7.2
[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.7.1...5.7.2), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/5.7.2/documentation/parseswift)

__Fixes__
* ParsePolygon encoding during a save and decoding resulted in (longitude, latitude) when it should be
(latitude, longitude). If a developer used ParseSwift <= 5.7.1
to save/update ParsePolygon's, they will need to update the respective ParseObjects by swapping the latitude
and longitude manually. Deprecated withinPolygon() query constraint for geoPoint() and polygonContains() for
polygon() ([#118](https://github.com/netreconlab/Parse-Swift/pull/118)), thanks to
[Corey Baker](https://github.com/cbaker6).

### 5.7.1
[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.7.0...5.7.1), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/5.7.1/documentation/parseswift)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ struct GameData: ParseObject {
var originalData: Data?

//: Your own properties.
var polygon: ParsePolygon?
var fence: ParsePolygon?
//: `ParseBytes` needs to be a part of the original schema
//: or else you will need your primaryKey to force an upgrade.
var bytes: ParseBytes?
Expand All @@ -92,9 +92,9 @@ struct GameData: ParseObject {
*/
func merge(with object: Self) throws -> Self {
var updated = try mergeParse(with: object)
if shouldRestoreKey(\.polygon,
if shouldRestoreKey(\.fence,
original: object) {
updated.polygon = object.polygon
updated.fence = object.fence
}
if shouldRestoreKey(\.bytes,
original: object) {
Expand All @@ -108,9 +108,9 @@ struct GameData: ParseObject {
//: to preserve the memberwise initializer.
extension GameData {

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

Expand Down Expand Up @@ -396,18 +396,18 @@ Task {

Task {
//: 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)
let detroitPoints = [
try ParseGeoPoint(latitude: 42.631655189280224, longitude: -83.78406753121705),
try ParseGeoPoint(latitude: 42.633047793854814, longitude: -83.75333640366955),
try ParseGeoPoint(latitude: 42.61625254348911, longitude: -83.75149921669944),
try ParseGeoPoint(latitude: 42.61526926650296, longitude: -83.78161794858735),
try ParseGeoPoint(latitude: 42.631655189280224, longitude: -83.78406753121705)
]

do {
let polygon = try ParsePolygon(points)
let detroit = try ParsePolygon(detroitPoints)
let bytes = ParseBytes(data: "hello world".data(using: .utf8)!)
var gameData = GameData(bytes: bytes, polygon: polygon)
var gameData = GameData(bytes: bytes, fence: detroit)
gameData = try await gameData.save()
print("Successfully saved: \(gameData)")
} catch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ struct GameScore: ParseObject {
//: Your own properties
var points: Int?
var location: ParseGeoPoint?
var polygon: ParsePolygon?
var fence: ParsePolygon?

/*:
Optional - implement your own version of merge
Expand All @@ -48,9 +48,9 @@ struct GameScore: ParseObject {
original: object) {
updated.location = object.location
}
if updated.shouldRestoreKey(\.polygon,
if updated.shouldRestoreKey(\.fence,
original: object) {
updated.polygon = object.polygon
updated.fence = object.fence
}
return updated
}
Expand All @@ -74,7 +74,7 @@ do {
try .init(latitude: 42.0, longitude: -35.0),
try .init(latitude: 42.0, longitude: -20.0)
]
score.polygon = try ParsePolygon(points)
score.fence = try ParsePolygon(points)
}

/*:
Expand All @@ -90,7 +90,7 @@ score.save { result in
assert(savedScore.updatedAt != nil)
assert(savedScore.points == 10)
assert(savedScore.location != nil)
assert(savedScore.polygon != nil)
assert(savedScore.fence != nil)

guard let location = savedScore.location else {
print("Something went wrong")
Expand All @@ -99,13 +99,13 @@ score.save { result in

print("Saved location: \(location)")

guard let polygon = savedScore.polygon else {
guard let fence = savedScore.fence else {
print("Something went wrong")
return
}

print("Saved polygon: \(polygon)")
print("Saved polygon geopoints: \(polygon.coordinates)")
print("Saved polygon: \(fence)")
print("Saved polygon geopoints: \(fence.coordinates)")

case .failure(let error):
assertionFailure("Error saving: \(error)")
Expand Down Expand Up @@ -293,7 +293,7 @@ do {
try .init(latitude: 42.0, longitude: -35.0),
try .init(latitude: 42.0, longitude: -20.0)
]
let query9 = GameScore.query(withinPolygon(key: "location", points: points))
let query9 = GameScore.query(geoPoint("location", within: points))
query9.find { results in
switch results {
case .success(let scores):
Expand All @@ -320,7 +320,7 @@ do {
try .init(latitude: 42.0, longitude: -20.0)
]
let polygon = try ParsePolygon(points)
let query10 = GameScore.query(withinPolygon(key: "location", polygon: polygon))
let query10 = GameScore.query(geoPoint("location", within: polygon))
query10.find { results in
switch results {
case .success(let scores):
Expand All @@ -339,6 +339,27 @@ do {
print("Could not create geopoints: \(error)")
}

do {
let location = try ParseGeoPoint(latitude: 40.0, longitude: -30.0)
let query = GameScore.query(polygon("fence", contains: location))
query.find { results in
switch results {
case .success(let scores):
scores.forEach { (score) in
print("""
Someone has a points value of \"\(String(describing: score.points))\"
with a geolocation \(location) within the
polygon: \(String(describing: score.location))
""")
}
case .failure(let error):
assertionFailure("Error querying: \(error)")
}
}
} catch {
print("Could not create geopoints: \(error)")
}

//: Hint of the previous query (asynchronous)
query2 = query2.hint("_id_")
query2.find { result in
Expand Down
2 changes: 1 addition & 1 deletion Sources/ParseSwift/ParseConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation

enum ParseConstants {
static let sdk = "swift"
static let version = "5.7.1"
static let version = "5.7.2"
static let fileManagementDirectory = "parse/"
static let fileManagementPrivateDocumentsDirectory = "Private Documents/"
static let fileManagementLibraryDirectory = "Library/"
Expand Down
9 changes: 7 additions & 2 deletions Sources/ParseSwift/Types/ParsePolygon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
public struct ParsePolygon: ParseTypeable, Hashable {
private let __type: String = "Polygon" // swiftlint:disable:this identifier_name
public let coordinates: [ParseGeoPoint]
var isSwappingCoordinates = false

enum CodingKeys: String, CodingKey {
case __type // swiftlint:disable:this identifier_name
Expand Down Expand Up @@ -108,6 +109,10 @@ extension ParsePolygon {
try container.encode(__type, forKey: .__type)
var nestedUnkeyedContainer = container.nestedUnkeyedContainer(forKey: .coordinates)
try coordinates.forEach {
guard isSwappingCoordinates else {
try nestedUnkeyedContainer.encode([$0.latitude, $0.longitude])
return
}
try nestedUnkeyedContainer.encode([$0.longitude, $0.latitude])
}
}
Expand All @@ -124,8 +129,8 @@ extension ParsePolygon {
let points = try values.decode([[Double]].self, forKey: .coordinates)
try points.forEach {
if $0.count == 2 {
guard let latitude = $0.last,
let longitude = $0.first else {
guard let latitude = $0.first,
let longitude = $0.last else {
throw ParseError(code: .otherCause, message: "Could not decode ParsePolygon: \(points)")
}
decodedCoordinates.append(try ParseGeoPoint(latitude: latitude,
Expand Down
54 changes: 51 additions & 3 deletions Sources/ParseSwift/Types/QueryConstraint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -587,11 +587,28 @@ public func withinGeoBox(key: String, fromSouthWest southwest: ParseGeoPoint,
- warning: Requires Parse Server 2.5.0+.
- returns: The same instance of `QueryConstraint` as the receiver.
*/
public func withinPolygon(key: String, points: [ParseGeoPoint]) -> QueryConstraint {
public func geoPoint(_ key: String, within points: [ParseGeoPoint]) -> QueryConstraint {
let dictionary = [QueryConstraint.Comparator.polygon.rawValue: points]
return .init(key: key, value: dictionary, comparator: .geoWithin)
}

/**
Add a constraint to the query that requires a particular key's
coordinates be contained within and on the bounds of a given polygon
Supports closed and open (last point is connected to first) paths.
Polygon must have at least 3 points.
- parameter key: The key to be constrained.
- parameter points: The polygon points as an Array of `ParseGeoPoint`'s.
- warning: Requires Parse Server 2.5.0+.
- returns: The same instance of `QueryConstraint` as the receiver.
*/
@available(*, deprecated, renamed: "geoPoint")
public func withinPolygon(key: String, points: [ParseGeoPoint]) -> QueryConstraint {
geoPoint(key, within: points)
}

/**
Add a constraint to the query that requires a particular key's
coordinates be contained within and on the bounds of a given polygon
Expand All @@ -602,11 +619,28 @@ public func withinPolygon(key: String, points: [ParseGeoPoint]) -> QueryConstrai
- warning: Requires Parse Server 2.5.0+.
- returns: The same instance of `QueryConstraint` as the receiver.
*/
public func withinPolygon(key: String, polygon: ParsePolygon) -> QueryConstraint {
public func geoPoint(_ key: String, within polygon: ParsePolygon) -> QueryConstraint {
var polygon = polygon
polygon.isSwappingCoordinates = true
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 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 polygon: The `ParsePolygon`.
- warning: Requires Parse Server 2.5.0+.
- returns: The same instance of `QueryConstraint` as the receiver.
*/
@available(*, deprecated, renamed: "geoPoint")
public func withinPolygon(key: String, polygon: ParsePolygon) -> QueryConstraint {
geoPoint(key, within: polygon)
}

/**
Add a constraint to the query that requires a particular key's
coordinates contains a `ParseGeoPoint`.
Expand All @@ -616,11 +650,25 @@ public func withinPolygon(key: String, polygon: ParsePolygon) -> QueryConstraint
- warning: Requires Parse Server 2.6.0+.
- returns: The same instance of `QueryConstraint` as the receiver.
*/
public func polygonContains(key: String, point: ParseGeoPoint) -> QueryConstraint {
public func polygon(_ key: String, contains point: ParseGeoPoint) -> QueryConstraint {
let dictionary = [QueryConstraint.Comparator.point.rawValue: point]
return .init(key: key, value: dictionary, comparator: .geoIntersects)
}

/**
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.
*/
@available(*, deprecated, renamed: "polygon")
public func polygonContains(key: String, point: ParseGeoPoint) -> QueryConstraint {
polygon(key, contains: point)
}

/**
Add a constraint for finding string values that contain a provided
string using Full Text Search.
Expand Down
7 changes: 4 additions & 3 deletions Tests/ParseSwiftTests/ParsePolygonTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class ParsePolygonTests: XCTestCase {

func testEncode() throws {
let polygon = try ParsePolygon(points)
let expected = "{\"__type\":\"Polygon\",\"coordinates\":[[0,0],[1,0],[1,1],[0,1],[0,0]]}"
let expected = "{\"__type\":\"Polygon\",\"coordinates\":[[0,0],[0,1],[1,1],[1,0],[0,0]]}"
XCTAssertEqual(polygon.debugDescription, expected)
guard polygon.coordinates.count == points.count else {
XCTAssertEqual(polygon.coordinates.count, points.count)
Expand All @@ -88,7 +88,8 @@ class ParsePolygonTests: XCTestCase {
}

func testDecode() throws {
let polygon = try ParsePolygon(points)
var polygon = try ParsePolygon(points)
polygon.isSwappingCoordinates = false
let encoded = try ParseCoding.jsonEncoder().encode(polygon)
let decoded = try ParseCoding.jsonDecoder().decode(ParsePolygon.self, from: encoded)
XCTAssertEqual(decoded, polygon)
Expand Down Expand Up @@ -141,7 +142,7 @@ class ParsePolygonTests: XCTestCase {

func testDescription() throws {
let polygon = try ParsePolygon(points)
let expected = "{\"__type\":\"Polygon\",\"coordinates\":[[0,0],[1,0],[1,1],[0,1],[0,0]]}"
let expected = "{\"__type\":\"Polygon\",\"coordinates\":[[0,0],[0,1],[1,1],[1,0],[0,0]]}"
XCTAssertEqual(polygon.description, expected)
}
}

0 comments on commit 47548b3

Please sign in to comment.