Scaling MKMapView Annotations relative to the zoom level - objective-c

The Problem
I'm trying to create a visual radius circle around a annonation, that remains at a fixed size in real terms. Eg. So If i set the radius to 100m, as you zoom out of the Map view the radius circle gets progressively smaller.
I've been able to achieve the scaling, however the radius rect/circle seems to "Jitter" away from the Pin Placemark as the user manipulates the view.
I'm lead to believe this is much easier to achieve on the forthcoming iPhone OS 4, however my application needs to support 3.0.
The Manifestation
Here is a video of the behaviour.
The Implementation
The annotations are added to the Mapview in the usual fashion, and i've used the delegate method on my UIViewController Subclass (MapViewController) to see when the region changes.
-(void)mapView:(MKMapView *)pMapView regionDidChangeAnimated:(BOOL)animated{
//Get the map view
MKCoordinateRegion region;
CGRect rect;
//Scale the annotations
for( id<MKAnnotation> annotation in [[self mapView] annotations] ){
if( [annotation isKindOfClass: [Location class]] && [annotation conformsToProtocol:#protocol(MKAnnotation)] ){
//Approximately 200 m radius
region.span.latitudeDelta = 0.002f;
region.span.longitudeDelta = 0.002f;
region.center = [annotation coordinate];
rect = [[self mapView] convertRegion:region toRectToView: self.mapView];
if( [[[self mapView] viewForAnnotation: annotation] respondsToSelector:#selector(setRadiusFrame:)] ){
[[[self mapView] viewForAnnotation: annotation] setRadiusFrame:rect];
}
}
}
The Annotation object (LocationAnnotationView)is a subclass of the MKAnnotationView and it's setRadiusFrame looks like this
-(void) setRadiusFrame:(CGRect) rect{
CGPoint centerPoint;
//Invert
centerPoint.x = (rect.size.width/2) * -1;
centerPoint.y = 0 + 55 + ((rect.size.height/2) * -1);
rect.origin = centerPoint;
[self.radiusView setFrame:rect];
}
And finally the radiusView object is a subclass of a UIView, that overrides the drawRect method to draw the translucent circles. setFrame is also over ridden in this UIView subclass, but it only serves to call [UIView setNeedsDisplay] in addition to [UIView setFrame:] to ensure that the view is redrawn after the frame has been updated.
The radiusView object's (CircleView) drawRect method looks like this
-(void) drawRect:(CGRect)rect{
//NSLog(#"[CircleView drawRect]");
[self setBackgroundColor:[UIColor clearColor]];
//Declarations
CGContextRef context;
CGMutablePathRef path;
//Assignments
context = UIGraphicsGetCurrentContext();
path = CGPathCreateMutable();
//Alter the rect so the circle isn't cliped
//Calculate the biggest size circle
if( rect.size.height > rect.size.width ){
rect.size.height = rect.size.width;
}
else if( rect.size.height < rect.size.width ){
rect.size.width = rect.size.height;
}
rect.size.height -= 4;
rect.size.width -= 4;
rect.origin.x += 2;
rect.origin.y += 2;
//Create paths
CGPathAddEllipseInRect(path, NULL, rect );
//Create colors
[[self areaColor] setFill];
CGContextAddPath( context, path);
CGContextFillPath( context );
[[self borderColor] setStroke];
CGContextSetLineWidth( context, 2.0f );
CGContextSetLineCap(context, kCGLineCapSquare);
CGContextAddPath(context, path );
CGContextStrokePath( context );
CGPathRelease( path );
//CGContextRestoreGState( context );
}
Thanks for bearing with me, any help is appreciated.
Jonathan

First, what's foo used in the first function? And I'm assuming radiusView's parent is the annotation view, right?
The "Jittering"
Also, the center point of radiusView should coincide with that of the annotationView. This should solve your problem:
-(void) setRadiusFrame:(CGRect)rect{
rect.origin.x -= 0.5*(self.frame.size.width - rect.size.width);
rect.origin.y -= 0.5*(self.frame.size.height - rect.size.height);
[self.radiusView setFrame:rect]
}
Unnecessary method
You could set the frame directly on the radiusView and avoid the above calculation:
UIView * radiusView = [[[self mapView] viewForAnnotation: annotation] radiusView];
rect = [[self mapView] convertRegion:foo toRectToView: radiusView.superView];
[radiusView setFrame:rect];
When drawing the ellipse, don't use the rect passed to drawRect:, it doesn't have to be the same as the frame. It's safer to directly use self.frame
Unnecessary view
Now I gave the above points if you need to use the above hierarchy, but I don't see why don't you just draw your ellipses directly in the LocationAnnotationView? It's there for this purpose after all. This is how you do this:
when scaling, change the annotationView's rect directly:
rect = [[self mapView] convertRegion:foo toRectToView: self.mapView];
[[[self mapView] viewForAnnotation: annotation] setFrame:rect];
Move the implementation of drawRect: to LocationAnnotationView.
This is easier to implement, and should address your problem as the center point of the annotation view moves with your pin and you shouldn't see this problem.
Fixes
There are two other issues in the code:
Set longitudeDelta like this:
region.span.longitudeDelta = 0.002*cos(region.center.latitude*pi/180.0);
as the longitude delta converted to meters changes with the latitude. Alternatively, you could only set latitude delta, then modify the rect so it becomes rectangular (width==height).
in drawRect:, don't use the passed rect; instead use self.frame. There's no guarantee that these are the same, and rect could have any value.
Let me know if these work ;-)

Related

SKShapeNode.Get Radius?

Is it possible to get an SKShapeNode's radius value?
The problem is, I need to create an identical circle after the user releases it, whilst removing the first circle from the view.
SKShapeNode *reticle = [SKShapeNode shapeNodeWithCircleOfRadius:60];
reticle.fillColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.3];
reticle.strokeColor = [UIColor clearColor];
reticle.position = CGPointMake(location.x - 50, location.y + 50);
reticle.name = #"reticle";
reticle.userInteractionEnabled = YES;
[self addChild:reticle];
[reticle runAction:[SKAction scaleTo:0.7 duration:3] completion:^{
[reticle runAction:[SKAction scaleTo:0.1 duration:1]];
}];
Then
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
....
SKNode *node = [self childNodeWithName:#"reticle"];
//take the position of the node, draw an imprint
/////////Here is where I need to get the circle radius.
[node removeFromParent];
}
How does one take the circle radius, so I can then say
SKShapeNode *imprint = [SKShapeNode shapeNodeWithCircleOfRadius:unknownValue];
I've tried, making an exact copy of the circle with
SKShapeNode *imprint = (SKShapeNode *)node;
However, this still follows the animation where as I need it to stop, at the point it was at. I don't want to take a "stopAllAnimations" approach.
You can use the shape node's path property to determine the bounding box of the shape with CGPathGetBoundingBox(path). From the bounds, you can compute the radius of the circle by dividing the width (or height) by 2. For example,
CGRect boundingBox = CGPathGetBoundingBox(circle.path);
CGFloat radius = boundingBox.size.width / 2.0;
Updated for Swift 5
myVariable.path.boundingBox.width / 2

Changing a circle's size with UIPinchGestureRecognizer

I'm drawing a simple circle in the center of the screen:
int radius = 100;
- (void)addCircle {
self.circle = [CAShapeLayer layer];
self.circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius)
cornerRadius:radius].CGPath;
self.circle.position = CGPointMake(CGRectGetMidX(self.view.frame)-radius,
CGRectGetMidY(self.view.frame)-radius);
self.circle.fillColor = [UIColor clearColor].CGColor;
self.circle.strokeColor = [UIColor blackColor].CGColor;
self.circle.lineWidth = 5;
[self.view.layer addSublayer:self.circle];
}
Using the pinch gesture, I allow the user to increase/decrease the radius of the shape:
- (void)scale:(UIPinchGestureRecognizer *)gestureRecognizer {
if (gestureRecognizer.state != UIGestureRecognizerStateBegan) {
if (gestureRecognizer.scale < lastScale) {
--radius;
}
else if (gestureRecognizer.scale > lastScale) {
++radius;
}
// Center the shape in self.view
self.circle.position = CGPointMake(CGRectGetMidX(self.view.frame)-radius, CGRectGetMidY(self.view.frame)-radius);
self.circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius) cornerRadius:radius].CGPath;
}
lastScale = gestureRecognizer.scale;
}
However, the circle doesn't stay dead center. Instead, it bounces around the middle and doesn't settle until the gesture finishes.
Does anyone know why this is happening and if so, how I can prevent it?
There are a few problems in your code. As #tc. said, you're not setting the shape layer's frame (or bounds). The default layer size is CGSizeZero, which is why you're having to offset the layer's position by the radius every time you change the radius.
Also, the position and path properties of a shape layer are animatable. So by default, when you change them, Core Animation will animate them to their new values. The path animation is contributing to your unwanted behavior.
Also, you should set the layer's position or frame based on self.view.bounds, not self.view.frame, because the layer's position/frame is the coordinate system of self.view, not the coordinate system of self.view.superview. This will matter if self.view is the top-level view and you support interface autorotation.
I would suggest revising how you're implementing this. Make radius a CGFloat property, and make setting the property update the layer's bounds and path:
#interface ViewController ()
#property (nonatomic, strong) CAShapeLayer *circle;
#property (nonatomic) CGFloat radius;
#end
#implementation ViewController
- (void)setRadius:(CGFloat)radius {
_radius = radius;
self.circle.bounds = CGRectMake(0, 0, 2 * radius, 2 * radius);
self.circle.path = [UIBezierPath bezierPathWithOvalInRect:self.circle.bounds].CGPath;
}
If you really want to force the radius to be an integer, I suggest internally tracking it as a float anyway, because the user interaction is smoother if it's a float. Just round it in a temporary variable before creating the CGRect for the bounds and path:
CGFloat intRadius = roundf(radius);
self.circle.bounds = CGRectMake(0, 0, 2 * intRadius, 2 * intRadius);
self.circle.path = [UIBezierPath bezierPathWithOvalInRect:self.circle.bounds].CGPath;
In addCircle, just set the radius property and let that setter take care of setting the layer's bounds and path. Also defer setting the layer's position until the system's layout phase. That way, you'll reposition the circle in the center again after an interface rotation.
- (void)viewDidLoad {
[super viewDidLoad];
[self addCircle];
}
- (void)addCircle {
self.circle = [CAShapeLayer layer];
self.circle.fillColor = nil;
self.circle.strokeColor = [UIColor blackColor].CGColor;
self.circle.lineWidth = 5;
self.radius = 100;
[self.view.layer addSublayer:self.circle];
[self.view setNeedsLayout];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
self.circle.position = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds));
}
Finally, to handle a pinch gesture, just set the new radius to the old radius times the gesture's scale. The radius setter will take care of updating the layer's path and bounds. Then reset the gesture's scale to 1. This is simpler than tracking the gesture's prior scale. Also, use CATransaction to disable animation of the path property.
- (IBAction)pinchGestureWasRecognized:(UIPinchGestureRecognizer *)recognizer {
[CATransaction begin]; {
[CATransaction setDisableActions:YES];
self.radius *= recognizer.scale;
recognizer.scale = 1;
} [CATransaction commit];
}

Wrong positioning of subviews

I want to arrange several custom NSViews one after the other.
But when I run the App, views are drawn with different (doubled) frame origin values, than are values set in code.
Here is simplified code with 2 views:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
float height1 = 40.0;
float height2 = 65.0;
float width = [[window contentView] frame].size.width;
NSRect r1 = NSMakeRect(0, 0, width, height1);
NSRect r2 = NSMakeRect(0, height1, width, height2);
MyView *c1 = [[MyView alloc] initWithFrame:r1];
MyView *c2 = [[MyView alloc] initWithFrame:r2];
[[window contentView] addSubview:c1];
[[window contentView] addSubview:c2];
}
code for MyView basically consists only from drawRect:
- (void)drawRect:(NSRect)dirtyRect {
// Drawing code here.
NSRect cellFrame = [self frame];
// frame Y coordinates at superview
float superY = [self convertPoint:[self frame].origin
toView:[self superview]].y;
NSLog(#"superY:%f selfY:%f", superY, cellFrame.origin.y);
// top, bottom border and diagonal line of [self frame]
NSBezierPath* borderLine = [NSBezierPath bezierPath];
NSPoint pt1 = NSMakePoint(cellFrame.origin.x,
cellFrame.origin.y);
NSPoint pt2 = NSMakePoint(cellFrame.origin.x + cellFrame.size.width,
cellFrame.origin.y);
NSPoint pt3 = NSMakePoint(cellFrame.origin.x,
cellFrame.origin.y + cellFrame.size.height);
NSPoint pt4 = NSMakePoint(cellFrame.origin.x + cellFrame.size.width,
cellFrame.origin.y + cellFrame.size.height);
[borderLine moveToPoint:pt1];
[borderLine lineToPoint:pt2];
[borderLine lineToPoint:pt3];
[borderLine lineToPoint:pt4];
[[NSColor redColor] setStroke];
[borderLine setLineWidth:01];
[borderLine stroke];
}
and here is the result (as you can see - 'y' coordinate of second view is doubled and for some reason, this view is only partly drawn):
result with console
You are mixing up the concept of a view's frame and bounds rectangles. "Bounds" refer's to a view's dimensions in its own coordinate system, i.e. the origin will be zero and the size will be the view's width and height.
"Frame" refers to a view's dimensions in it's parent view's coordinate system, i.e. the origin will be wherever the view is positioned in its superview, and the width and height will be the same as the bounds rectangle's.
So for the logging in your example code, you are calling "convertPoint" unnecesarily and incorrectly, because you can get the view's actual origin simply by calling "[self frame].origin"
When doing drawing, you need to call "[self bounds]" to get the rectangle in which to draw. In your code you are calling "[self frame]" which gives you a rectangle in your superview's coordinate system (frame), but that won't work because the drawing routines draw in the view's own (bounds) coordinate system (i.e. with origin at {0, 0})
An exception to this would be if a view fills the entire content of its superview, in which case you could call either [self bounds] or [self frame], since both would return the same rectangle.
I got your code to work by changing
NSRect cellFrame = [self frame];
to
NSRect cellFrame = [self bounds];
Also, the easiest way to log an NSRect is
NSLog(#"%#", NSStringFromRect([self frame])); for example.
Hope that helps.

Move NSView around until it hits a border

In a Cocoa-based App i'm having a canvas for drawing, inherited from NSView, as well as a rectangle, also inherited from NSView. Dragging the rectangle around inside of the canvas is no problem:
-(void)mouseDragged:(NSEvent *)theEvent {
NSPoint myOrigin = self.frame.origin;
[self setFrameOrigin:NSMakePoint(myOrigin.x + [theEvent deltaX],
myOrigin.y - [theEvent deltaY])];
}
Works like a charm. The issue i'm having now: How can i prevent the rectangle from being moved outside the canvas?
So, first of all i would like to fix this just for the left border, adapting the other edges afterwards. My first idea is: "check whether the x-origin of the rectangle is negative". But: once it is negative the rectangle can't be moved anymore around (naturally). I solved this with moving the rectangle to zero x-offset in the else-branch. This works but it's ... ugly.
So i'm little puzzled with this one, any hints? Definitely the solution is really near and easy. That easy, that i cannot figure it out (as always with easy solutions ;).
Regards
Macs
I'd suggest not using the deltaX and deltaY; try using the event's location in the superview. You'll need a reference to the subview.
// In the superview
- (void)mouseDragged:(NSEvent *)event {
NSPoint mousePoint = [self convertPoint:[event locationInWindow]
fromView:nil];
// Could also add the width of the moving rectangle to this check
// to keep any part of it from going outside the superview
mousePoint.x = MAX(0, MIN(mousePoint.x, self.bounds.size.width));
mousePoint.y = MAX(0, MIN(mousePoint.y, self.bounds.size.height));
// position is a custom ivar that indicates the center of the object;
// you could also use frame.origin, but it looks nicer if objects are
// dragged from their centers
myMovingRectangle.position = mousePoint;
[self setNeedsDisplay:YES];
}
You'd do essentially the same bounds checking in mouseUp:.
UPDATE: You should also have a look at the View Programming Guide, which walks you through creating a draggable view: Creating a Custom View.
Sample code that should be helpful, though not strictly relevant to your original question:
In DotView.m:
- (void)drawRect:(NSRect)dirtyRect {
// Ignoring dirtyRect for simplicity
[[NSColor colorWithDeviceRed:0.85 green:0.8 blue:0.8 alpha:1] set];
NSRectFill([self bounds]);
// Dot is the custom shape class that can draw itself; see below
// dots is an NSMutableArray containing the shapes
for (Dot *dot in dots) {
[dot draw];
}
}
- (void)mouseDown:(NSEvent *)event {
NSPoint mousePoint = [self convertPoint:[event locationInWindow]
fromView:nil];
currMovingDot = [self clickedDotForPoint:mousePoint];
// Move the dot to the point to indicate that the user has
// successfully "grabbed" it
if( currMovingDot ) currMovingDot.position = mousePoint;
[self setNeedsDisplay:YES];
}
// -mouseDragged: already defined earlier in post
- (void)mouseUp:(NSEvent *)event {
if( !currMovingDot ) return;
NSPoint mousePoint = [self convertPoint:[event locationInWindow]
fromView:nil];
spot.x = MAX(0, MIN(mousePoint.x, self.bounds.size.width));
spot.y = MAX(0, MIN(mousePoint.y, self.bounds.size.height));
currMovingDot.position = mousePoint;
currMovingDot = nil;
[self setNeedsDisplay:YES];
}
- (Dot *)clickedDotForPoint:(NSPoint)point {
// DOT_NUCLEUS_RADIUS is the size of the
// dot's internal "handle"
for( Dot *dot in dots ){
if( (abs(dot.position.x - point.x) <= DOT_NUCLEUS_RADIUS) &&
(abs(dot.position.y - point.y) <= DOT_NUCLEUS_RADIUS)) {
return dot;
}
}
return nil;
}
Dot.h
#define DOT_NUCLEUS_RADIUS (5)
#interface Dot : NSObject {
NSPoint position;
}
#property (assign) NSPoint position;
- (void)draw;
#end
Dot.m
#import "Dot.h"
#implementation Dot
#synthesize position;
- (void)draw {
//!!!: Demo only: assume that focus is locked on a view.
NSColor *clr = [NSColor colorWithDeviceRed:0.3
green:0.2
blue:0.8
alpha:1];
// Draw a nice border
NSBezierPath *outerCirc;
outerCirc = [NSBezierPath bezierPathWithOvalInRect:
NSMakeRect(position.x - 23, position.y - 23, 46, 46)];
[clr set];
[outerCirc stroke];
[[clr colorWithAlphaComponent:0.7] set];
[outerCirc fill];
[clr set];
// Draw the "handle"
NSRect nucleusRect = NSMakeRect(position.x - DOT_NUCLEUS_RADIUS,
position.y - DOT_NUCLEUS_RADIUS,
DOT_NUCLEUS_RADIUS * 2,
DOT_NUCLEUS_RADIUS * 2);
[[NSBezierPath bezierPathWithOvalInRect:nucleusRect] fill];
}
#end
As you can see, the Dot class is very lightweight, and uses bezier paths to draw. The superview can handle the user interaction.

NSSlider NSSliderCell clipping custom knob

I am creating a custom NSSlider with a custom NSSliderCell. All is working beautifully, other than the knob. When I drag it to the max value the knob is being clipped, I can only see 50% of the knob image.
When assigning my custom NSSliderCell I am setting the knobThickness to the width of the image I am using as the knob. I assumed (I guess wrongly) that it would take that into account and stop it from clipping?
Any ideas what I am doing wrong? The slider is hitting the maxValue only when the knob is clipped at 50%, so its not travelling without adding any value.
- (void)drawKnob:(NSRect)knobRect {
NSImage * knob = _knobOff;
knobRectVar = knobRect;
[[self controlView] lockFocus];
[knob
compositeToPoint:
NSMakePoint(knobRect.origin.x+4,knobRect.origin.y+knobRect.size.height+20)
operation:NSCompositeSourceOver];
[[self controlView] unlockFocus];
}
- (void)drawBarInside:(NSRect)rect flipped:(BOOL)flipped {
rect.size.height = 8;
[[self controlView] lockFocus];
NSImage *leftCurve = [NSImage imageNamed:#"customSliderLeft"];
[leftCurve drawInRect:NSMakeRect(5, 25, 8, 8) fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1];
NSRect leftRect = rect;
leftRect.origin.x=13;
leftRect.origin.y=25;
leftRect.size.width = knobRectVar.origin.x + (knobRectVar.size.width/2);
[leftBarImage setSize:leftRect.size];
[leftBarImage drawInRect:leftRect fromRect: NSZeroRect operation: NSCompositeSourceOver fraction:1];
[[self controlView] unlockFocus];
}
The NSSLider expects a special sizes off the knob images for each control size:
NSRegularControlSize: 21x21
NSSmallControlSize: 15x15
NSMiniControlSize: 12x12
Unfortunately the height of your knob image mustn't exceed one of this parameters. But it's width may be longer. If it is you may count an x position for the knob like this:
CGFloat newOriginX = knobRect.origin.x *
(_barRect.size.width - (_knobImage.size.width - knobRect.size.width)) / _barRect.size.width;
Where _barRect is a cellFrame of your bar background from:
- (void)drawBarInside:(NSRect)cellFrame flipped:(BOOL)flipped;
I've created a simple solution for the custom NSSlider. Follow this link
https://github.com/Doshipak/LADSlider
You can override [NSSliderCell knobRectFlipped:] in addition to [NSSliderCell drawKnob:].
Here is my solution:
- (void)drawKnob:(NSRect)rect
{
NSImage *drawImage = [self knobImage];
NSRect drawRect = [self knobRectFlipped:[self.controlView isFlipped]];
CGFloat fraction = 1.0;
[drawImage drawInRect:drawRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:fraction respectFlipped:YES hints:nil];
}
- (NSRect)knobRectFlipped:(BOOL)flipped
{
NSImage *drawImage = [self knobImage];
NSRect drawRect = [super knobRectFlipped:flipped];
drawRect.size = drawImage.size;
NSRect bounds = self.controlView.bounds;
bounds = NSInsetRect(bounds, ceil(drawRect.size.width / 2), 0);
CGFloat val = MIN(self.maxValue, MAX(self.minValue, self.doubleValue));
val = (val - self.minValue) / (self.maxValue - self.minValue);
CGFloat x = val * NSWidth(bounds) + NSMinX(bounds);
drawRect = NSOffsetRect(drawRect, x - NSMidX(drawRect) + 1, 0);
return drawRect;
}
Know it's been awhile but I ran into this issue myself and found a quick-and-dirty workaround.
I couldn't get around the initial reason for this but it seems that NSSlider is expecting a quadratic handle image.
The easiest way I found was to set the range of your slider to be from 0.0f - 110.0f for example.
Then you check in the valueChanged target method assigned if the value is > 100.0f and set it back to that value if it is. I created a background image with some pixels of alpha-only pixels on the right side so your background isn't wider than the actual fader range.
Quick-and-dirty but doesn't require a lot code and works pretty well.
Hope this helps other guys stumbling upon the same issue.
You don’t need to lock and unlock focus on the controlView from inside cell drawing methods. These methods are only called by your controlView’s -drawRect: method, which is called with the view’s focus locked.
Why are you adding 20 points to the Y coordinate the knob image is composited to in -drawKnob?