How to simulate a user driving a route in a MKMapView? - iphone

I need to simulate how my application will look when a user is driving around for a demo. I have a MKMapView, how can I simulate the look of a user driving around which will use the map.userLocation functionality, which obviously will not be available in the demo.
Thanks!

No way to simulate in iPhone simulator. You'll need to load it onto your device and move around.

Well I got something going, I just did essentially this
- (void)moveIcon:(MKAnnotationView*)locationView toLocation:(CLLocation*)newLoc
{
LocationAnnotation* annotation = [[[LocationAnnotation alloc] initWithCoordinate:newLoc.coordinate] autorelease];
[locationView setAnnotation:annotation];
[map setCenterCoordinate:newLoc.coordinate animated:YES];
}
Then I call this guy in a loop between all of my vertices with a slight delay. Works quite qell.

I'm not an iPhone dev expert, but how does the map view receive the coordinates? If it's through a function that calls the CoreLocation API, could you possibly just write a function that randomly generates longitude and latitude values at a certain time interval and have your map view pull the coordinates from there instead? Just a thought.

You could also check out iSimulate which claims to be able to simulate several features only available on the iPhone in the iPhone simulator include CoreLocation. I have not tried this myself so your mileage may vary.

In order to simulate driving you'll need to establish 2 basic functionalities:
Reading CLLocations from an archive (which you'd log during the drive test with a device). Ideally you'll do this based on the timestamps on the locations, i.e. reproducing the exact same location updates which were received during the drive test.
Updating your MKAnnotationView's position on the map based on the locations read from log.
For part 1, take a look at CLLocationDispatch, a handy class which provides archiving/unarchiving of CLLocations and dispatches them to one or more listeners (using CLLocationManagerDelegate protocol).
For part 2, take a look at Moving-MKAnnotationView.

I found a better way would be to subclass MKUserLocation:
class SimulatedUserLocation: MKUserLocation {
private var simulatedCoordinate = CLLocationCoordinate2D(latitude: 39, longitude: -76)
override dynamic var coordinate: CLLocationCoordinate2D {
get {
return simulatedCoordinate
}
set {
simulatedCoordinate = newValue
}
}
}
Then add it as an annotation mapView.addAnnotation(SimulatedUserLocation()). (You might also want to hide the real location first mapView.showsUserLocation = false)
iOS would render the annotation exactly like the real user location.
dynamic is used on the property so that changing coordinate triggers KVO and moves it on the map.

The answer is NO. Then, how about adding an abstraction layer between your code and MKMapKit? You can do xUnit tests for your objective.

Related

HERE API:Swift. Wierd behaviour : Loops in a route - Creating a route based on GPS trace creates loops. / passthrough waypoints

im trying to create a route that follows a gps trace i provide.
The gps trace is cleaned up and it has no loops in it and is in correct order.
I checked it with other services.
It has 1920 points.
You can find the trace here GPX Files
Sadly if i create a route based on provided sdk example (github) i get loops in my path.
I was hoping you could help me to solve following problems:
how do i avoid loops while creating route by using HERE ios Swift SDK
how do i set route options is such way to follow provided point array and not create a fastest or balanced route.
Since i could not find those functions in Ios sdk i used additional REST API to filter the route a bit to remove all points that were not matched correctly according to here maps... before drawing the route.. ie everything with low probability, warnings, big distance to the road... yet the result is still not good. Here is a cleaned up file.. the file is being created after the original was maped / run once through HERE Maps. In this file all points that have low confidence or produce warnings or have big distance to original points .. are removed. This is the one i use to create a route and it still have the same issues like loops and weird turns.
Thank you very much in advance!
BR.
So far i have this code:
private lazy var router = NMACoreRouter()
#objc func do_routing_stuff( gps_trace :[NMAWaypoint]) {
var stops = [Any]()
stops = gps_trace
let routingMode = NMARoutingMode(routingType: .fastest,
transportMode: .car,
routingOptions: .avoidHighway)
// Trigger the route calculation
router.calculateRoute(withStops: stops ,
routingMode: routingMode)
{ [weak self] routeResult, error in
guard error == .none else {
self?.showMessage("Error:route calculation returned error code \(error.rawValue)")
return
}
guard let result = routeResult, let routes = result.routes, routes.count > 0 else {
self?.showMessage("Error:route result returned is not valid")
return
}
// Let's add the 1st result onto the map
self?.route = routes[0]
self?.updateMapRoute(with: self?.route)
// self?.startNavigation()
}
}
private func updateMapRoute(with route: NMARoute?) {
// remove previously created map route from map
if let previousMapRoute = mapRoute {
mapView.remove(mapObject:previousMapRoute)
}
guard let unwrappedRoute = route else {
return
}
mapRoute = NMAMapRoute(unwrappedRoute)
mapRoute?.traveledColor = .clear
_ = mapRoute.map{ mapView?.add(mapObject: $0) }
// In order to see the entire route, we orientate the
// map view accordingly
if let boundingBox = unwrappedRoute.boundingBox {
geoBoundingBox = boundingBox
mapView.set(boundingBox: boundingBox, animation: .linear)
}
}
in comparison same route presented with leaflet maps.
I believe the problem you have is that you are feeding the Routing API a large number of waypoints, all of which are in close proximity to each other.
You have almost 2000 waypoints in your GPX file (and ~1300 in your cleaned one). Each of these waypoints is less than 10 meters distance from their closest neighbors. This is not the type of data that the Routing API is really designed to work with.
I've experimented with your GPX Trace and I have come up with the following solution: simply skip a bunch of coordinates in your trace.
First, clean up your trace using the Route Matching API (which I believe you have been doing).
Second, pick the first trkpt in the GPX file as your first waypoint for the Routing call. Then skip the next 20 points. Pick the following trkpoint as the second waypoint. Repeat this until you are at the end of the file. Then add the last trkpt in the trace as the final waypoint.
Then call the Routing API and you should get a good match between your trace and your route, without any loops or other weird routing artefacts.
Some notes:
I have picked 20 as the number of traces to skip, because this would put about 200m in between each waypoint. That should be close enough to ensure that the Routing API does not deviate too much from the traced route. For larger traces you may wish to increase that number. For traces in urban environments with lots alternate routes, you may want to use a smaller number.
It's important to clean the data with the Route Matching API first, to avoid picking outliers as waypoints.
Also, you may not wish to use the "avoidHighways" option. Given your use case, there doesn't seem to be a benefit and I could see it causing additional problems.
By now you probably worked it out, but your waypoints are likely landing on bridges or tunnels that are along your route but not on the road you want. I.e. the waypoint is intended to be on the road under the bridge but the routing engine perceives that you want to drive on the bridge.
The routing engine is looping around those roads to drive you on that waypoint on the bridge or in the tunnel.
There is no simple solution to this that I have found.

Pulling data from a GKLeaderboard

Okay, so I have spent a large amount of time searching the internet for help on this to no success, so I would like some help.
I am making a game with SpriteKit, and I have decided to implement my own leaderboard style, rather than the clunky Game Center default. I have managed to log the user into GC, but cannot find the correct (and working) Swift 3 code for pulling information from the leaderboard. I want to pull the top 10 score, along with the current user score (if they aren't already in the top 10). The information I would like from them is position, username and score.
I know this is a fairly simple concept, but every tutorial online either uses the default GC view, or is extremely old/outdated code which no longer works. I just need to know how to pull this information from the leaderboard, then I can process it all myself!
Thank you for any help in advance!
It seems like Apple doesn't have proper example code in Swift, but here's a Swift version loosely based on their Objective-C example:
let leaderboard = GKLeaderboard()
leaderboard.playerScope = .global
leaderboard.timeScope = .allTime
leaderboard.identifier = "YourIdentifier"
leaderboard.loadScores { scores, error in
guard let scores = scores else { return }
for score in scores {
print(score.value)
}
}
Note, this is just a translation from Apple's Objective-C code, and I haven't tested it with real data.

How to best load Locations for MapView from Webserver

I have like 2000 locations in a web database which a user should be able to select on a Map. I can ask the web database to give me only a certain number of locations originating from a current location.
To make everything smooth and elegant I would first instantiate MKMapView, start CLLocationManager and wait until I get a didUpdateLocations. Then I would try to get my data from a Database with a completion handler.
Should I
a) get all data at once
b) get the data in little pieces or chunks ?
What it the best way?
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
self.gotoCurrentLocation()
if let userLocation = manager.location {
GroundHelper.getAllGroundLocations(userLocation) { self.handleWaypoints($0!) }
}
}
private func handleWaypoints(grounds: [Ground]) {
mapView.addAnnotations(grounds)
}
// MARK: - Helper Methods
typealias GPXCompletionHandler = ([Ground]?) -> Void
class func getAllGroundLocations(userlocation: CLLocation, completionHandler: GPXCompletionHandler) {
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0), { ()->() in
var results = RestApiManager.sharedInstance.getGPS(userlocation, limit: 50)
// return first 50 results
dispatch_async(dispatch_get_main_queue(), {
var grounds = [Ground]()
for result in results {
let (_,jsonGround) = result
let ground = Ground(json:jsonGround)
grounds.append(ground)
}
completionHandler(grounds)
})
// get the rest
results = RestApiManager.sharedInstance.getGPS(userlocation)
// return them
dispatch_async(dispatch_get_main_queue(), {
var grounds = [Ground]()
for result in results {
let (_,jsonGround) = result
let ground = Ground(json:jsonGround)
grounds.append(ground)
}
completionHandler(grounds)
})
})
}
Getting all data at once is not scalable. You might make it work on a timely manner with 2000 entries, but what if the data set grows? Can you handle 3000? 5000? 10000 annotations?
Getting your data in chunks, returning only entries near the location the map is centered in, and displaying those entries as the user moves around the map makes more sense. However, this approach is very slow, as there is usually a long delay between the user dragging the map and the annotations appearing on screen (network requests are slow in nature).
Thus, the recommended approach for a great user experience is to cache the results locally. You can do this if you have a local database (Core Data, for example) or you could do this with NSCache.
With this approach, you will be hitting the server with new requests as the user moves around the map, but the amount of results returned can be limited to 20, 50 or 100 (something that gets you the highest amounts of data while being very responsive).
Next, you would be rendering all annotations from your cached results on the map, so the number of annotations will grow as the user moves around the map.
The guys from http://realm.io have a pretty nice video that explains this approach: https://www.youtube.com/watch?v=hNDNXECD84c While you do not need to use their mobile database (Realm), you can get the idea of the application architecture and design.
First of all, you should think if 2000 annotations are going to fit at the same time on the MKMapView. Maybe you should try to only recover the locations that fit in the map you are presenting.
After that, in your code, you are adding annotations everytime didUpdateLocations is called. ¿Are you removing the older annotations in any other place?. If the annotations are not changing, and are on the map, you should't be adding them again.
I think that a good approach should be:
1.- First time didUpdateLocations is called: ask your web service for the annotations that fit in a distance equal to two times the area that is showing your map. Save that location as locationOrigin, save that distance as distanceOrigin
2.- Next time didUpdateLocations is called: If the distance moved from current location to locationOrigin is equal to half the distanceOrigin. You should query the web service again, update your 'Origin' variables, and add only the new annotations to the mapView.
3.- If regionWillChange is called (MKMapViewDelegate): the user is zooming, moving the map, or rotating the device.... the simple approach is reloading the annotations as if we were on step 1. A more smart approach is adding a gesture recognizer to detect if the user is zooming in (in that case the annotations don't change), or zooming out or moving (the annotations may change)
Load them in chunks near the current user locations, if user zoom out load another chunk, if user move the map load another chunk and so on. this way your app would scale better if your annotations become more and more like 1M annotations.
I have actually written an app that can place 7000 annotations on an MKMapView with no performance problems... and that's on an iPad 1. You really don't need to worry about the map.
The only limiting factor I can think of is how long the network call would take and whether that will be a problem. In my case, I was pulling addresses out of the contacts database, geocoding them and then storing the lat-longs in core data. Yes geocoding 7000 addresses takes forever, but once you have the lat-longs putting them on the map is easy stuff and the map code can handle it just fine...

Displaying google direction api response on Map

I am using google api V3 in my iOS App,
I requested to google api direction service, by http GET method, I got a very large json response, I got many alternate routes from origin to destination, now I want to show the route of each alternative on map, how should I done this ? is there any need to integrate google sdk in iOS, or can i use webView only, please help me, and suggest the simplest way.
Thanks in advance
you do not need to include any google SDK to draw the route over map. Look at the following classes.
MKPolyline
MKPolyLineview
The google map api Direction service will give you the legs(coordinates) to draw the route between two specific points.
As you already have the json response from the Direction api now you have to parse the json and get all the legs point from the json to create Coordinate array.
Jsonkit for parsing
These points may be or mostly encripted. How to decode the Google Directions API polylines field into lat long points in objective-C for iPhone?
If you have the coordinates array then first you have to create a CLLocationCoordinate2D array like following:
CLLocationCoordinate2D *pointArr = malloc(sizeof(CLLocationCoordinate2D) * [your_CoordinateArray count]);
for(int i = 0; i < [your_CoordinateArray count]; i++){
pointArr[i] = [your_CoordinateArray objectAtIndex:i];
}
Then you have to add the polyline to your map
MKPolyline *line = [MKPolyline polylineWithCoordinates:pointArr count:[your_CoordinateArray count]];
if (nil != line)
{
[your_Map addOverlay:line];
}
Then you have to implement the following map delegate:
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay
{
MKOverlayView* overlayView = nil;
lineView = [[MKPolylineView alloc] initWithPolyline:overlay];// lineView is an ivar, object of MKPolylineView
lineView.fillColor = [UIColor greenColor];
lineView.strokeColor = [UIColor greenColor];
lineView.lineWidth = 10.0;
lineView.alpha = 0.9;
overlayView = lineView;
return overlayView;
}
Now, if you want to show two different routes between two points then you have to create two different array of coordinates and apply the above method for both Arrays
Hope this will help you.
There is no need per say to integrate google sdk and you can simply use webView since I believe the webView gives you alternative views, but the user interface is much cleaner in the SDK than online. But at this moment, I don't believe the SDK allows other apps to show the alternative routes from what I've used of the new SDK. The code in maps.google.com is now considerably different from that in
the API and has access to different services. In particular, it has a large external module, mod_suggest.js, that doesn't exist in the API. But you may be able to implement it by
1) Throw the request at maps.google.com, and let it do the suggestions,
i.e. open a new browser window and pass it something like
http://www.google.com/maps?f=d&source=s_d&saddr=Jurong+West,+Singapore&da
ddr=Ang+Mo+Kio,+Singapore&hl=en
2) Just display the normal route and the avoid highways route.
3) Try to guess sensible waypoints to add in the middle of he route that
might lead to reasonable alternatives. That's not easy unless you have a
database of highways or highway intersections, and can look for such an
intersection that's somewhere between your start and end points (for
some approximate value of "between").
There may be problems using lat/lng coordinates for the waypoints,
particularly if there's a dual carriageway involved. If the coordinates
are for a point on the wrong carriageway, GDirections will drive a
considerable distance out of your way so that you visit the wrong side
of the road. But even using street names you may well get strange kinks
near the waypoints, like this:
from: Jurong West, Singapore to: Ayer Rajah Expy singapore to: Ang Mo
Kio, Singapore
Perhaps the only way to really deal with that is to include points on
both sides of a dual carriageway, and well clear of roads that cross
underneath or overhead, and then try to filter out the ones that are
silly.
Consider these examples
from: Jurong West, Singapore to: 1.32558,103.740764 to: Ang Mo Kio,
Singapore
from: Jurong West, Singapore to: 1.32582,103.740764 to: Ang Mo Kio,
Singapore
One of those adds 7 minutes to the trip by a complicated excursion to
visit the wrong side of the road.
Writing code to filter out silly routes isn't easy. As a first
approximation, you could discard routes that have estimated times that
are more than, say, 10% longer than the initial route. And you can also
compare the durations for pairs of points on opposite sides of a dual
carriageway and always discard the slower one.
Writing code to discard duplicate routes isn't easy either. For example,
a point on Bukit Timah Expy or Kranji Expy might create a route that's a
duplicate of Google's Seletar Expy suggestion.
You might want to look at: http://www.geocodezip.com/example_geo2.asp?waypts=yes&addr1=Jurong+West,+Singapore&addr2=Ang+Mo+Kio,+Singapore&waypt=Ayer+Rajah+Expy+singapore&geocode=1&geocode=2
But that may not be a legal implementation since the API does not allow it. Hope this helps.

How can I group MKAnnotations automatically regarding zoom level?

if the user zooms out on a MKMapView, i want MKAnnotations which are near to each other automatically grouped into one "group" annotation.
if the user zooms back in, the "group" annotation should be split again to the unique/original annotations.
apple does this already in the iOS 4 Photos.app
is there a common, "predefined" way to do this?
Its normal working with more than 1500 annotations on the map:
-(void)mapView:(MKMapView *)mapView_ regionDidChangeAnimated:(BOOL)animated
{
NSMutableSet * coordSet = [[NSMutableSet alloc] init];
for(id<MKAnnotation> an in mapView_.annotations)
{
if([an isKindOfClass:[MKUserLocation class]])
continue;
CGPoint point = [mapView_ convertCoordinate:an.coordinate toPointToView:nil];
CGPoint roundedPoint;
roundedPoint.x = roundf(point.x/10)*10;
roundedPoint.y = roundf(point.y/10)*10;
NSValue * value = [NSValue valueWithCGPoint:roundedPoint];
MKAnnotationView * av = [mapView_ viewForAnnotation:an];
if([coordSet containsObject:value])
{
av.hidden = YES;
}
else
{
[coordSet addObject:value];
av.hidden = NO;
}
}
[coordSet release];
}
That's a brilliant idea. I'm working on a similar app, I hope you don't mind if I als implement the concept :).
To answer your question to the best of my own ability, no, I don't think there is a predefined way to do this.
The best way I can think of to do it (after looking at the iOS4 photos app), is to make use of the mapView:regionDidChangeAnimated: delegate method. Any time the user scrolls/zooms, this method will be called.
Inside that method, you could have some quick geometry math to determine whether your points are "close enough" to consider merging. Once they're "merged", you'd remove one or both annotations, and put another annotation back in the same place that is a reference to both (you could make an AnnotationCluster class very easily that could conform to MKAnnotation but also hold an NSArray of annotations, and also contain methods for "breaking out" or "absorbing" other annotations and/or AnnotationCluster instances, etc).
When I say "quick geometry math", I mean the distance of the two points relative to the span of the map, and taking their relative distance as a percentage of the span of the whole map.
Where that would get tricky is if you had hundreds of annotations, as I can't off-hand think of a good way to implement that w/o a double loop.
What do you reckon?
This project does something interesting. Though, have a look at reported issues before changing too many things in your code. Because it could be not good enough for your needs yet.
I personnaly ended up implementing this