I have an MKMapView that has a MKTileOverlay so that I can show Open Street Map tiles:
NSString *templateURL = #"http://tile.openstreetmap.org/{z}/{x}/{y}.png";
self.tileOverlay = [[MKTileOverlay alloc] initWithURLTemplate:templateURL];
self.tileOverlay.canReplaceMapContent = YES;
[self.mapView addOverlay:self.tileOverlay level:MKOverlayLevelAboveLabels];
I also want to show an MKPolyline from my current location to Apple Park in Cupertino. This polyline needs to be updated as I move, and since an MKPolyline object isn't mutable, I have to remove it and add it for each location update:
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray*)locations {
self.currentLocation = userLocation;
// Update polyline
CLLocationCoordinate2D applePark = CLLocationCoordinate2DMake(37.334626, -122.008895);
[self buildPolylineWithDestinationLocation:applePark];
}
- (void)buildPolylineWithDestinationLocation:(CLLocationCoordinate2D)coordinate {
// Remove the polyline each time so we can redraw it
if (self.polylineApple) {
[self.mapView removeOverlay:self.polylineApple];
}
// Get current location
CLLocation *location = self.currentLocation;
CLLocationCoordinate2D currentLocation = location.coordinate;
CLLocationCoordinate2D points[2];
points[0] = currentLocation;
points[1] = coordinate;
// Remove all route polylines
MKPolyline *oldPolyline = self.polylineApple;
// Draw a line
self.polylineApple = [MKPolyline polylineWithCoordinates:points count:2];
[self.mapView addOverlay:self.polylineApple];
if (oldPolyline) {
[self.mapView removeOverlay:oldPolyline];
oldPolyline = nil;
}
}
The problem is, this used to work great in older versions of iOS, but ever since iOS 13 this has caused the tiles to be redrawn each time that MKPolyline is removed and added:
Is this just an iOS 13 bug, or is there something I need to fix in my code to make this not happen?
As I know since iOS 8, I have seen this issue. Not always but some times.
This issue is linked to 2 things:
1) The action every second to remove and add again a polyline will ask the MKMapView to redrawn the part with the polyline and as a consequence the MKTileOverlay below.
2) If the tile's size (in KB, not the resolution) is low, the issue may not be present.
The best advice I can give to you is to add you own view to the MKMapView and update this own view by calling setNeedsDisplay. It will trigger the draw method where you can convert the map points (latitude, longitude) to screen points and draw the line.
Edit: A link speaking about MKTileOverlay reload issue
https://forums.developer.apple.com/message/313677#313677
I want to change user nearest location pin image in map view.
"In my Project i show the some of the shop locations in map view. The locations (lat,long) are get from the api. Here i changed the given location pin image. it works fine. but i need to change the user nearest location pin image in the map view. I already get the distance details from the user current location to given api locations in that which are the locations are below 5 miles that location pin images are need to change. "
Here is my Annotation Code:
// View for Annotation Delegate Code for changing the pin image.
-(MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation: (id<MKAnnotation>)annotation
{
[self.annotationCustom_View removeFromSuperview];
[self.annotationCurrentLoc_View removeFromSuperview];
static NSString *identifier = #"myAnnotation";
CustomMapViewAnnotation * annotationView = (CustomMapViewAnnotation *)[self.locationsMap_View dequeueReusableAnnotationViewWithIdentifier:identifier];
if (!annotationView)
{
annotationView = [[CustomMapViewAnnotation alloc] initWithAnnotation:annotation reuseIdentifier:identifier];
if([annotation isKindOfClass:[MKUserLocation class]])
{
annotationView.image = [UIImage imageNamed:#"LocationsYour-Current-Location-Icon"]; // User Current Location Image
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
for(int i=0;i<[locations_ArrayList count];i++)
{
MapViewLocationModel *objValue=[locations_ArrayList objectAtIndex:i];
float value = [objValue.kiosk_distance floatValue];
if(value < 5.0)
{
annotationView.image = [UIImage imageNamed:#"LocationsFridge-Location-Icon"]; // Change the pin image which are the below 5.0 miles distance from the user current locations
}
else
{
annotationView.image = [UIImage imageNamed:#"LocationsBlackDot"]; // given api locations pin images
}
}
});
}
}
annotationView.canShowCallout = NO;
return annotationView;
}
This is my code. any one can help me on this?
Try following:
In order to display the pins in certain map area, first you need to
get the all pins inside the 5 miles.
// 1. Set the map zoom area visible of 5 miles:
mapView.region = MKCoordinateRegionMakeWithDistance(
centerCoordinate,
1609.344f * miles (5 in your case),
1609.344f * miles (5 in your case)
);
// 2. Now get the Rect of this map area:
MKMapRect mRect = self.map.visibleMapRect;
// 3. Get the all pins inside this Rect:
NSSet *annotationSet = [myMapView annotationsInMapRect:mRect];
// print number of annotations
NSLog(#"Number of annotations in rect: %d", annotationSet.count);
// this will return an array from the NSSet
NSArray *annotationArray = [annotationSet allObjects];
// 4. Assign some parameter to this annotation, by taking some property in the annotation class.
// 5. Now in your MapView Delegate method viewForAnnotation check the parameter and do the need full with the respective pins.
Hope this will help you to achieve what you want.
I have a function in my view controller with a mapkit that is called when the location changes. I would like set to the map view so that is is centered around the current location and moves with it as it updates.
It works in a sense i.e it tracks along, however it is always zoomed in really far. If I zoom out it snaps back to where it was when it calls the update again after getting a new location.
in from params CLLocation *loc
bool first = TRUE;
if(first){
first=FALSE; // bit of a bodge...if i set this to false it will track the update possition but not at the desired "zoom" its look far to close.
CLLocation *loc = [[CLLocation alloc] initWithLatitude:54.976619 longitude:-1.613118];//newcastle city center.
CLLocationDegrees spanLat = 1000;
CLLocationDegrees spanLng = 1000;
userLocation = MKCoordinateRegionMakeWithDistance(loc.coordinate, spanLat, spanLng);
[mapview setRegion:userLocation animated:TRUE];
}else {
CLLocationDegrees spanLat = [mapview region].span.latitudeDelta;// keep the originals? DOESN'T WORK!!
CLLocationDegrees spanLng = [mapview region].span.longitudeDelta;//
userLocation = MKCoordinateRegionMakeWithDistance(loc.coordinate, spanLat, spanLng);
[mapview setRegion:userLocation animated:TRUE];
}
Just keep setting the center coordinate and don't set the region.
By the way, you do know that you don't need any of that code, right? You can just tell the mapview to track the device's current location. See my book for an example:
http://www.apeth.com/iOSBook/ch34.html#_map_kit_and_current_location
I'm displaying an MKMapView inside a Path-style parallax table view header. To create the effect, the mapView bounds is larger than the area visible to the user. I need to set the map view region such that all the map's annotations are contained within the visible rect of MKMapView. What's the best way to do this?
Edit for clarity: Here's a use-case. The mapView size is 320 x 380. The visible area, however, is defined by the rect (0.0, 20.0, 320.0, 100.0). I need to set the region such that all the annotations appear in this rect within the mapView.
Setting the map region so that all annotations are contained in a certain part of an MKMapView can be done in three steps. Input are the mapView and the annotationsFrame.
Calculate an MKMapRect mapRect that contains all annotations.
Calculate the padding insets from mapView.bounds and annotationsFrame.
Call -setVisibleMapRect:edgePadding:animated: on the map view.
Below is a screen shot of a test. The red overlay shows the annotationsFrame.
Here is the code. Beware: It's all in one method to simplify adding it to your code, and it is not tested for edge cases like passing in n annotations with the same coordinate, or having the annotations so far apart that the map would have to get zoomed out too much, or having coordinates that span the edge of the map at +/-180 degrees longitude.
- (void)zoomAnnotationsOnMapView:(MKMapView *)mapView toFrame:(CGRect)annotationsFrame animated:(BOOL)animated
{
if (_mapView.annotations.count < 2) return;
// Step 1: make an MKMapRect that contains all the annotations
NSArray *annotations = _mapView.annotations;
id <MKAnnotation> firstAnnotation = [annotations objectAtIndex:0];
MKMapPoint minPoint = MKMapPointForCoordinate(firstAnnotation.coordinate);
MKMapPoint maxPoint = minPoint;
for (id <MKAnnotation> annotation in annotations) {
MKMapPoint point = MKMapPointForCoordinate(annotation.coordinate);
if (point.x < minPoint.x) minPoint.x = point.x;
if (point.y < minPoint.y) minPoint.y = point.y;
if (point.x > maxPoint.x) maxPoint.x = point.x;
if (point.y > maxPoint.y) maxPoint.y = point.y;
}
MKMapRect mapRect = MKMapRectMake(minPoint.x, minPoint.y, maxPoint.x - minPoint.x, maxPoint.y - minPoint.y);
// Step 2: Calculate the edge padding
UIEdgeInsets edgePadding = UIEdgeInsetsMake(
CGRectGetMinY(annotationsFrame),
CGRectGetMinX(annotationsFrame),
CGRectGetMaxY(mapBounds) - CGRectGetMaxY(annotationsFrame),
CGRectGetMaxX(mapBounds) - CGRectGetMaxX(annotationsFrame)
);
// Step 3: Set the map rect
[mapView setVisibleMapRect:mapRect edgePadding:edgePadding animated:animated];
}
If you go for a perfect placement (and who doesn't), here are three things to consider:
The code assures that all the coordinates are in the annotationsFrame, but the annotations themselves may be outside. To prevent that, simply use more padding. For example, if your annotations are 20x20 and centered on the coordinate, use 10 more padding on all sides.
Below iOS 7, the map was not zooming to the perfect zoom scale, but to the next tile size (power of two). So there will be more space around the annotations than needed, just as shown on the screenshot.
On iOS 7, the map view will not only zoom perfectly, but automatically care about the status bar. To make the calculation correct, you need to subtract the status bar height from the top padding on iOS 7.
Starting from iOS 7.0, this can be easily achieved with showAnnotations.
Swift:
mapView.showAnnotations(mapView.annotations, animated: true)
Objective-C:
[mapView showAnnotations:mapView.annotations animated:YES];
The above statement will adjust the map view's visible rect in order to display all annotations.
You first need to add the annotations:
(of course this is after you already have a list of annotations)
Swift4:
self.mapView.addAnnotations(annotations)
let currentView = mapView.visibleMapRect
mapView.annotations(in: currentView)
You can use the currentView constant or directly place the MKMapRect as such: Below: (.visibleMapRect returns:
"The area currently displayed by the map view."
mapView.annotations(in: mapView.visibleMapRect)
I found an easier way without calculating is let the map view calculate it, then we adjust the edges.
//1: Show all annotation on the map view, but without animation
self.mapView.showAnnotations(self.mapView.annotations, animated: false)
//2: Get the current visible map rect
let currentMapRect = self.mapView.visibleMapRect
//3: Create the edges inset that you want the map view to show all annotation within
let padding = UIEdgeInsets(top: 100, left: 100, bottom: 100, right: 100)
//4: Set current map rect but with new padding, also set animation to true to see effect
self.mapView.setVisibleMapRect(currentMapRect, edgePadding: padding, animated: true)
If you're prepared to approximate the calculations you can do it using some clever scaling.
Your target area is 80 tall out of a mapView that is 380. Therefore you want a region that is 4.75x taller than the region calculated to fit your annotations. (0.25 extra above and 3.5 extra below).
First you need to get a region (or maprect, what ever you prefer working in) and make it the same proportions as your target viewable area. This is because a really wide and short region would not be touching the top and bottom of the viewable area and therefore multiplying its height would not make something that touched the top and bottom of your map view. So if viewable_height/viewable_width > annotations_height/annotations_width you should set the annotations_height to annotations_width * (viewable_height/viewable_width).
With that you then add 25% on to the north of the annotations box and 350% on to the south. You can do this by moving the center 212.5% (of the current height) south and increasing the vertical span by 475%.
Now, all of this is an approximation given that the world is sphere and we're not looking at a planar projection (i.e. 1 degree of latitude near the equator is drawn smaller than 1 degree near the poles). But if you wally want to be accurate you could look into scaling the numbers according to latitude and such. If you're only dealing with annotations on a city-sized scale you'll probably be ok.
Hope that helps.
if you want to find the annotations that are in a given rect:
- (NSArray*)allAnnotationsInMapRect:(MKMapRect)mapRect {
NSMutableArray *annotationsInRect = [NSMutableArray array];
for(id<MKAnnotation *ann in self.allAnnotations) {
MKMapPoint pt = MKMapPointForCoordinate(ann.coordinate);
if(MKMapRectContainsPoint(mapRect, pt)) {
[annotationsInRect addObject:ann];
}
}
return annotationsInRect;
}
and to assure the annotation VIEWS are in the rect, get the region for the annotations,
then walk through them and get each view's bounds see if the bounds fit inside the visibleRect of the map and if not modify the region!
~~ like this:
- (void)assureAnnotationViewsAreVisible:(NSArray*)annotations originalRegion:(MKCoordinateRegion)originalRegion {
CGFloat smallestX = MAXFLOAT;
CGFloat smallestY = MAXFLOAT;
CGFloat biggestX = -100;
CGFloat biggestY = -100;
//NSLog(#"---: %d", annotations.count);
for(id<MKAnnotation> *annotation in annotations) {
UIView *annotationView = [self.mapView viewForAnnotation:v];
CGRect annotationViewFrame = annotationView.bounds;
annotationViewFrame.origin = [self.mapView convertCoordinate:annotationView.coordinate toPointToView:self.mapView];
annotationViewFrame.origin = CGPointMake(annotationViewFrame.origin.x-annotationViewFrame.size.width/2,
annotationViewFrame.origin.y-annotationViewFrame.size.height);
smallestX = MIN(annotationViewFrame.origin.x, smallestX);
smallestY = MIN(annotationViewFrame.origin.y, smallestY);
biggestX = MAX(annotationViewFrame.origin.x+annotationViewFrame.size.width, biggestX);
biggestY = MAX(annotationViewFrame.origin.y+annotationViewFrame.size.height, biggestY);
}
//NSLog(#"---");
CGRect bounds = self.mapView.bounds;
if(smallestX < bounds.origin.x || smallestY < bounds.origin.y || biggestX > bounds.origin.x+bounds.size.width || biggestY > bounds.origin.y+bounds.size.height) {
CGRect neededRect = bounds;
neededRect.origin = CGPointMake(MIN(bounds.origin.x, smallestX), MIN(bounds.origin.y, smallestY));
neededRect.size = CGSizeMake(MAX(bounds.size.width, biggestX), MAX(bounds.size.height, biggestY));
MKCoordinateRegion neededRegion = [self.mapView convertRect:neededRect toRegionFromView:self.mapView];
_ignoreRegionChange = YES;
[self.mapView setRegion:originalRegion animated:NO];
_ignoreRegionChange = NO;
[self.mapView setRegion:neededRegion animated:YES];
}
else {
MKCoordinateRegion currentRegion = self.mapView.region;
_ignoreRegionChange = YES;
[self.mapView setRegion:originalRegion animated:NO];
_ignoreRegionChange = NO;
[self.mapView setRegion:currentRegion animated:YES];
}
}
Try to get from all your annotation edges value (max and min) for lan and lon.
Define this value on the beginning:
static float maxLat = FLT_MIN;
static float maxLon = FLT_MIN;
static float minLat = FLT_MAX;
static float minLon = FLT_MAX;
and then use this function to calculate span and region:
- (void) zoomAndFit {
for(int i = 0; i < [self.points count]; i++) {
PRPlaceModel *place = [self.points objectAtIndex:i];
CLLocationCoordinate2D location;
location.latitude = [place.lat floatValue];
location.longitude = [place.lon floatValue];
minLat = MIN(minLat, location.latitude);
minLon = MIN(minLon, location.longitude);
maxLat = MAX(maxLat, location.latitude);
maxLon = MAX(maxLon, location.longitude);
}
MKCoordinateRegion region;
MKCoordinateSpan span;
span.latitudeDelta = 1.2*(maxLat - minLat);
span.longitudeDelta = 1.2*(maxLon - minLon);
CLLocationCoordinate2D location;
location.latitude = (minLat + maxLat)/2;
location.longitude = (minLon + maxLon)/2;
region.span=span;
region.center=location;
[self.mapView setRegion:region animated:TRUE];
[self.mapView regionThatFits:region];
}
And use it in viewDidLoad method:
-(void)viewDidLoad
{
[super viewDidLoad];
[self zoomAndFit];
}
I'm trying to add the distance from the user's position to a selected annotation's subtitle in a mapview. The mechanics of it are working, but the actual callout gets messed up the first time it's displayed. There appears to be a redraw problem.
Subsequent taps on the pin show the correct layout.
Here's the relevant code:
// called when selecting annotations
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view{
MKPointAnnotation *selectedAnnotation = view.annotation;
//attempt to add distance on annotation
CLLocation *pointALocation = [[CLLocation alloc]
initWithLatitude:selectedAnnotation.coordinate.latitude
longitude:selectedAnnotation.coordinate.longitude];
float distanceMeters = [pointALocation distanceFromLocation:locationManager.location];
//for sending info to detail
myPinTitle = selectedAnnotation.title;
[selectedAnnotation setSubtitle:[NSString stringWithFormat:#"%.2f miles away", (distanceMeters / 1609.344)]];
}
I've tried calling [view setNeedsDisplay], but to no avail.
Thanks in advance for your help.
The Solution that Worked
Here's the solution I finally came up with. It seems to work.
I edited out the duplicate code from the didSelectAnnotationView method, above, and came up with:
//called when user location changes
- (void)updatePinsDistance
{
for (int x=0; x< [[mapView annotations]count]; x++) {
MKPointAnnotation *thisPin =[[mapView annotations] objectAtIndex:x];
//attempt to add distance on annotation
CLLocation *pointALocation = [[CLLocation alloc]
initWithLatitude:thisPin.coordinate.latitude
longitude:thisPin.coordinate.longitude];
float distanceMeters = [pointALocation distanceFromLocation:locationManager.location];
NSString *distanceMiles = [NSString stringWithFormat:#"%.2f miles from you",
(distanceMeters / 1609.344)];
[thisPin setSubtitle:distanceMiles];
}
}
You should set your subtitle in another place than didSelectAnnotationView. Actually all annotationViews should have their title and subtitle set before they are returned by the mapView:viewForAnnotation: method.
The fact that you set a long subtitle certainly explains that the callout is not the right size. The size must be calculated before the didSelectAnnotationView is called.