I'm trying to figure out a way to detect which MKOverlayView (actually MKPolygonView) was tapped and then change its color.
I got it running with this code:
- (void)mapTapped:(UITapGestureRecognizer *)recognizer {
MKMapView *mapView = (MKMapView *)recognizer.view;
MKPolygonView *tappedOverlay = nil;
for (id<MKOverlay> overlay in mapView.overlays)
{
MKPolygonView *view = (MKPolygonView *)[mapView viewForOverlay:overlay];
if (view){
// Get view frame rect in the mapView's coordinate system
CGRect viewFrameInMapView = [view.superview convertRect:view.frame toView:mapView];
// Get touch point in the mapView's coordinate system
CGPoint point = [recognizer locationInView:mapView];
// Check if the touch is within the view bounds
if (CGRectContainsPoint(viewFrameInMapView, point))
{
tappedOverlay = view;
break;
}
}
}
if([[tappedOverlay fillColor] isEqual:[[UIColor cyanColor] colorWithAlphaComponent:0.2]]){
[listOverlays addObject:tappedOverlay];
tappedOverlay.fillColor = [[UIColor redColor] colorWithAlphaComponent:0.2];
}
else{
[listOverlays removeObject:tappedOverlay];
tappedOverlay.fillColor = [[UIColor cyanColor] colorWithAlphaComponent:0.2];
}
//tappedOverlay.strokeColor = [[UIColor blueColor] colorWithAlphaComponent:0.7];
}
Which works but sometimes, depending where I tap it gets wrong which MKPolygonView was tapped. I suppose because CGRectContainsPoint doesnt calculate properly the area, since it's not a rectangle it's a Polygon.
What other methods there are to do this? I tried CGPathContainsPoint but I get worse results.
Thanks to #Ana Karenina, that pointed out the right way, this is how you have to convert the gesture so that the method CGPathContainsPoint' works right.
- (void)mapTapped:(UITapGestureRecognizer *)recognizer{
MKMapView *mapView = (MKMapView *)recognizer.view;
MKPolygonView *tappedOverlay = nil;
int i = 0;
for (id<MKOverlay> overlay in mapView.overlays)
{
MKPolygonView *view = (MKPolygonView *)[mapView viewForOverlay:overlay];
if (view){
CGPoint touchPoint = [recognizer locationInView:mapView];
CLLocationCoordinate2D touchMapCoordinate =
[mapView convertPoint:touchPoint toCoordinateFromView:mapView];
MKMapPoint mapPoint = MKMapPointForCoordinate(touchMapCoordinate);
CGPoint polygonViewPoint = [view pointForMapPoint:mapPoint];
if(CGPathContainsPoint(view.path, NULL, polygonViewPoint, NO)){
tappedOverlay = view;
tappedOverlay.tag = i;
break;
}
}
i++;
}
if([[tappedOverlay fillColor] isEqual:[[UIColor cyanColor] colorWithAlphaComponent:0.2]]){
[listOverlays addObject:tappedOverlay];
tappedOverlay.fillColor = [[UIColor redColor] colorWithAlphaComponent:0.2];
}
else{
[listOverlays removeObject:tappedOverlay];
tappedOverlay.fillColor = [[UIColor cyanColor] colorWithAlphaComponent:0.2];
}
//tappedOverlay.strokeColor = [[UIColor blueColor] colorWithAlphaComponent:0.7];
}
Related
I'm developing an app with a map in which the user can draw a polygon area.
My issue is what it's possible drawing polygons with knots (see the image) (I don't know if knot is the right word). I didn't find a simply way preventing the polygon to get knots.
For the case of the attached image, I would like the small curl to be removed and even the outline to be smoothed
Do you know a way to make that?
The process of drawing the polygon while the user is touching the screen, does use MKPolyline, MKPolygon and MKOverlay as follows:
- (void)touchesBegan:(UITouch*)touch
{
CGPoint location = [touch locationInView:self.mapView];
CLLocationCoordinate2D coordinate = [self.mapView convertPoint:location toCoordinateFromView:self.mapView];
[self.coordinates addObject:[NSValue valueWithMKCoordinate:coordinate]];
}
- (void)touchesMoved:(UITouch*)touch
{
CGPoint location = [touch locationInView:self.mapView];
CLLocationCoordinate2D coordinate = [self.mapView convertPoint:location toCoordinateFromView:self.mapView];
[self.coordinates addObject:[NSValue valueWithMKCoordinate:coordinate]];
}
- (void)touchesEnded:(UITouch*)touch
{
CGPoint location = [touch locationInView:self.mapView];
CLLocationCoordinate2D coordinate = [self.mapView convertPoint:location toCoordinateFromView:self.mapView];
[self.coordinates addObject:[NSValue valueWithMKCoordinate:coordinate]];
[self didTouchUpInsideDrawButton:nil];
}
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay
{
MKOverlayPathView *overlayPathView;
if ([overlay isKindOfClass:[MKPolygon class]])
{
// create a polygonView using polygon_overlay object
overlayPathView = [[MKPolygonView alloc] initWithPolygon:overlay];
overlayPathView.fillColor = [UIColor redColor];
overlayPathView.lineWidth = 1.5;
return overlayPathView;
}
else if ([overlay isKindOfClass:[MKPolyline class]])
{
overlayPathView = [[MKPolylineView alloc] initWithPolyline:(MKPolyline *)overlay];
overlayPathView.fillColor = [UIColor redColor];
overlayPathView.lineWidth = 3;
return overlayPathView;
}
return nil;
}
MKOverlayPathView was deprecated since iOS 7.0. You'd use MKOverlayRenderer instead of it and also related map delegate method.
Try to play with miterLimit property of MKOverlayRenderer.
Example:
-(MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay {
if ([overlay isKindOfClass:[MKPolygon class]]) {
MKPolygonRenderer *polygonRenederer = [[MKPolygonRenderer alloc] initWithPolygon:overlay];
polygonRenederer.fillColor = [UIColor redColor];
polygonRenederer.lineWidth = 1.5;
polygonRenederer.miterLimit = 10;
return polygonRenederer;
} else if ([overlay isKindOfClass:[MKPolyline class]]) {
MKPolylineRenderer *lineRenderer = [[MKPolylineRenderer alloc] initWithPolyline:overlay];
lineRenderer.strokeColor = [UIColor redColor];
lineRenderer.lineWidth = 3;
return lineRenderer;
}
return nil;
}
I'm trying to modify a the path of my CAShapeLayer and then redraw it using setNeedsDisplay,
But for some reason it wont redraw. The only way i can make it to be redrawn is calling viewDidLoad which i am sure is not the correct way.
Here is the code for initialisation:
self.shape.lineWidth = 2.0;
[self.shape setFillColor:[[UIColor clearColor] CGColor]];
self.shape.strokeColor = [[UIColor blackColor] CGColor];
[self.shape setPath:[self.shapePath CGPath]];
self.shape.fillRule = kCAFillModeRemoved;
[self.cuttingView.layer addSublayer:self.shape];
And the gesture recogniser for the drag gesture:
- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
static CGPoint lastLocation;
CGPoint location = [gesture locationInView:self.cuttingView];
if (gesture.state == UIGestureRecognizerStateBegan)
{
if (![self.shapePath containsPoint:location] ) {
gesture.enabled = NO;
} else {
lastLocation = location;
}
}
if (gesture.state == UIGestureRecognizerStateChanged) {
CGAffineTransform translation = CGAffineTransformMakeTranslation((location.x - lastLocation.x), (location.y - lastLocation.y));
self.shapePath.CGPath = CGPathCreateCopyByTransformingPath(self.shapePath.CGPath, &translation);
[self.shape setNeedsDisplay];
}
gesture.enabled = YES;
}
I am printing the locations, and they do change, but the screen shows nothing.
How could this be fixed?
Thanks!
You are changing self.shapePath.CGPath, but you are not changing the path in the CAShapeLayer.
Instead of [self.shape setNeedsDisplay], do this again:
[self.shape setPath:[self.shapePath CGPath]];
you don't assign the path to the layer. I'd remove the shapePath variable as it has no use (AFAICS)
I'm attempting to get the reference to a UImageView that is underneath a masked UIImageView using hitTest withEvent. Here is what I have that is not working:
UIView A that contains 3 UIImageViews as subviews: panelOne, panelTwo, and panelThree. panelThree is takes up the entire frame but is masked into a triangle, revealing parts of panels one and two. So I need to detect when a user taps outside of that rectangle and send the touch to the appropriate UIImageView.
Code: (CollagePanel is a subclass of UIImageView)
-(void)triangleInASquare
{
CGSize size = self.frame.size;
CollagePanel *panelOne = [[CollagePanel alloc] initWithFrame:CGRectMake(0,0, size.width/2, size.height)];
panelOne.panelScale = panelOne.frame.size.width/self.frame.size.width;
panelOne.backgroundColor = [UIColor greenColor];
CollagePanel *panelTwo = [[CollagePanel alloc] initWithFrame:CGRectMake(size.width/2,0, size.width/2, size.height)];
panelTwo.panelScale = panelOne.frame.size.width/self.frame.size.width;
panelTwo.backgroundColor = [UIColor purpleColor];
CollagePanel *panelThree = [[CollagePanel alloc] initWithFrame:CGRectMake(0,0, size.width, size.height)];
panelThree.backgroundColor = [UIColor orangeColor];
UIBezierPath* trianglePath = [UIBezierPath bezierPath];
[trianglePath moveToPoint:CGPointMake(0, panelThree.frame.size.height)];
[trianglePath addLineToPoint:CGPointMake(panelThree.frame.size.width/2,0)];
[trianglePath addLineToPoint:CGPointMake(panelThree.frame.size.width, panelTwo.frame.size.height)];
[trianglePath closePath];
// Mask the panels's layer to triangle.
CAShapeLayer *triangleMaskLayer = [CAShapeLayer layer];
[triangleMaskLayer setPath:trianglePath.CGPath];
triangleMaskLayer.strokeColor = [[UIColor whiteColor] CGColor];
panelThree.layer.mask = triangleMaskLayer;
//Add border
CAShapeLayer *borderLayer = [CAShapeLayer layer];
borderLayer.strokeColor = [[UIColor whiteColor] CGColor];
borderLayer.fillColor = [[UIColor clearColor] CGColor];
borderLayer.lineWidth = 6;
[borderLayer setPath:trianglePath.CGPath];
[panelThree.layer addSublayer:borderLayer];
NSMutableArray *tempArray = [[NSMutableArray alloc] init];
[tempArray addObject:panelOne];
[tempArray addObject:panelTwo];
[tempArray addObject:panelThree];
[self addGestureRecognizersToPanelsInArray:tempArray];
[self addPanelsFromArray:tempArray];
self.panelArray = tempArray;
}
-(void)handleTap: (UITapGestureRecognizer*) recognizer //coming from panel.imageView
{
CGPoint tapPoint = [recognizer locationInView:self];
NSLog(#"Location in self: %#", NSStringFromCGPoint(tapPoint));
NSLog(#"self.subviews: %#", self.subviews);
UIView *bottomView = [self hitTest:tapPoint withEvent:nil];
NSLog(#"Bottom View: %#", bottomView);
}
The NSLog of bottomView is always panelThree (the topmost panel). From what I understand the hit test should be returning the "bottom most" subview.
you understand wrong. it will return the view that recognizes itself as touched and is nearest to your finger, nearer to the top.
If a view shall not recognize itself as touch for a certain point, you need to overwrite
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
for that view.
I think Ole Begemann's Shapped Button is a great example how to do so.
In your project this method could determine, if a point lies within the paths: CGPathContainsPoint.
Your pointInside:withEvent: might look like this:
#import "CollagePanel.h"
#import <QuartzCore/QuartzCore.h>
#implementation CollagePanel
//
// ...
//
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
CGPoint p = [self convertPoint:point toView:[self superview]];
if(self.layer.mask){
if (CGPathContainsPoint([(CAShapeLayer *)self.layer.mask path], NULL, p, YES) )
return YES;
}else {
if(CGRectContainsPoint(self.layer.frame, p))
return YES;
}
return NO;
}
#end
In my app, user draws a shape on map and using UIBeizerPath i am drawing that path. Then based on the coordinates of the path i am displaying the results which are only in that area. Everything works great except that now when Annotations drops on the Map view the pins looks like they are behind the path which means path looks in the front.
I am using this code to display the Annotation and path :
-(void)clearAnnotationAndPath:(id)sender {
[_mapView removeAnnotations:_mapView.annotations];
path = [UIBezierPath bezierPath];
[shapeLayer removeFromSuperlayer];
}
- (void)handleGesture:(UIPanGestureRecognizer *)gesture
{
CGPoint location = [gesture locationInView:_pathOverlay];
if (gesture.state == UIGestureRecognizerStateBegan)
{
shapeLayer = [[CAShapeLayer alloc] init];
shapeLayer.fillColor = [[UIColor clearColor] CGColor];
shapeLayer.strokeColor = [[UIColor greenColor] CGColor];
shapeLayer.lineWidth = 5.0;
//[_mapView.layer addSublayer:shapeLayer];
[pathOverlay.layer addSublayer:shapeLayer];
path = [UIBezierPath bezierPath];
[path moveToPoint:location];
}
else if (gesture.state == UIGestureRecognizerStateChanged)
{
[path addLineToPoint:location];
shapeLayer.path = [path CGPath];
}
else if (gesture.state == UIGestureRecognizerStateEnded)
{
// MKMapView *mapView = (MKMapView *)gesture.view;
[path addLineToPoint:location];
[path closePath];
allStations = [RoadmapData sharedInstance].data;
for (int i=0; i<[allStations count]; i++) {
NSDictionary * itemNo = [allStations objectAtIndex:i];
NSString * fullAddress = [NSString stringWithFormat:#"%#,%#,%#,%#",[itemNo objectForKey:#"address"],[itemNo objectForKey:#"city"],[itemNo objectForKey:#"state"],[itemNo objectForKey:#"zip"]];
CLGeocoder * geoCoder = [[CLGeocoder alloc]init];
[geoCoder geocodeAddressString:fullAddress completionHandler:^(NSArray *placemarks, NSError *error) {
if (error) {
NSLog(#"Geocode failed with error: %#", error);
return;
}
if(placemarks && placemarks.count > 0)
{
CLPlacemark *placemark = placemarks[0];
CLLocation *location = placemark.location;
CLLocationCoordinate2D coords = location.coordinate;
CGPoint loc = [_mapView convertCoordinate:coords toPointToView:_pathOverlay];
if ([path containsPoint:loc])
{
NSString * name = [itemNo objectForKey:#"name"];
stationAnn = [[LocationAnnotation alloc]initWithCoordinate:coords Title:name subTitle:#"Wells Fargo Offer" annIndex:i];
stationAnn.tag = i;
[_mapView addAnnotation:stationAnn];
}
else{
NSLog(#"Out of boundary");
}
}
}];
[self turnOffGesture:gesture];
}
}
}
- (void)mapView:(MKMapView *)aMapView didAddAnnotationViews:(NSArray *)views{
if (views.count > 0) {
UIView* firstAnnotation = [views objectAtIndex:0];
UIView* parentView = [firstAnnotation superview];
if (_pathOverlay == nil){
// create a transparent view to add bezier paths to
pathOverlay = [[UIView alloc] initWithFrame: parentView.frame];
pathOverlay.opaque = NO;
pathOverlay.backgroundColor = [UIColor clearColor];
[parentView addSubview:pathOverlay];
}
// make sure annotations stay above pathOverlay
for (UIView* view in views) {
[parentView bringSubviewToFront:view];
}
}
}
Also once i go back from this and view and come again its not even drawing the Path.
Please help.
Thanks,
Apparently, when you add your bezier path to the map via:
[_mapView.layer addSublayer:shapeLayer];
it is getting added above some internal layer that MKMapView uses to draw the annotations. If you take a look at this somewhat related question, you'll see that you can implement the MKMapViewDelegate protocol, and get callbacks when new station annotations are added. When this happens, you basically inspect the view heirarchy of the newly added annotations, and insert a new, transparent UIView layer underneath them. You take care to bring all the annotations in front of this transparent UIView.
// always remember to assign the delegate to get callbacks!
_mapView.delegate = self;
...
#pragma mark - MKMapViewDelegate
- (void)mapView:(MKMapView *)aMapView didAddAnnotationViews:(NSArray *)views{
if (views.count > 0) {
UIView* firstAnnotation = [views objectAtIndex:0];
UIView* parentView = [firstAnnotation superview];
// NOTE: could perform this initialization in viewDidLoad, too
if (self.pathOverlay == nil){
// create a transparent view to add bezier paths to
pathOverlay = [[UIView alloc] initWithFrame: parentView.frame];
pathOverlay.opaque = NO;
pathOverlay.backgroundColor = [UIColor clearColor];
[parentView addSubview:pathOverlay];
}
// make sure annotations stay above pathOverlay
for (UIView* view in views) {
[parentView bringSubviewToFront:view];
}
}
}
Then, instead of adding your shape layer to _mapView.layer, you add it to your transparent view layer, also using this new layer in the coordinate conversion:
- (void)handleGesture:(UIPanGestureRecognizer*)gesture
{
CGPoint location = [gesture locationInView: self.pathOverlay];
if (gesture.state == UIGestureRecognizerStateBegan)
{
if (!shapeLayer)
{
shapeLayer = [[CAShapeLayer alloc] init];
shapeLayer.fillColor = [[UIColor clearColor] CGColor];
shapeLayer.strokeColor = [[UIColor greenColor] CGColor];
shapeLayer.lineWidth = 5.0;
[pathOverlay.layer addSublayer:shapeLayer]; // <- change here !!!
}
self.path = [[UIBezierPath alloc] init];
[path moveToPoint:location];
}
else if (gesture.state == UIGestureRecognizerStateChanged)
{
[path addLineToPoint:location];
shapeLayer.path = [path CGPath];
}
else if (gesture.state == UIGestureRecognizerStateEnded)
{
/*
* This code is the same as what you already have ...
*/
// But replace this next line with the following line ...
//CGPoint loc = [_mapView convertCoordinate:coords toPointToView:self];
CGPoint loc = [_mapView convertCoordinate:coords toPointToView: self.pathOverlay];
/*
* And again use the rest of your original code
*/
}
}
where I also added an ivar (and property) for the new transparent layer:
UIView* pathOverlay;
I tested this with a bogus grid of stations and got the following results:
P.S. I'd also recommend getting rid of your static variables. Just make them ivars/properties of your class.
I have created some overlays:
//additionally draw an overlay
MKCircle *circle = [MKCircle circleWithCenterCoordinate:choosenCountry.coordinate radius:choosenCountry.placeMark.region.radius/4];
circle.title = #"test";
[_mapView addOverlay:circle];
and:
-(MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id)overlay
{MKCircleView* circleView = [[MKCircleView alloc] initWithOverlay:overlay];
circleView.fillColor = [[UIColor cyanColor] colorWithAlphaComponent:0.2];
circleView.strokeColor = [[UIColor blueColor] colorWithAlphaComponent:0.7];
circleView.lineWidth = 2;
return circleView;}
But now, somehow I need to remove them but I can't:
- (void)clearOverlays{
NSArray *overlayCountries = [self.mapView overlays];
[self.mapView removeOverlays:overlayCountries];
}
Do you know, how this can be done? Thanks!