Free hand Draw polyline overlay in ios - iphone

I'm trying to trace the route by free hand on a MKMapView using overlays (MKOverlay).
Each time when we move the finger i extend the polyline with last coordinate with new coordinate,all are working fine except when extending polyline overlay the whole overlay is blinking in device(only sometimes),so i can,t trace the problem.
The code i have tried is given below.
- (void)viewDidLoad
{
j=0;
coords1 = malloc(2* sizeof(CLLocationCoordinate2D));
coordinatearray=[[NSMutableArray alloc]init];
UIPanGestureRecognizer *GestureRecogonized = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(gestureDetacted:)];
[self.myMapView addGestureRecognizer:GestureRecogonized];
}
- (void)gestureDetacted:(UIPanGestureRecognizer *)recognizer
{
if(UIGestureRecognizerStateBegan==recognizer.state)
{
CGPoint point = [recognizer locationInView:self.myMapView];
CLLocationCoordinate2D tapPoint = [self.myMapView convertPoint:point toCoordinateFromView:self.view];
CLLocation *curLocation = [[CLLocation alloc] initWithLatitude:tapPoint.latitude longitude:tapPoint.longitude];
[coordinatearray addObject:curLocation];
}
coords1[0]=[[coordinatearray objectAtIndex:j] coordinate];
if(UIGestureRecognizerStateChanged==recognizer.state)
{
j++;
CGPoint point = [recognizer locationInView:self.myMapView];
CLLocationCoordinate2D tapPoint = [self.myMapView convertPoint:point toCoordinateFromView:self.view];
CLLocation *curLocation = [[CLLocation alloc] initWithLatitude:tapPoint.latitude longitude:tapPoint.longitude];
[coordinatearray addObject:curLocation];
coords1[1]=CLLocationCoordinate2DMake(tapPoint.latitude,tapPoint.longitude);
polyLine = [MKPolyline polylineWithCoordinates:coords1 count:2];
[self.myMapView addOverlay:polyLine];
}
}
in overlay delegate
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay {
if([overlay isKindOfClass:[MKPolyline class]]){
MKPolylineView *polylineView = [[MKPolylineView alloc] initWithPolyline:overlay];
polylineView.strokeColor = [UIColor orangeColor];
polylineView.lineWidth = 20;
polylineView.fillColor=[[UIColor orangeColor] colorWithAlphaComponent:.1];
return polylineView;
}
}
can anybody know why that flickering or blinking effect is coming and how to remove it.
Thanks in advance.

Rather than adding hundreds of really small views (which is really computationally intensive), i would rather remove the polyline overlay and add a new one with all the points on the map in it at each change in the pan recognizer (for a smoother effect, you can first add the new one and then remove the old one). Use your coordinateArray to create the MKPolyline overlay that contains all of the points, rather than the last 2 points.
You could do something like:
[coordinatearray addObject:curLocation];;
CLLocationCoordinate2D* coordArray = malloc(sizeof(CLLocationCoordinate2D)*[coordinatearray count]);
memcpy(coordArray, self.coordinates, sizeof(CLLocationCoordinate2D)*([coordinatearray count]-1));
coordArray[[coordinatearray count]-1] = curLocation;
free(self.coordinates);
self.coordinates = coordArray;
MKPolyline *polyline = [MKPolyline polylineWithCoordinates:coordArray count:[coordinatearray count]];
MKPolyline *old = [[self.mapView overlays] lastObject];
[self.mapView addOverlay:polyline];
[self.mapView removeOverlay:old];
Where self.coordinate is a property of type CLLocationCoordinate2D*, this way you can memcpy the existing array into a new one (memcpy is really efficent) and only need to add the last point to the array, rather than having to loop through each point of the NSArray *coordinatearray.
You also have to change your if(UIGestureRecognizerStateBegan==recognizer.state) part, to add in self.coordinates the first tapped point.
Just something like:
self.coordinates = malloc(sizeof(CLLocationCoordinate2D));
self.coordinates[0] = curLocation;
EDIT: I think the problem is that map overlays draw themselves for different discrete values of zoom levels. At a certain zoom level it would appear that the overlay first draws itself at the bigger zoom level, and then at the smaller zoom level (in fact drawing over the previously drawn overlay). When you try to show an animation such as that from drawing the user panning gesture at this zoom level, that is why the overlay keeps flickering. A possible solution would be to use a transparent view that you put on top of the map view, where the drawing is performed while the user keeps moving the finger. Only once the panning gesture ends you then create a map overlay that you "pin" to the map and you clean the drawing view. You also need to be careful when redrawing the view, as you should each time specify only the rect to be redrawn and then redraw only in that rect, as redrawing the whole thing each time would cause a flicker in this view as well. This is definitely much more coding involved, but it should work. Check this question for how to incrementally draw on a view.

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];
}

hitTest overlapping CALayers

I have a UIView that contains a drawing that I've made using CALayers added as sublayers. It is a red square with a blue triangle centered inside. I am able to determine which shape has been touched using the following code:
CGPoint location = [gesture locationInView:self.view];
CALayer* layerThatWasTapped = [self.view.layer hitTest:location];
NSLog(#"Master Tap Location: %#", NSStringFromCGPoint(location));
NSLog(#"Tapped Layer Name: %#", layerThatWasTapped.name);
NSLog(#"Tapped Layer Parent: %#", layerThatWasTapped.superlayer.name);
int counter = layerThatWasTapped.superlayer.sublayers.count;
NSArray * subs = layerThatWasTapped.superlayer.sublayers;
//Loop through all sublayers of the picture
for (int i=0; i<counter; i++) {
CALayer *layer = [subs objectAtIndex:i];
CAShapeLayer* loopLayer = (CAShapeLayer*)layerThatWasTapped.modelLayer;
CGPathRef loopPath = loopLayer.path;
CGPoint loopLoc = [gesture locationInView:cPage];
loopLoc = [self.view.layer convertPoint:loopLoc toLayer:layer];
NSLog(#"loopLoc Tap Location: %#", NSStringFromCGPoint(loopLoc));
//determine if hit is on a layer
if (CGPathContainsPoint(loopPath, NULL, loopLoc, YES)) {
NSLog(#"Layer %i Name: %# Hit",i, layer.name);
} else {
NSLog(#"Layer %i Name: %# No Hit",i, layer.name);
}
}
My problem lies with areas where the bounds of the triangle overlap the square.
This results in the triangle registering the hit even when the hit is outside of the
triangles path. This is a simplified example (I may have many overlapping shapes stacked in the view)
Is there a way to loop through all of the sublayers and hittest each one to see if it lies under the tapped point?
OR
Is there a way to have the bounds of my layers match their paths so the hit occurs only on a visible area?
Since you're using CAShapeLayer, this is pretty easy. Make a subclass of CAShapeLayer and override its containsPoint: method, like this:
#implementation MyShapeLayer
- (BOOL)containsPoint:(CGPoint)p
{
return CGPathContainsPoint(self.path, NULL, p, false);
}
#end
Make sure that wherever you were allocating a CAShapeLayer, you change it to allocate a MyShapeLayer instead:
CAShapeLayer *triangle = [MyShapeLayer layer]; // this way
CAShapeLayer *triangle = [[MyShapeLayer alloc] init]; // or this way
Finally, keep in mind that when calling -[CALayer hitTest:], you need to pass in a point in the superlayer's coordinate space:
CGPoint location = [gesture locationInView:self.view];
CALayer *myLayer = self.view.layer;
location = [myLayer.superlayer convertPoint:location fromLayer:myLayer];
CALayer* layerThatWasTapped = [myLayer hitTest:location];

MKOverlay not resizing smoothly

I have added a MKCircle as MKOverlay to my MKMapView. Also I added an UISlider to decide the radius of the circle. Unfortunately when using this it seems a bit "laggy", not smootly like I want it to be.
Example:
http://dl.dropbox.com/u/3077127/mkoverlayDelay.mov
This is my code:
- (void)addCircle
{
// draw the radius circle for the marker
double radius = 2000.0;
MKCircle *circle = [MKCircle circleWithCenterCoordinate:location radius:radius];
[circle setTitle:#"background"];
[mapView addOverlay:circle];
MKCircle *circleLine = [MKCircle circleWithCenterCoordinate:location radius:radius];
[circleLine setTitle:#"line"];
[mapView addOverlay:circleLine];
}
- (void)addCircleWithRadius:(double)radius
{
MKCircle *circle = [MKCircle circleWithCenterCoordinate:location radius:radius];
[circle setTitle:#"background"];
[mapView addOverlay:circle];
MKCircle *circleLine = [MKCircle circleWithCenterCoordinate:location radius:radius];
[circleLine setTitle:#"line"];
[mapView addOverlay:circleLine];
}
- (void)sliderChanged:(UISlider*)sender
{
[mapView removeOverlays:[mapView overlays]];
double radius = (sender.value * 100);
[self addCircleWithRadius:radius];
}
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay{
MKCircle *circle = overlay;
MKCircleView *circleView = [[[MKCircleView alloc] initWithCircle:overlay] autorelease];
if ([circle.title isEqualToString:#"background"])
{
circleView.fillColor = UIColorFromRGB(0x598DD3);
circleView.alpha = 0.25;
}
else
{
circleView.strokeColor = UIColorFromRGB(0x5C8AC7);
circleView.lineWidth = 2.0;
}
return circleView;
}
Does anybody have any suggestions on how I can smoothen this?
Best regards,
Paul Peelen
I have tried your code and found a very easy way to make it smoother.
If you change the order of the calls in: - (void)sliderChanged:(UISlider*)sender
You can call [self addCircleWithRadius:radius];
before calling [mapView removeOverlays:[mapView overlays]];
Just make sure you dont remove the overlays you just added, only the old ones.
This will give you a smoother resizing, specially when the new circle is smaller than the old one.
For circles that are bigger you are probably better off using NSOperations to ensure the views are created faster, this will make it smoother.
Hope this helps.
Your best bet might be to draw the circle yourself, either using another UIView on top of the MKMapView, or using a MKAnnotationView within the Map View.
This blogger has done a similar thing (before iOS4 when overlays were added).
http://spitzkoff.com/craig/?p=65

An offscreen MKMapView behaves differently in 3.2, 4.0

In 3.1 I've been using an "offscreen" MKMapView to create map images that I can rotate, crop and so forth before presenting them the user. In 3.2 and 4.0 this technique no longer works quite right. Here's some code that illustrates the problem, followed by my theory.
// create map view
_mapView = [[MKMapView alloc] initWithFrame:CGRectMake(0, 0, MAP_FRAME_SIZE, MAP_FRAME_SIZE)];
_mapView.zoomEnabled = NO;
_mapView.scrollEnabled = NO;
_mapView.delegate = self;
_mapView.mapType = MKMapTypeSatellite;
// zoom in to something enough to fill the screen
MKCoordinateRegion region;
CLLocationCoordinate2D center = {30.267222, -97.763889};
region.center = center;
MKCoordinateSpan span = {0.1, 0.1 };
region.span = span;
_mapView.region = region;
// set scrollview content size to full the imageView
_scrollView.contentSize = _imageView.frame.size;
// force it to load
#ifndef __IPHONE_3_2
// in 3.1 we can render to an offscreen context to force a load
UIGraphicsBeginImageContext(_mapView.frame.size);
[_mapView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIGraphicsEndImageContext();
#else
// in 3.2 and above, the renderInContext trick doesn't work...
// this at least causes the map to render, but it's clipped to what appears to be
// the viewPort size, plus some padding
[self.view addSubview:_mapView];
#endif
when the map is done loading, I snap picture of it and stuff it in my scrollview
- (void)mapViewDidFinishLoadingMap:(MKMapView *)mapView {
NSLog(#"[MapBuilder] mapViewDidFinishLoadingMap");
// render the map to a UIImage
UIGraphicsBeginImageContext(mapView.bounds.size);
// the first sub layer is just the map, the second is the google layer, this sublayer structure might change of course
[[[mapView.layer sublayers] objectAtIndex:0] renderInContext:UIGraphicsGetCurrentContext()];
// we are done with the mapView at this point, we need its ram!
_mapView.delegate = nil;
[_mapView release];
[_mapView removeFromSuperview];
_mapView = nil;
UIImage* mapImage = [UIGraphicsGetImageFromCurrentImageContext() retain];
UIGraphicsEndImageContext();
_imageView.image = mapImage;
[mapImage release], mapImage = nil;
}
The first problem is that in 3.1 rendering to a context would trigger the map to begin loading. This no longer works in 3.2, 4.0. The only thing I have found would trigger the load is to temporarily add the map to the view (i.e. make it visible). The problem being that the map only renders to the visible area of the screen, plus a little padding. The frame/bounds are fine, but it appears to be "helpfully" optimizes the loading to limit the tiles to those visible on the screen or close to it.
Any ideas how to force the map to load at full size? Anyone else have this issue?

Adding subview to MKMapView that's above the map but below the annotation views?

For an app I'm building, I have a custom "old map" effect PNG that I'd like to overlay on the MKMapView view. Unfortunately, neither way I've tried to do this is exactly right:
Add a subview to the MKMapView (issue: the UIImageView for the PNG gets placed above the Annotation pins, which looks weird
Make the image an annotation view itself and place it below the other ones (issue: on scroll, I have to move the annotation view image, and for a while there's just the regular map instead of the old-timey-effect).
What I basically want to accomplish is having a subview that is layered above the maptiles but below the annotation views, and that will hold steady while the user scrolls around—any thoughts?
Note that all MKMapView subviews hierarchy, annotations, behaviour etc proceeds in internal private MKMapViewInternal class so changing anything (in a way I suggest or another) in this structure may cause your application to be rejected from Appstore.
I do not have a full solution, just an idea. My idea is to make an overlay view have the same superview as annotation views and ensure that annotations will be placed over our overlay. In MKMapViewDelegate we implement:
- (void)mapView:(MKMapView *)aMapView didAddAnnotationViews:(NSArray *)views{
if (views.count > 0){
UIView* tView = [views objectAtIndex:0];
UIView* parView = [tView superview];
UIView* overlay = [tView viewWithTag:2000];
if (overlay == nil){
overlay = [[UIImageView alloc] initWithImage:[UIImage imageNamed:MY_IMAGE_NAME]];
overlay.frame = parView.frame; //frame equals to (0,0,16384,16384)
overlay.tag = 2000;
overlay.alpha = 0.7;
[parView addSubview:overlay];
[overlay release];
}
for (UIView* view in views)
[parView bringSubviewToFront:view];
}
}
This code does a half of what you want - you get a view placed between map tiles and annotations. The remaining task is to change custom overlay view frame according to map scrolling/zooming (I'll post if I find the way).
I did something similar and in a different way in the end. I wanted to fade the mapview out by adding a white overlay, but I didn't want to fade the annotations. I added an overlay to the map (actually I had to add two as it didn't like me trying to add an overlay for the entire globe).
CLLocationCoordinate2D pt1, pt2, pt3, pt4;
pt1 = CLLocationCoordinate2DMake(85, 0);
pt2 = CLLocationCoordinate2DMake(85, 180);
pt3 = CLLocationCoordinate2DMake(-85, 180);
pt4 = CLLocationCoordinate2DMake(-85, 0);
CLLocationCoordinate2D coords[] = {pt1, pt2, pt3, pt4};
MKPolygon *p = [MKPolygon polygonWithCoordinates:coords count:4];
[self.mapView addOverlay:p];
pt1 = CLLocationCoordinate2DMake(85, 0);
pt2 = CLLocationCoordinate2DMake(85, -180);
pt3 = CLLocationCoordinate2DMake(-85, -180);
pt4 = CLLocationCoordinate2DMake(-85, 0);
CLLocationCoordinate2D coords2[] = {pt1, pt2, pt3, pt4};
MKPolygon *p2 = [MKPolygon polygonWithCoordinates:coords2 count:4];
[self.mapView addOverlay:p2];
Then you need to add the map delegate to draw them:
- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay {
if ([overlay isKindOfClass:MKPolygon.class]) {
MKPolygonRenderer *polygonView = [[MKPolygonRenderer alloc] initWithOverlay:overlay];
polygonView.fillColor = [UIColor colorWithRed:255.0/255.0 green:255.0/255.0 blue:255.0/255.0 alpha:0.4];
return polygonView;
}
return nil;
}
It's pretty old, but maybe still relevant for some of you.
I wanted to make the map darker, but not the annotation views.
What i did was very simple and i don't yet know if it has some fallbacks...
I put a UIView with a black-transparent background above the MKMapView, and then added the following code:
- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray<MKAnnotationView *> *)views
{
for (MKAnnotationView *view in views) {
view.center = [self.mapView convertCoordinate:view.annotation.coordinate toPointToView:self.viewDark];
[self.viewDark addSubview:view];
}
}
Hope it helps.. and of course - if you find any issues about this solution please let me know :)
Edit #1:
Forgot to mention that self.viewDark should have userInteraction disabled.
Edit #2:
Another thing that helped me is setting self.viewDark's autoresizesSubviews to NO.