37

Want to center MKMapView on a point N-pixels below a given pin (which may or may not be visible in the current MapRect).

I've been trying to solve this using various plays with -(CLLocationCoordinate2D)convertPoint:(CGPoint)point toCoordinateFromView:(UIView *)view to no success.

Anyone been down this road (no pun intended)?

enter image description here

eric
  • 4,863
  • 11
  • 41
  • 55

8 Answers8

86

The easiest technique is to just shift the map down, say 40% from where the coordinate would be, taking advantage of the span of the region of the MKMapView. If you don't need actual pixels, but just need it to move down so that the CLLocationCoordinate2D in question is near the top of the map (say 10% away from the top):

CLLocationCoordinate2D center = coordinate;
center.latitude -= self.mapView.region.span.latitudeDelta * 0.40;
[self.mapView setCenterCoordinate:center animated:YES];

If you want to account for rotation and pitch of the camera, the above technique may not be adequate. In that case, you could:

  • Identify the position in the view to which you want to shift the user location;

  • Convert that to a CLLocation;

  • Calculate the distance of the current user location from that new desired location;

  • Move the camera by that distance in the direction 180° from the current heading of the map's camera.

E.g. in Swift 3, something like:

var point = mapView.convert(mapView.centerCoordinate, toPointTo: view)
point.y -= offset
let coordinate = mapView.convert(point, toCoordinateFrom: view)
let offsetLocation = coordinate.location

let distance = mapView.centerCoordinate.location.distance(from: offsetLocation) / 1000.0

let camera = mapView.camera
let adjustedCenter = mapView.centerCoordinate.adjust(by: distance, at: camera.heading - 180.0)
camera.centerCoordinate = adjustedCenter

Where CLLocationCoordinate2D has the following extension:

extension CLLocationCoordinate2D {
    var location: CLLocation {
        return CLLocation(latitude: latitude, longitude: longitude)
    }

    private func radians(from degrees: CLLocationDegrees) -> Double {
        return degrees * .pi / 180.0
    }

    private func degrees(from radians: Double) -> CLLocationDegrees {
        return radians * 180.0 / .pi
    }

    func adjust(by distance: CLLocationDistance, at bearing: CLLocationDegrees) -> CLLocationCoordinate2D {
        let distanceRadians = distance / 6_371.0   // 6,371 = Earth's radius in km
        let bearingRadians = radians(from: bearing)
        let fromLatRadians = radians(from: latitude)
        let fromLonRadians = radians(from: longitude)

        let toLatRadians = asin( sin(fromLatRadians) * cos(distanceRadians)
            + cos(fromLatRadians) * sin(distanceRadians) * cos(bearingRadians) )

        var toLonRadians = fromLonRadians + atan2(sin(bearingRadians)
            * sin(distanceRadians) * cos(fromLatRadians), cos(distanceRadians)
                - sin(fromLatRadians) * sin(toLatRadians))

        // adjust toLonRadians to be in the range -180 to +180...
        toLonRadians = fmod((toLonRadians + 3.0 * .pi), (2.0 * .pi)) - .pi

        let result = CLLocationCoordinate2D(latitude: degrees(from: toLatRadians), longitude: degrees(from: toLonRadians))

        return result
    }
}

So, even with the camera pitched and at a heading other than due north, this moves the user's location (which is centered, where the lower crosshair is) up 150 pixels (where the upper crosshair is), yielding something like:

enter image description here

Obviously, you should be conscious about degenerate situations (e.g. you're 1 km from the south pole and you try to shift the map up 2 km meters; you're using a camera angle pitched so far that the desired screen location is past the horizon; etc.), but for practical, real-world scenarios, something like the above might be sufficient. Obviously, if you don't let the user change the pitch of the camera, the answer is even easier.


Original answer: for moving the annotation n pixels

If you have a CLLocationCoordinate2D, you can convert it to a CGPoint, move it x pixels, and then convert it back to a CLLocationCoordinate2D:

- (void)moveCenterByOffset:(CGPoint)offset from:(CLLocationCoordinate2D)coordinate
{
    CGPoint point = [self.mapView convertCoordinate:coordinate toPointToView:self.mapView];
    point.x += offset.x;
    point.y += offset.y;
    CLLocationCoordinate2D center = [self.mapView convertPoint:point toCoordinateFromView:self.mapView];
    [self.mapView setCenterCoordinate:center animated:YES];
}

You can call this by:

[self moveCenterByOffset:CGPointMake(0, 100) from:coordinate];

Unfortunately, this only works if the coordinate is visible before you start, so you might have to go to the original coordinate first, and then adjust the center.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • If anyone needs to offset the map in pixels please read the original edits of this answer – MK Yung Jun 12 '15 at 04:02
  • 1
    @Rob Can you help me with this MKMapView zoom/centre question please: http://stackoverflow.com/questions/30831318/programatically-move-mkmapview-ios I would really appreciate it if you can :) – Supertecnoboff Jun 18 '15 at 06:02
  • This is a superb answer, really helped me too :) – Supertecnoboff Jun 20 '15 at 12:26
  • 1
    The only issue with this answer is that it assumes the map is oriented with North pointing up. This is okay for some scenarios like the initial view of the map. However, if you the user could orient the map differently (i.e. rotate the map 90 degrees and have East pointing up) before your set the center, it will center it to the right. This is because latitude is north-south data. In order to use this same algorithm, you'd need to calculate the rotation and adjust both the latitude and longitude depending on the orientation. – dustinrwh Dec 03 '15 at 21:09
  • @Jaro - First, I'm not multiplying coordinates. I'm subtracting a fraction of a span (which if you're pointing north, is fine). Second, regarding degenerate situations, the North Pole is not the problem, but rather the South Pole. But that degenerate situation is not just a mathematical problem, but functional one, too (e.g. what does it mean to shift map up 2km when you're only one 1km from South Pole?!). Regardless, I've added another alternative that not only handle's dustinrwh's simple non-north scenario, but the more complicated situation where the the map view's camera is pitched. – Rob Mar 04 '17 at 21:33
7

For Swift:

import MapKit

extension MKMapView {

func moveCenterByOffSet(offSet: CGPoint, coordinate: CLLocationCoordinate2D) {
    var point = self.convert(coordinate, toPointTo: self)

    point.x += offSet.x
    point.y += offSet.y

    let center = self.convert(point, toCoordinateFrom: self)
    self.setCenter(center, animated: true)
}

func centerCoordinateByOffSet(offSet: CGPoint) -> CLLocationCoordinate2D {
    var point = self.center

    point.x += offSet.x
    point.y += offSet.y

    return self.convert(point, toCoordinateFrom: self)
}
}
temp
  • 639
  • 1
  • 8
  • 22
srstomp
  • 328
  • 5
  • 12
  • i set custom image to user current location and i want it to be 20 px right above (center) so where i need to call this method , could you help me outt – Shobhakar Tiwari Dec 07 '16 at 19:06
  • could you guide me for this , i need little bit clarification on this – Shobhakar Tiwari Dec 08 '16 at 17:15
  • Hi @ShobhakarTiwari, off course. I'll get back to you tomorrow. Busy day at work and of to sleep now. You're my first item on my list tomorrow. :) – srstomp Dec 08 '16 at 23:56
  • `func centerMapOnLocation(coordinate: CLLocationCoordinate2D) { var region = MKCoordinateRegion(center: coordinate, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)) region.center = coordinate mapView.setRegion(region, animated: true) mapView.moveCenterByOffSet(CGPoint(x: 0, y: -30), coordinate: coordinate) }` – srstomp Dec 09 '16 at 14:04
  • Let me check , appreciate your effort – Shobhakar Tiwari Dec 09 '16 at 14:05
  • @ShobhakarTiwari Call centerMapOnLocation from anywhere in your class – srstomp Dec 09 '16 at 14:05
  • yes thanks , it just set pin at the same position ( i set -130) but everytime user zoom it changes its position , really appreciate your effort for quick reply – Shobhakar Tiwari Dec 09 '16 at 19:12
6

The only way to reliably do this is to use the following:

- (void)setVisibleMapRect:(MKMapRect)mapRect edgePadding:(UIEdgeInsets)insets animated:(BOOL)animate

In order to do that given a map region you want to center on, you have to convert the map region to a MKMapRect. Use the edge padding for the pixel offset, obviously.

See here for that: Convert MKCoordinateRegion to MKMapRect

Comment: I find it rather strange that that's the only way to do it, given that MKMapRect is not something one normally uses with an MKMapView - all the conversion methods are for MKMapRegion. But, ok, at least it works. Tested in my own project.

Community
  • 1
  • 1
n13
  • 6,843
  • 53
  • 40
  • @n13 could you elaborate it little bit more , i have user location pin (with custom pin ) i want it to be at the bottom of map ( from bottom 20 px) how can i do it ? any help is highly appreciated – Shobhakar Tiwari Dec 08 '16 at 19:36
2

One easy solution is that you make the frame of your map view larger than the visible area. Then position your pin in the center of the map view and hide all the unwanted areas behind another view or outside of the screen bounds.

Let me elaborate. If I look at your screen shot, do the following:

The distance between you pin and the bottom is 353 pixel. So make your map views frame twice the height: 706 pixel. You screenshot has a height of 411 pixel. Position your frame at an origin of 706px - 411px = -293 pixel. Now center your map view at the coordinate of the pin and you are done.

Update 4-March-2014:

I created a small sample application with Xcode 5.0.2 to demo this: http://cl.ly/0e2v0u3G2q1d

Klaas
  • 22,394
  • 11
  • 96
  • 107
  • 1
    This solution will work even if the map is tilted (the other solutions do not). – sdsykes Nov 28 '13 at 12:34
  • Erm - does not work because setCenterCoordinate centers on the _visible_ portion of the map. So if the map sticks out of the screen, iOS will _ignore_ that and only work on the visible portion. – n13 Mar 03 '14 at 09:26
  • @n13 have you tried it?? The "visible" portion of the map refers to the frame and bounds of the map view. There is nothing related to the screen there. – Klaas Mar 03 '14 at 13:20
  • ^ yes I have tried it, have you? Unfortunately, iOS seems to be too clever about the visible portion of the view. It seems to do an intersect with the visible screen rect. See my response for the solution, there is a map method that takes insets as parameter, which is exactly what we need. – n13 Mar 03 '14 at 15:54
  • @n13 Yes, I use it successfully in several occasions. Keep in mind that under iOS 7 the layout guides influence the center of the map as well. – Klaas Mar 03 '14 at 19:36
  • @Klaas I am not sure what you mean by that. All I know is that it didn't work. I made my map view very tall then moved it up on the screen a bit and added an annotation in the center, centered the view on that. The annotation would always land smack in the center of the screen rather than the center of the map view. I even added a subview to the center of the map view to confirm. – n13 Mar 04 '14 at 10:15
  • @n13 see my updated answer. Still works for me. You must have done something different. – Klaas Mar 04 '14 at 14:29
  • @Klass - ok that explains it. You are solving a different problem. This is what I needed to do: 1 - center map on San Francisco, zoom in to 3km range. 2 - Add a pin to the center 3 - have the pin and center appear in top 1/3rd of screen 3 - have the map still take up the whole screen (not sure if relevant). To do that you must define an inset in the visible map rect method, see my answer. Moving the map view around doesn't work. Moving the map after the zoom in animation doesn't count as it looks bad. – n13 Mar 12 '14 at 17:16
  • @Klaas - My theory is that setVisibleMapRect is very clever about actually visible parts of the map, especially offscreen ones. But feel free to prove me wrong using your example, this is interesting. – n13 Mar 12 '14 at 17:18
  • This should also work when the map automatically "follows" the "userLocation", i.e. you are not constantly "manually" centering it, (which other methods rely on). – Brad Dec 11 '14 at 18:51
  • I am seeing similar problems to what is being discussed. When I change a constraint to make the view SMALLER, the centering works as expected - i.e. centers to the smaller rect - but when I make it larger than screen bounds - it seems to be very disruptive to the automatic userLocation centering. – Brad Dec 11 '14 at 19:02
1

SWIFT 3 UPDATED

Updated the function with Zoom

func zoomToPos() {

        let span = MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta:  0.1)

        // Create a new MKMapRegion with the new span, using the center we want.
        let coordinate = moveCenterByOffset(offset: CGPoint(x: 0, y: 100), coordinate: (officeDetail?.coordinate)!)
        let region = MKCoordinateRegion(center: coordinate, span: span)

        mapView.setRegion(region, animated: true)


    }

    func moveCenterByOffset (offset: CGPoint, coordinate: CLLocationCoordinate2D) -> CLLocationCoordinate2D {
        var point = self.mapView.convert(coordinate, toPointTo: self.mapView)
        point.x += offset.x
        point.y += offset.y
        return self.mapView.convert(point, toCoordinateFrom: self.mapView)
    }
Mikel Sanchez
  • 2,870
  • 1
  • 20
  • 28
  • in which delegate method i need to use this method – Shobhakar Tiwari Dec 08 '16 at 17:16
  • You don't need any delegate method, when you want to zoom only call to the function, for example I zoom to officeDetail?.coordinate, here you can zoom to any coordinate that suits to you. – Mikel Sanchez Dec 09 '16 at 12:17
  • so within zoom delegate this method need to call rit ? and also i want to put my current location in the bottom of map ( 100 pixel above from bottom of map) kindly suggest – Shobhakar Tiwari Dec 09 '16 at 12:20
0

after reading this thread ad playing around, especially with the zooming on annotation, I ended up with following procedures:

** Centering on annotation:**

- (void) centerOnSelection:(id<MKAnnotation>)annotation
{
    MKCoordinateRegion region = self.mapView.region;
    region.center = annotation.coordinate;

    CGFloat per = ([self sizeOfBottom] - [self sizeOfTop]) / (2 * self.mapView.frame.size.height);
    region.center.latitude -= self.mapView.region.span.latitudeDelta * per;

    [self.mapView setRegion:region animated:YES];
}

** Zooming on annotation:**

- (void) zoomAndCenterOnSelection:(id<MKAnnotation>)annotation
{
    DLog(@"zoomAndCenterOnSelection");

    MKCoordinateRegion region = self.mapView.region;
    MKCoordinateSpan span = MKCoordinateSpanMake(0.005, 0.005);

    region.center = annotation.coordinate;

    CGFloat per = ([self sizeOfBottom] - [self sizeOfTop]) / (2 * self.mapView.frame.size.height);
    region.center.latitude -= self.mapView.region.span.latitudeDelta * span.latitudeDelta / region.span.latitudeDelta * per;

    region.span = span;

    [self.mapView setRegion:region animated:YES];
}

-(CGFloat) sizeOfBottom and -(CGFloat) sizeOfTop both return height of panels covering the mapview from the layout guides

altagir
  • 640
  • 8
  • 18
  • not sure why this was mod down, but this is used in commercial app and is adressing the issue of zooming without using setCenterCoordinate, which creates an issue with zooming animation.. try it. – altagir Apr 08 '16 at 20:25
0

As an alternative to the accepted answer, I suggest your original instincts were correct. You can work strictly within the map views pixel coordinate space to get offsets and final positioning. Then using the conversion call from location to screen view, you can get the final location and set the maps center.

This will work with the camera rotated and is in respect to the screen space. In my case I needed to center the map on a pin, with an offset to account for a map drawer.

Here are the conversion calls

func convert(_ coordinate: CLLocationCoordinate2D, toPointTo view: UIView?) -> CGPoint
func convert(_ point: CGPoint, toCoordinateFrom view: UIView?) -> CLLocationCoordinate2D

And here is a swift 4 example

//First get the position you want the pin to be (say 1/4 of the way up the screen)
let targetPoint = CGPoint(x: self.frame.width / 2.0, y: self.frame.height * CGFloat(0.25))

//Then get the center of the screen (this is used for calculating the offset as we are using setCenter to move the region
let centerPoint = CGPoint(x: self.frame.width / 2.0, y: self.frame.height / 2.0)

//Get convert the CLLocationCoordinate2D of the pin (or map location) to a screen space CGPoint
let annotationPoint = mapview.convert(myPinCoordinate, toPointTo: mapview)

//And finally do the math to set the offsets in screen space                
let mapViewPointFromAnnotation = CGPoint(x: annotationPoint.x + (centerPoint.x - targetPoint.x), y: annotationPoint.y + (centerPoint.y - targetPoint.y))

//Now convert that result to a Coordinate
let finalLocation = self.convert(mapViewPointFromAnnotation, toCoordinateFrom: mapview)

//And set the map center
mapview.setCenter(finalLocation, animated: true)
Drewsipher
  • 41
  • 5
-6

Look at this method on MKMapView:

- (void)setCenterCoordinate:(CLLocationCoordinate2D)coordinate animated:(BOOL)animated
Jack Freeman
  • 1,414
  • 11
  • 18