问题
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)?
回答1:
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:
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.
回答2:
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.
回答3:
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)
}
}
回答4:
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
回答5:
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)
}
回答6:
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
回答7:
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)
回答8:
Look at this method on MKMapView
:
- (void)setCenterCoordinate:(CLLocationCoordinate2D)coordinate animated:(BOOL)animated
来源:https://stackoverflow.com/questions/30831318/programmatically-move-mkmapview-ios