how can I show compass/heading on mapkit mapview - iphone

on the iPhone 3GS in the "Maps" app you can click the icon which usually shows your position twice and the blue dot gains what looks like a beam from a headlamp, basically showing you the direction you are facing on the map and rotating the image accordingly.
Is this option available using MapKit MapView ?
I'm know that I can get my heading with something like
- (void)locationManager:(CLLocationManager *) manager didUpdateHeading:(CLHeading *) newHeading {
// If the accuracy is valid, process the event.
if (newHeading.headingAccuracy > 0) {
CLLocationDirection theHeading = newHeading.magneticHeading;
...
}
}
but I don't know how to get that nice headlamp effect in Mapkit and there doesn't seem to be any documentation.
Any ideas?

Adding user tracking mode also helps. I know I am late, but possibly a help to other developers like me :)
self.mapView.userTrackingMode = RMUserTrackingModeFollowWithHeading;

I found a solution:
I rotate the map using the available heading-information with
[mapView setTransform:CGAffineTransformMakeRotation(heading.magneticHeading * M_PI / -180.0)];
Therefore the "beam" always points to the top of the device. I now just display an ImageView on top of the map and change it's position in locationManager:didUpdateToLocation:fromLocation:
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
// scroll to new location
MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(newLocation.coordinate, 2000, 2000);
[self.mapView setRegion:region animated:YES];
// set position of "beam" to position of blue dot
self.headingAngleView.center = [self.mapView convertCoordinate:newLocation.coordinate toPointToView:self.view];
// slightly adjust position of beam
self.headingAngleView.frameTop -= self.headingAngleView.frameHeight/2 + 8;
}
Whereby frameTop and frameHeight are shortcuts for frame.origin.y and frame.size.height.
It is not ideal and sometimes lacks a little bit when the dot changes it's position, but I'm happy with the solution b/c it works.
Have a look at my OpenSource framework MTLocation which does this all (and a lot of other cool Map-related stuff for you):
MTLocation

The rotating logic of the other answers are good, however relying on the location manager delegate methods won't result in a good solution. The "beam" image will rarely be in the same place as the blue dot and will bounce around a lot.
The best solution is to get a reference to the blue dot view, and add the "beam" image as a subview to it. I went with a beam image that was a square, with the beam up top and transparency all around it. Imagine the blue dot being in the center of the image.
// An MKMapViewDelegate method. Use this to get a reference to the blue dot annotation view as soon as it gets added
- (void)mapView:(MKMapView *)aMapView didAddAnnotationViews:(NSArray *)views {
for (MKAnnotationView *annotationView in views) {
// Get the user location view, check for all of your custom annotation view classes here
if (![annotationView isKindOfClass:[MKPinAnnotationView class]])
{
self.userLocationAnnotationView = annotationView;
gotUsersLocationView = YES;
}
}
}
// Adds the 'viewPortView' to the annotation view. Assumes we have a reference to the annotation view. Call this before you start listening to for heading events.
- (void)addViewportToUserLocationAnnotationView {
TTDASSERT(self.userLocationAnnotationView != nil);
if (self.userLocationAnnotationView == nil) {
// No reference to the view, can't do anything
return;
}
if (self.viewPortView == nil) {
self.viewPortView = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:#"map_viewport.png"]] autorelease];
}
[self.userLocationAnnotationView addSubview:self.viewPortView];
[self.userLocationAnnotationView sendSubviewToBack:self.viewPortView];
self.viewPortView.frame = CGRectMake((-1 * self.viewPortView.frame.size.width/2) + self.userLocationAnnotationView.frame.size.width/2,
(-1 * self.viewPortView.frame.size.height/2) + self.userLocationAnnotationView.frame.size.height/2,
self.viewPortView.frame.size.width,
self.viewPortView.frame.size.height);
}

I could think of one method, though I have not implemented but may help you a bit.
Firstly you need an image with a pointer of your choice, this pointer must be pointing Upwards 90degree.
Now in your MapKit off the Current Location Marker.
In your didUpdateHeading use the x and y values to calculate the angle of the direction.
Use this angle to rotate the image of the pointer
Use this rotated image as a Annotation Pin in your map.
You would require to update the position of the pointer frequently.
Please post your suggestions/changes for the above approach.

Swift 3 implementation
mapView.userTrackingMode = MKUserTrackingMode.followWithHeading

Related

Showing Specific Region Using MapKit

I want to know is it possible to show only specific region on map not the full world map using Map Kit.
Like if i want to show Asia map in my application then map kit hides remaining part of the map.
To handle the "map kit hides remaining part of the map" requirement, one thing you can do is create a black polygon overlay that covers the whole world with a cutout over Asia (or wherever you like).
For example, where you initialize the map (eg. in viewDidLoad):
CLLocationCoordinate2D asiaCoords[4]
= { {55,60}, {55,150}, {0,150}, {0,60} };
//change or add coordinates (and update count below) as needed
self.asiaOverlay = [MKPolygon polygonWithCoordinates:asiaCoords count:4];
CLLocationCoordinate2D worldCoords[4]
= { {90,-180}, {90,180}, {-90,180}, {-90,-180} };
MKPolygon *worldOverlay
= [MKPolygon polygonWithCoordinates:worldCoords
count:4
interiorPolygons:[NSArray arrayWithObject:asiaOverlay]];
//the array can have more than one "cutout" if needed
[myMapView addOverlay:worldOverlay];
and implement the viewForOverlay delegate method:
-(MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id<MKOverlay>)overlay
{
if ([overlay isKindOfClass:[MKPolygon class]])
{
MKPolygonView *pv = [[[MKPolygonView alloc] initWithPolygon:overlay] autorelease];
pv.fillColor = [UIColor blackColor];
pv.alpha = 1.0;
return pv;
}
return nil;
}
This looks like this:
If you also want to restrict the user from scrolling beyond Asia or zooming too far out, then you'll need to do that manually as well. One possible way is described in Restrict MKMapView scrolling. Replace theOverlay in that answer with asiaOverlay.
You can specify the region as an MKCoordinateRegion and then tell an MKMapView instance to only show that region using the setRegion and regionThatFits message.
Alternatively you could use the visibleMapRect property instead of the region. This might better fit your needs.
In short read the MKMapView Class Reference document from Apple.
Lifting from some code I've done in the past that assumes a mapView and a given location called locationToShow I used an MKCoordinateRegion.
- (void) goToLocation {
MKCoordinateRegion region;
MKCoordinateSpan span;
span.latitudeDelta=0.01;
span.longitudeDelta=0.01;
region.span=span;
region.center=locationToShow;
[mapView setRegion:region animated:TRUE];
[mapView regionThatFits:region];
}

Invalid MKMapView region in iOS 5

MKMapView returns an invalid region under iOS 5 -- latitude + latitudeDelta/2 is over 100 and shouldn't be over 90.
Has anyone seen this problem?
Steps to Reproduce:
Create an MKMapView
Log the mapView.region from within the regionDidChangeAnimated delegate method
Zoom out the map as far as possible and drag it to the right, so the view is scrolled to the top/left
Expected Results:
In iOS 4, the mapView.region is reasonable:
lat=2.202047 lon=-67.500000 latDelta=165.698164 lonDelta=225.000000
In iOS 5, however, the mapView.region is out of bounds:
lat=17.978733 lon=-67.500000 latDelta=165.698164 lonDelta=225.000000
Latitudes should be within the -90 to 90 range. However, in iOS 5, lat + latDelta/2 is 100.827815. That is not possible. While I can clamp the values at +/- 90, the offset difference is causing problems with our overlays.
Regression:
Does not happen in iOS 4.3. Happens regularly in iOS 5. Screen dumps of the map views look identical even though the center latitude is 15 degrees off.
Notes:
Project file and screen dumps can be downloaded here.
This seems to be an adequate workaround. Rather than reading the mapView.region property, call this method instead:
#implementation MKMapView(fixedRegion)
-(MKCoordinateRegion) fixedRegion_
{
// this call is broken on iOS 5, as is the region property, so don't use them
// return( [self convertRect:self.bounds toRegionFromView:self] );
CLLocationCoordinate2D topLeft = [self convertPoint:CGPointZero toCoordinateFromView:self];
CLLocationCoordinate2D bottomRight = [self convertPoint:CGPointMake(self.bounds.size.width, self.bounds.size.height) toCoordinateFromView:self];
MKCoordinateRegion region;
region.center.latitude = (topLeft.latitude + bottomRight.latitude)/2;
region.center.longitude = (topLeft.longitude + bottomRight.longitude)/2;
region.span.latitudeDelta = fabs( topLeft.latitude - bottomRight.latitude );
region.span.longitudeDelta = fabs( topLeft.longitude - bottomRight.longitude );
return region;
}
#end
Now one could argue (correctly!) that this code isn't 100% correct either because the center value of a Mercator projection in lon/lat isn't really halfway between the top and bottom, but since this matches iOS 4 functionality and keeps the values within the legal range for the map, it works for me.
By using the MKMapView+ZoomLevel category, you won’t have to bother setting the region at all.
here is very good tutorial on the same
http://troybrant.net/blog/2010/01/set-the-zoom-level-of-an-mkmapview/
or
http://mayurbirari.wordpress.com/2011/02/07/how-to-access-mkmapkit-in-iphone/
After you perform the zoom/pinch operation try to load the region in
-(void) mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated
instead in
regionDidChangeAnimated.
hope this will help..:)

To show heading direction on blue pulsating dot(user's current location) without loosing accuracy circle

I have a CLLocation manager as follows
myLocation = [[CLLocationManager alloc] init];
myLocation.desiredAccuracy = kCLLocationAccuracyBestForNavigation ;
myLocation.delegate=self;
if device supports heading information then method is called to update heading
if([CLLocationManager headingAvailable])
[myLocation startUpdatingHeading];
and change in heading are retrieved from following method
- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading
{
[mymap setTransform:CGAffineTransformMakeRotation(-1 * newHeading.trueHeading * 3.14159 / 180)];
}
I can use this information (newHeading.trueHeading) to rotate the map. But how can I show a sector that will depict heading direction with given or forced accuracy, on blue dot (indicator of users current location). I am able to use custom image for blue dot but it looses light "accuracy circle" then. Is there any way to get a reference to the blue dot view and modify it, so that accuracy circle is not lost.
Also rotation of annotations can be avoided with negative rotation but when map is rotating the "Title" and "subTitle" of annotations gets extended beyond view of visible screen. Is there any way to limit them to map's view.
Thanks for any help in advance
You can use this project as a guide. It rotates the whole mapView, but you can extrapolate it to your view.
EDIT: If you want to customize the userLocation annotation you have to implement viewForAnnotation and check if the given annotation is MKUserLocation and return a custom annotationView configured as you want.
- (MKAnnotationView *)mapView:(MKMapView *)theMapView viewForAnnotation:(id <MKAnnotation>)annotation
{
if ([annotation isKindOfClass:[MKUserLocation class]]) {
//Configure your custom view to point to the current heading and return it.
}
}
- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading {
if (newHeading.headingAccuracy > 0) {
// [newHeading retain];
float heading = newHeading.trueHeading >= 0 ? newHeading.trueHeading : newHeading.magneticHeading;
[mymap setTransform:CGAffineTransformMakeRotation(heading)];
}
}

Rotating only the MapView's content

I am trying to rotate a map view in my app according the the user's current route. (I don't like to use the built in compasse because only 3GS uses it and it suffers too much interference from other machines, i.e. your own car.).
the CGAffineTransformMakeRotation method will rotate the Whole Map View, so the Google Logo will not be in the lower right of the screen anymore. Also all Annotation Views will rotate and look weird on the App.
Does anyone knows how to rotate just the content (streets drwaings) of the MkMapView?
Thanks
Here is how I'm rotating the MapView (see Rotate MapView using Compass orientation):
[self.mapView setTransform:CGAffineTransformMakeRotation(-1 * currentHeading.magneticHeading * 3.14159 / 180)];
Then to keep the orientation of the annotation views with the top of the device:
for (MKAnnotation *annotation in self.mapView.annotations) {
MKAnnotationView *annotationView = [self.mapView viewForAnnotation:annotation];
[annotationView setTransform:CGAffineTransformMakeRotation(currentHeading.magneticHeading * 3.14159 / 180)];
}
If you want the point of rotation of the annotationViews to be say the bottom center, I believe you would set each view's anchorPoint as follows:
annotationView.layer.anchorPoint = CGPointMake(0.5, 1.0);
I'm still having some issues with the annotationView position changing when I do this though (probably same problem as stackoverflow question: changing-my-calayers-anchorpoint-moves-the-view).

Rotate MapView using Compass orientation

Is it possible to have an embedded MKMapView rotate to always face the direction the iPhone is facing? Basically I want to mimic the Map app rotation feature on my own app.
I see that the iPhone SDK does not expose the functionality. However, I wonder if it would work to rotate the entire view using CGAffineTransformMakeRotate. Would it affect tapping and zooming? Is there a better way?
To rotate the mapView but not the annotations you could use the following code to compensate for the maps rotation.
- (void)locationManager:(CLLocationManager *)manager
didUpdateHeading:(CLHeading *)newHeading
{
double rotation = newHeading.magneticHeading * 3.14159 / 180;
CGPoint anchorPoint = CGPointMake(0, -23); // The anchor point for your pin
[mapView setTransform:CGAffineTransformMakeRotation(-rotation)];
[[mapView annotations] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
MKAnnotationView * view = [mapView viewForAnnotation:obj];
[view setTransform:CGAffineTransformMakeRotation(rotation)];
[view setCenterOffset:CGPointApplyAffineTransform(anchorPoint, CGAffineTransformMakeRotation(rotation))];
}];
}
Another sollution is using a new method that has been added in iOS 5 to MKMapView.
Take a look at: http://developer.apple.com/library/ios/#documentation/MapKit/Reference/MKMapView_Class/MKMapView/MKMapView.html
- (void)setUserTrackingMode:(MKUserTrackingMode)mode animated:(BOOL)animated;
I can confirm that it works fine. Here's the code that I'm using:
[mapView setTransform:CGAffineTransformMakeRotation(-1 * currentHeading.magneticHeading * 3.14159 / 180)];
mapView is my MKMapView instance
Simple solution in swift 3.0. Make sure to put the line in mapViewDidFinishLoadingMap or it will ignore it
public func mapViewDidFinishLoadingMap(_ mapView: MKMapView)
{
mapView.setUserTrackingMode(.followWithHeading, animated: false)
}
If you don't want the map to center on the user location you might do something like this
public func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading)
{
if(CLLocationCoordinate2DIsValid(mapView.centerCoordinate))
{
mapView.camera.heading = newHeading.trueHeading
}
}
If you look in the 3.0 iPhone Application Programming Guide under Device Support you'll find information on the magnetometer (aka compass). Once you start getting heading information (through the didUpdateHeading method) you should be able to get the compass data then use that to calculate the proper rotation transform value to apply to the map view.
Not sure if it handles zooming. In the standard map application I've noticed that compass heading tracking stops as soon as you start pinch-zooming.
Also, keep in mind that location directions are in degrees whereas transform rotation angles are in radians.
if you use a navigationVontroller try this:
//For Left button
MKUserTrackingBarButtonItem *buttonItem = [[MKUserTrackingBarButtonItem
alloc]initWithMapView:_mapView];
self.navigationItem.leftBarButtonItem = buttonItem;
// For Right Button
MKUserTrackingBarButtonItem *buttonItem = [[MKUserTrackingBarButtonItem
alloc]initWithMapView:_mapView];
self.navigationItem.rightBarButtonItem = buttonItem;