Efficiently finding NSManagedObject (Polygon) that a given point is within - ios

I'm trying to find if a given point (CLLocationCoordinate2D) is within a Polygon NSManagedObject.
My Polygon object is defined as:
public class Polygon: NSManagedObject {
#NSManaged var points: NSOrderedSet?
#NSManaged var centroid: Point?
#NSManaged var computed : NSNumber!
}
And the Point object:
public class Point: NSManagedObject {
#NSManaged var longitude: NSNumber!
#NSManaged var latitude: NSNumber!
}
My current method uses this for creating the predicate for Polygon objects:
public static func nearbyPredicate(offset offset: Double, nearLocation location: CLLocationCoordinate2D) -> NSPredicate {
let maxLat = location.latitude + offset
let minLat = location.latitude - offset
let maxLong = location.longitude - offset
let minLong = location.longitude + offset
return NSPredicate(format: "(centroid.latitude <= %#) && (centroid.latitude >= %#) && (centroid.longitude >= %#) && (centroid.longitude <= %#) && (computed == false)", argumentArray: [maxLat, minLat, maxLong, minLong])
}
Where offset is an arbitrary search 'radius'. The computed property is a boolean that I set to true once I have tried to detect if the Polygon contains the given point so that subsequent fetch calls exclude the object (as seen in the predicate). For detection, I first fetch Polygons with that predicate above, then use this:
let location : CLLocationCoordinate2D // ... point to test
for poly in polysFetched {
poly.computed = NSNumber(bool: true)
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: (poly.points!.array as! [Point]).first!.longitude.doubleValue, y: (poly.points!.array as! [Point]).first!.latitude.doubleValue)) //set initial point
for pt in poly.points!.array as! [Point] {
path.addLineToPoint(CGPoint(x: pt.longitude.doubleValue, y: pt.latitude.doubleValue))
}
if (CGPathContainsPoint(path.CGPath, nil, CGPoint(x: location.longitude, y: location.latitude), false)) {
print("Poly \(poly) matches location \(location)")
return
}
}
This can be computationally heavy. The polygons I'm testing against can be quite large and oddly shaped.
Is there anyway to make this process more efficient using Core Data? Is there a way to offset the computation of calculating the containment of a point in an NSFetchRequest or in other ways?

I ended up resolving this in a few ways and sped up the process significantly. Since the Point NSManagedObject only existed for the purpose of fetching them via the Polygon relationships and computing the path (UIBezierPath) for the Polygon, I just computed the path during the creation of the Polygon object and stored it as a property. I also changed the Centriod relationship into the two properties c_lat, c_long and removed the Point class altogether! Indexing the c_lat and c_long properties sped up fetch requests too.
The new Polygon class is:
public class Polygon: NSManagedObject {
#NSManaged internal var pathData : NSData!
#NSManaged internal var c_lat : NSNumber!
#NSManaged internal var c_long : NSNumber!
#NSManaged var computed : NSNumber!
var path : UIBezierPath {
get {
return NSKeyedUnarchiver.unarchiveObjectWithData(pathData) as! UIBezierPath
}
set {
pathData = NSKeyedArchiver.archivedDataWithRootObject(newValue)
}
}
var centroid : CLLocationCoordinate2D {
get {
return CLLocationCoordinate2D(latitude: c_lat.doubleValue, longitude: c_long.doubleValue)
}
set {
c_lat = newValue.latitude
c_long = newValue.longitude
}
}
func containsLocation(location : CLLocationCoordinate2D) -> Bool {
return self.path.containsPoint(CGPoint(x: location.latitude, y: location.longitude))
}
}
Code Different's comment on my original post suggested the Wikipedia page for detecting points in polygons, but conveniently that is abstracted away from me with UIBezierPath and there exists a function CGPathContainsPoint that takes a boolean as its last argument to choose between the two algorithms for detecting points in polygons mentioned in the Wikipedia article.
For those encountering the same or similar issues that I did, I recommend inlining relationships and using computed properties to access non-primitive types of data you'd like to store in Core Data.

Related

Convert MKAnnotation array to set

I have a situation where I have map annotations as [MKAnnotation] array in swift. Now I need to convert this into a set for some operation. How can I do this in swift? Basically I need to add only the non existent annotations on the map while updating the map view.
You can find out if an annotation exists on the map by using mapView.view(for:) as mentioned here:
if (self.mapView.view(for: annotation) != nil) {
print("pin already on mapview")
}
"Basically I need to add only the non existent annotations on the map while updating the map view."
We first need to define what makes two annotations equal (in your scenario). Once that is clear you an override the isEqual method. You can then add annotations to a Set.
Here is an example :
class MyAnnotation : NSObject,MKAnnotation{
var coordinate: CLLocationCoordinate2D
var title: String?
convenience init(coord : CLLocationCoordinate2D, title: String) {
self.init()
self.coordinate = coord
self.title = title
}
private override init() {
self.coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0)
}
override func isEqual(_ object: Any?) -> Bool {
if let annot = object as? MyAnnotation{
// Add your defintion of equality here. i.e what determines if two Annotations are equal.
return annot.coordinate.latitude == coordinate.latitude && annot.coordinate.longitude == coordinate.longitude && annot.title == title
}
return false
}
}
In the code above two instances of MyAnnotation are considered equal when you they have the same coordinates and the same title.
let ann1 = MyAnnotation(coord: CLLocationCoordinate2D(latitude: 20.0, longitude: 30.0), title: "Annot A")
let ann2 = MyAnnotation(coord: CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0), title: "Annot B")
let ann3 = MyAnnotation(coord: CLLocationCoordinate2D(latitude: 20.0, longitude: 30.0), title: "Annot A")
var annSet = Set<MyAnnotation>()
annSet.insert(ann1)
annSet.insert(ann2)
annSet.insert(ann3)
print(annSet.count) // Output : 2 (ann1 & ann3 are equal)

Order the items inside an array from nearest to fairest (from my position)

I created two functions to calculate the distance from me to a point on the map
func distance(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D) -> CLLocationDistance {
let from = CLLocation(latitude: from.latitude, longitude: from.longitude)
let to = CLLocation(latitude: to.latitude, longitude: to.longitude)
return from.distance(from: to)
}
func choSed(car:Car) {
guard let coordinates = car.location else {
return
}
self.destination = coordinates
if currentLocation != nil {
let dis = distance(from: currentLocation!, to: coordinates)
}
}
and they work well. Now what i need to do (in another function) is to order the items inside an array carsArray from nearest to fairest (from my position) . I don't know how can i do, maybe i have to calculate the distance of all the items of the array with a cycle and than use a filter to order the position of them, for now i tried to build something like this
for car in carsArray {
for di in distance(from: currentLocation!, to: car.location!) {
}
}
but i get the error (Type 'CLLocationDistance' (aka 'Double') does not conform to protocol 'Sequence') and i also don't know if this could be the correct way to do it. Someone can help me? (car.location! location in my custom class Car is var location: CLLocationCoordinate2D? and the array carsArray is a vector of my custom class Car i can add to this class all the parameters that it could be useful)
CLLocationDistance is a typealias of a Double.
As already mentioned in one of your previous questions the in parameter in a for loop must be an array or a range.
To order the items sort them
let sortedArray = carsArray.sorted {
distance(from: currentLocation!, to: $0.location!) < distance(from: currentLocation!, to: $1.location!)
}

Check if array has same Latitude Longitude swift

I have a class which has those data
class Events {
var name: String!
var latitude: Double!
var longitude: Double!
}
And I fill it out with data from json.
So some Events have the same lat and lon but they are not on continuous, i mean its not that event3 is the same as event4 etc.
So I'm trying to show them on a map
Filling out this array
var events = [Events]()
And in this for loop i'm making the pins.
for events in events {
let annotation = MKPointAnnotation()
annotation.title = events.name
annotation.coordinate = CLLocationCoordinate2D(latitude: events.latitude, longitude: events.longitude)
mapView.addAnnotation(annotation)
}
How can I make a quick search before deploying the pins, to see if a pin has the same lat and lon with another pin, to add some digits just to show them both close?
Thanks a lot!
Use Set to find unique instances. In order to use Set your base element, Events in this case must be Hashable and by implication Equatable:
class Events : Hashable {
var name: String!
var latitude: Double!
var longitude: Double!
// implement Hashable
var hashValue: Int {
return latitude.hashValue | longitude.hashValue
}
// Implement Equatable
static func ==(lhs:Events, rhs:Events) -> Bool {
return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude
}
}
Then your main loop is a direct extension of what you already have, note that this collapses all matches down to a single point and changes the name to indicate how many matches there are:
// Use a Set to filter out duplicates
for event in Set<Events>(events) {
let annotation = MKPointAnnotation()
// Count number of occurrences of each item in the original array
let count = events.filter { $0 == event }.count
// Append (count) to the title if it's not 1
annotation.title = count > 1 ? "\(event.name) (\(count))" : event.name
// add to the map
}
If, instead, you want to move the points so that they don't stack up, then you want something like, where we build up the set of occupied locations as we go and mutate the points to move them a little.
func placeEvents(events:[Events], mapView:MKMapView) {
var placed = Set<Events>()
for event in events {
if placed.contains(event) {
// collision: mutate the location of event as needed,
}
// Add the mutated point to occupied points
placed.formUnion([event])
// Add the point to the map here
}
}
If the values aren't expected to be exactly the same, but only within, eg., .0001 of each other, then you could use the following for hashValue and ==
fileprivate let tolerance = 1.0 / 0.0001
private var tolerantLat : Long { return Long(tolerance * latitude) }
private var tolerantLon : Long { return Long(tolerance * longitude) }
var hashValue : Int {
return tolerantLat.hashValue | tolerantLon.hashValue
}
static func ==(lhs:Events, rhs:Events) -> Bool {
return lhs.tolerantLat == rhs.tolerantLat && lhs.tolerantLon == rhs.tolerantLon
}

MKAnnotation subclass issues

I upgraded to Swift 1.2 last night, and I got a bug I really can't figure out. The below code worked fine in the previous version of Xcode and Swift.
//MARK: Annotation Object
class PointAnnotation : NSObject, MKAnnotation {
var coordinate: CLLocationCoordinate2D
var title: String
var subtitle: String
var point: Point
var image: UIImage
var md: String
init(point: Point) {
self.coordinate = point.coordinate
self.title = point.title
self.subtitle = point.teaser
self.image = UIImage(named: "annotation.png")!
self.point = point
self.md = point.content
}
}
On line 3, I get the somewhat hard to understand error
Objective-C method 'setCoordinate:' provided by the setter for 'coordinate' conflicts with the optional requirement method 'setCoordinate' in protocol 'MKAnnotation' I tried changing variable names and such, but no help. Does anyone have any idea how to fix this?
The class is for annotations on my mapview.
If you do not require to change coordinates after initialization then you can use it that way. It works for me with Swift 1.2:
class CustomAnnotation : NSObject, MKAnnotation {
let coordinate: CLLocationCoordinate2D
var title: String
init(coordinate: CLLocationCoordinate2D, title: String) {
self.coordinate = coordinate
self.title = title
}
}
You are overriding a readonly ivar in MKAnnotation with a readwrite.
var coordinate: CLLocationCoordinate2D { get }
I had the same issue so I just did this:
private var _coordinate: CLLocationCoordinate2D
var coordinate: CLLocationCoordinate2D {
return _coordinate
}
init(coordinate: CLLocationCoordinate2D) {
self._coordinate = coordinate
super.init()
}
func setCoordinate(newCoordinate: CLLocationCoordinate2D) {
self._coordinate = newCoordinate
}
That way coordinate is still readonly and you can use the setCoordinate method.

Swift optional variable not set using Xcode 6.1

I'm trying to build my first Swift application. In this application I'm looping over an KML file that contains information about some restaurant and for each one of them I'm trying to build a Place object with the available information, compare a distance and keep the Place which is the closest to a given point.
Here is my Place model, a very simple model (Place.swift):
import Foundation
import MapKit
class Place {
var name:String
var description:String? = nil
var location:CLLocationCoordinate2D = CLLocationCoordinate2D(latitude:0, longitude:0)
init(name: String, description: String?, latitude: Double, longitude: Double)
{
self.name = name
self.description = description
self.location = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
func getDistance(point: CLLocationCoordinate2D) -> Float
{
return Geo.distance(point, coordTo: self.location)
}
}
and here is the part of the application that is looping over the items from the KML file.
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) {(response, data, error) in
let xml = SWXMLHash.parse(data);
var minDistance:Float = Float(UInt64.max)
var closestPlace:Place? = nil
var place:Place? = nil
for placemark in xml["kml"]["Document"]["Folder"]["Placemark"] {
var coord = placemark["Point"]["coordinates"].element?.text?.componentsSeparatedByString(",")
// Create a place object if the place has a name
if let placeName = placemark["name"].element?.text {
NSLog("Place name defined, object created")
// Overwrite the place variable with a new object
place = Place(name: placeName, description: placemark["description"].element?.text, latitude: (coord![1] as NSString).doubleValue, longitude: (coord![0] as NSString).doubleValue)
var distance = place!.getDistance(self.middlePosition)
if distance < minDistance {
minDistance = distance
closestPlace = place
} else {
NSLog("Place name could not be found, skipped")
}
}
}
I added breakpoints in this script, when the distance is calculated. The value of the place variable is nil and I don't understand why. If I replace this line:
place = Place(name: placeName, description: placemark["description"].element?.text, latitude: (coord![1] as NSString).doubleValue, longitude: (coord![0] as NSString).doubleValue)
by this line:
let place = Place(name: placeName, description: placemark["description"].element?.text, latitude: (coord![1] as NSString).doubleValue, longitude: (coord![0] as NSString).doubleValue)
I can see that my place object is instantiated correctly now but I can't understand why.
Also I have the exact same issue when I tried to save the closest place:
closestPlace = place
In the inspector the value of closestPlace is nil even after being set with my place object.
I fixed my issue adding a ! after the Place object. I guess is to tell that I am sure I have an Object in this variable and that is not nil.
if let placeName = placemark["name"].element?.text {
NSLog("Place name defined, object created")
// Overwrite the place variable with a new object
place = Place(name: placeName, description: placemark["description"].element?.text, latitude: (coord![1] as NSString).doubleValue, longitude: (coord![0] as NSString).doubleValue)
var distance = place!.getDistance(self.middlePosition)
if distance < minDistance {
minDistance = distance
closestPlace = place!
} else {
NSLog("Place name could not be found, skipped")
}
}

Resources