I am attempting to include a function in a Predicate definition. Is this possible?
Let's say you have a Core Data entity of Places with attributes for
latitude and longitude.
I would like to add annotations to a mapview of those Places within a
specified distance from the user location. I can, of course, loop through the entire database and calculate the distance between each Place and the
user location but I will have about 35000 places and it would seem that
it would be more efficient to have a predicate in the
fetchedResultsController setup.
I tried the code below but I receive an error message of "Problem with
subpredicate BLOCKPREDICATE(0x2808237b0) with userInfo of (null)"
func myDistanceFilter(myLat : CLLocationDegrees, myLong : CLLocationDegrees, cdLat : CLLocationDegrees, cdLong : CLLocationDegrees) -> Bool {
let myLocation = CLLocation(latitude: myLat, longitude: myLong)
let cdLocation = CLLocation(latitude: cdLat, longitude: cdLong)
if myLocation.distance(from: cdLocation) < 5000.0 {
return true
} else {
return false
}
}//myDistancePredicate
And inside the fetchedResultsController:
let distancePredicate = NSPredicate {_,_ in self.myDistanceFilter(myLat: 37.774929, myLong: -122.419418, cdLat: 38.0, cdLong: -122.0)}
If it is possible to have a block/function inside a predicate how do you get a reference to an attribute for the entity object being evaluated?
Any guidance would be appreciated.
An additional observation for anyone else struggling with a similar issue.
Considering the suggestions of pbasdf and Jerry above, at least in my case, there is no reason why a region has to be round. I'll craft a name indicating a nearly rectangular area. I ran some tests with latitude and longitude values. These latitude and longitude values can be scaled to a rectangle enclosing a circular radius specified by the user. One degree of latitude is about 69 miles and one degree of longitude at the latitude of Chicago is about 51 miles. I used the following:
var myUserLatitude : CLLocationDegrees!
var myUserLongitude : CLLocationDegrees!
In the init file for the view:
guard let userLatitude = locationManager.location?.coordinate.latitude else {return}
guard let userLongitude = locationManager.location?.coordinate.longitude else {return}
myUserLatitude = userLatitude
myUserLongitude = userLongitude
Inside the fetchedResultsController variable creation:
let latitudeMinPredicate = NSPredicate(format: "latitude >= %lf", myUserLatitude - 1.0)
let latitudeMaxPredicate = NSPredicate(format: "latitude <= %lf", myUserLatitude + 1.0)
let longitudeMinPredicate = NSPredicate(format: "longitude >= %lf", myUserLongitude - 1.0)
let longitudeMaxPredicate = NSPredicate(format: "longitude <= %lf", myUserLongitude + 1.0)
var compoundPredicate = NSCompoundPredicate()
compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [anotherUnrelatedPredicate,latitudeMinPredicate,latitudeMaxPredicate,longitudeMinPredicate, longitudeMaxPredicate])
fetchRequest.predicate = compoundPredicate
Obviously I will create another property to scale the 1.0 value per the user desired region. Initial tests seem to work and best of all I can't believe how fast it it. Literally, the tableView is populated by the time the viewController segues to the enclosing mapView.
Well, yes it is possible. NSPredicate does have +predicateWithBlock: and init(block:).
However if you scroll down on that page you see the bad news:
Core Data supports block-based predicates in the in-memory and atomic stores, but not in the SQLite-based store.
So, whether you use an in-memory store, or do the filtering in code, either way you need to bring these 35,000 items into memory, and performance of a brute force approach will be poor.
There is a point of complexity at which SQL is no longer appropriate – you get better performance with real code. I think your requirement is far beyond that point. This will be an interesting computer science project in optimization. You need to do some pre-computing, analagous to adding an index to your database. Consider adding a region attribute to your place entities, then write your predicate to fetch all places within the target location's region and all immediate neighbors. Then filter through those candidates in code. I'm sure this has been done by others – think of cells in a cell phone network – but Core Data is not going to give you this for free.
Related
I'm trying to go to another area after a location is input (zip, address, city, st). It works-SOMETIMES. After I move the map (zoom out or move around), I'm not able to search for an area using the location. I thought I should call startUpdatingLocation again, but that didn't seem to work. Any help would be greatly appreciated considering this is my first time working with maps. Code below:
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(self.zipCode.text!.capitalized) {
placemarks, error in
if error == nil {
let placemark = placemarks?.first
let lat = placemark?.location?.coordinate.latitude
let lon = placemark?.location?.coordinate.longitude
let center = CLLocationCoordinate2D(latitude: lat ?? 0.0, longitude: lon ?? 0.0)
print("\(self.zipCode.text) lat \(lat) long \(lon)")
let mRegion = MKCoordinateRegion(center: center, span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2))
self.mapView.setRegion(mRegion, animated: true)
}
else{
print("Error finding location \(self.zipCode.text!)")
I would need a little more context to give you an exact answer (like where in the code are you calling this, how often are you doing, what is the string dress value), but as far as I can see the problem could be:
lat or Lon could be nil and would default to 0.0 (?? operator)
all the code is running inside the closure, so it could be a scope problem
Also, taking a quick look at Apple documentation I found the following:
After initiating a forward-geocoding request, do not attempt to initiate another forward- or reverse-geocoding request. Geocoding requests are rate-limited for each app, so making too many requests in a short period of time may cause some of the requests to fail. When the maximum rate is exceeded, the geocoder passes an error object with the value CLError.Code.network to your completion handler.
So it could be an problem with the amount of the requests you are doing in a sort amount of time
I would say that is due to the number of requests made within a small time interval. I experimented with Apple's location framework for a long time and found that a single app can't send multiple geocoding request within short time. If you try so, you will get an error.
I'm relatively new to iOS and Mapbox development. I'm working on an app where a user can freely manipulate a map full of places they have saved.
When they reach a zoom-level that is completely filled by the geography of a city, I would like to display the name of the city which they are viewing in a banner-style view, even if a city label is not within view on the map (as is often the case when zoomed in).
Here's a screenshot of the UI for context.
I'm trying to query the Mapbox tileset for the city name using the following code:
func mapViewRegionIsChanging(_ mapView: MGLMapView) {
let zoomLevel = mapView.zoomLevel
if zoomLevel >= 14.0 {
// layer identifier taken from layer name in Mapbox Studio
let layerIdentifier = "place-city-lg-n"
let screenRect = UIScreen.main.bounds
let cityName = mapView.visibleFeatures(in: screenRect, styleLayerIdentifiers: Set([layerIdentifier]))
print(cityName)
}
I think this code doesn't work because the label is not onscreen at the specified zoom level.
I'm wondering if using visibleFeaturesInRect is the best approach for my need—is there a better way to retrieve city name regardless of visible elements and zoom level?
For this task I'd recommend using MapboxGeocoder from Mapbox. It is for getting information about the city/village.
you can install pod:
pod 'MapboxGeocoder.swift', '~> 0.12'
and use this code:
let geocoder = Geocoder.shared
func mapViewRegionIsChanging(_ mapView: MGLMapView) {
let geocodeOptions = ReverseGeocodeOptions(coordinate: mapView.centerCoordinate)
geocodeOptions.allowedScopes = [.place]
let _ = geocoder.geocode(geocodeOptions) { (placemarks, attribution, error) in
guard let placemark = placemarks?.first else { return }
print(placemark.name)
print(placemark.qualifiedName)
}
}
you can add your conditions and it really helps to solve your task
I have used GeoFire to fetch location based data from my Firebase database. I know if I set less radius in my query then I am able to load data quickly, but my requirement is that I want shorted data based on location, so nearest records shows first, and so on. So I have passed current location in GeoFire query with total earth radius, because I want all data. But I don't how to apply pagination with GeoFire, so in future when more records are available in Firebase database my current implementation will definitely takes more time to load.
Below is the code snipped which I have used to get location based records.
let eartchRadiusInKms = 6371.0
let geoFire = GeoFire(firebaseRef: databaseRef.child("items_location"))
let center = CLLocation(latitude: (self.location?.coordinate.latitude)!, longitude: (self.location?.coordinate.longitude)!)
let circleQuery = geoFire?.query(at: center, withRadius: eartchRadiusInKms)
if CLLocationCoordinate2DIsValid(center.coordinate) {
circleQuery?.observeReady({
let myPostHandle : DatabaseHandle = circleQuery?.observe(.keyEntered, with: { (key: String?, location: CLLocation?) in
// Load the item for the key
let itemsRef = self.databaseRef.child("items").child(key!)
itemsRef.observeSingleEvent(of: .value, with: { (snapshot) in
// Manage Items data
})
})
})
}
So can pagination is possible with GeoFire? Or I have to use some different mechanism, can anyone please advise me on this scenario?
I faced a similar issue and I actually loaded a small radius first, then increased the radius and loaded another chunk of data in a background service. In the completion of the call I reloaded my data in collection view using
collectionView.reloadData()`
Here is how you query in geofire
self.circleQuery = self.geoFire?.query(at: myLocation, withRadius: myRadius)
self.circleQuery?.observe(.keyEntered, with: { (key: String?, location: CLLocation?) in ....
check out this function in geofire documentation. It keeps a track of the new entered locations in background. Now as you want pagination consider an example of tableView, you can just call this on onScroll
myRadius = myRadius + 1000 // or any number to increase radius
As the keyEntered observer is already set so it will return you back the new results. Just add them to your list and update the table / collection view
I posted this issue on GitHub, though it has been over a week and no response from the developers, so hoping to get an answer here.
Using the example code, plus adding a bit to show placemarks returned from ForwardGeocodeOptions, I came up with this testing code:
(Swift 3, Xcode 8)
func mapView(_ mapView: MGLMapView, regionDidChangeAnimated animated: Bool) {
geocodingDataTask?.cancel()
self.outputText.text = ""
// Variables.userLat and Variables.userLng are set through locationManager
let options = ReverseGeocodeOptions(coordinate: CLLocationCoordinate2D(latitude: Variables.userLat, longitude: Variables.userLng))
geocodingDataTask = geocoder.geocode(options) { [unowned self] (placemarks, attribution, error) in
if let error = error {
NSLog("%#", error)
} else if let placemarks = placemarks, !placemarks.isEmpty {
self.resultsLabel.text = placemarks[0].qualifiedName
let foptions = ForwardGeocodeOptions(query: self.inputText.text!)
// To refine the search, you can set various properties on the options object.
foptions.allowedISOCountryCodes = ["US"]
foptions.focalLocation = CLLocation(latitude: Variables.userLat, longitude: Variables.userLng)
let neLat = Variables.userLat + 1.0
let neLng = Variables.userLng + 1.0
foptions.allowedRegion?.northEast = CLLocationCoordinate2D(latitude: neLat, longitude: neLng)
let swLat = Variables.userLat - 1.0
let swLng = Variables.userLng - 1.0
foptions.allowedRegion?.southWest = CLLocationCoordinate2D(latitude: swLat, longitude: swLng)
foptions.allowedScopes = [.address, .pointOfInterest]
let _ = geocoder.geocode(foptions) { (placemarks, attribution, error) in
guard let placemark = placemarks?.first else {
return
}
let coordinate = placemark.location.coordinate
print("\(coordinate.latitude), \(coordinate.longitude)")
self.inputLat.text = coordinate.latitude.description
self.inputLng.text = coordinate.longitude.description
var string = ""
for mark in placemarks! {
if string != "" {
string += "\n"
}
string += mark.qualifiedName
}
self.outputText.text = string
}
} else {
self.resultsLabel.text = "No results"
}
}
}
That gives me a mini-app to test out the data that is returned when I change locations in the Xcode Simulator.
screenshot 2017-07-12 13 54 09
As you can see from this shot, I have centered the map in Jenks, OK (a small town just outside of Tulsa, OK - sort of a 'central US' location.)
When searching for a common place in that area ("Walmart" - which is based in nearby Arkansas, so there are plenty of them around), you can see that only 2 'local' Walmart's come back in the search.
Now, let's move to Bentonville, AR - the home of Walmart......
And, we get two top new results, but the others are the same (and much farther away than Tulsa, OK.....)
We found that if we add the town to the first of the search, the results are much better:
(similar results are true for every search we did - various cities around the US and with other 'common places' like Quiznos (similar results as Walmart when in their home town of Denver, CO...)
As you can see from my code, I tried using the allowedRegion?.northEast and southWest (as I understand it, those should set the search area to about 100 miles around the location, though I'm not sure I set that up right), though no difference was found from this setup (i.e., with/without I get the same results).
The only 'better' results were by putting in the town name along with the 'common' one (though, oddly, different results were returned if the town name was before or after the common one - I didn't check exactly, though I think they are 'best' (i.e., locations are all pretty near) from putting the town name after the common one)
What can I do to get better results without having to tell the user to enter the town name (not a very desirable plan! :)
Thank you in advance for tips - the lookup is a key part of the app (not the test stuff shown in the pictures! ) and users expect to pull up several 'nearby' common places (in this case, we would expect to see all 5 results within something like 20 miles - certainly no more than 100 miles away), so it is an important thing for us that this work much more reliably than we are seeing now.
I created a small messenger app using Parse as my database in swift. I wanted each time any user sends a message only other users within 10 meter to get the notification. The app is working but the push notification is not. I did some research it looks like my code is similar to the one i found but still not working. Please help me. Thank you
var CurrentLocation : PFGeoPoint = PFGeoPoint(latitude: 44.6854, longitude: -73.873) // assume the current user is here
let userQuery = PFUser.query()
userQuery?.whereKey("Location", nearGeoPoint: CurrentLocation, withinMiles: 10.0)
let pushQuery = PFInstallation.query()
pushQuery?.whereKey("username", matchesQuery: userQuery!)
let push = PFPush()
push.setQuery(pushQuery)
push.setMessage(" New message")
push.sendPushInBackground()
Your problem is the line pushQuery?.whereKey("username", matchesQuery: userQuery!). According to the parse docs Warning: This only works where the key’s values are PFObjects or arrays of PFObjects. Read more here: https://parse.com/docs/osx/api/Classes/PFQuery.html#//api/name/whereKey:matchesQuery
Instead change it to chain the query by first performing the first query and then using the strings of the userIds for the second query which is performed inside the first's completionBlock.
Update
Also, just as a side note you are not abiding by the camel case rules of swift and also the rules set out by Parse. You should follow conventions. (See my code for the correct case for keys in Parse and variable names).
Example:
var currentLocation : PFGeoPoint = PFGeoPoint(latitude: 44.6854, longitude: -73.873) // assume the current user is here
let userQuery = PFUser.query()
userQuery?.whereKey("location", nearGeoPoint: currentLocation, withinMiles: 10.0) // Note I changed Location to location
userQuery?.findObjectsInBackground({ results, error in
let usernames = (results as! [PFObject]).map { $0.username }
let pushQuery = PFInstallation.query()
pushQuery?.whereKey("username", containedIn: usernames)
let push = PFPush()
push.setQuery(pushQuery)
push.setMessage("New message")
push.sendPushInBackground()
})
Also note that you may need to change your structure because the docs for PFInstallation.query note that you must use one of three query parameters and you are using none (you might have to save the installation object id to a field on the user. Then instead of making an array with username make an array with the installation object ids and query PFInstallation like that. However, this may still work so try it first, you never know.