how to customize MKPolyLineView to draw different style lines - iphone

I want to customize the lines drawn on MKMapView to show a route so that the lines have a border color and a fill color. Similar to this where it has a black border and is filled with another color:
I'm currently just returning MKPolyLineView objects from mapView:viewForOverlay: which works fine for plain lines. The docs says the MKPolyLineView is not to be subclassed, so should I subclass MKOverlayView and implement my own drawMapRect? Or should I subclass MKOverlayPathView? Or create a replacement for MKPolylineView?
EDIT - what I'm asking is: where is the place to put your own Quartz drawing code in order to draw your own annotations/overlays? Currently I've created a subclass of MKOverlayView and implement my own drawMapRect:zoomScale:inContext: It's pretty easy to draw the overlay that way but is that the best solution?

You can do this by implementing your own MKOverlayPathView subclass, which draws the path twice in the map rect. Once thicker with black and once thinner on top with another colour.
I have created a simple drop-in replacement of MKPolylineView which lets you do that: ASPolylineView.
If you want to do it yourself, the two main methods that you need to implement could look like this:
- (void)drawMapRect:(MKMapRect)mapRect
zoomScale:(MKZoomScale)zoomScale
inContext:(CGContextRef)context
{
UIColor *darker = [UIColor blackColor];
CGFloat baseWidth = self.lineWidth / zoomScale;
// draw the dark colour thicker
CGContextAddPath(context, self.path);
CGContextSetStrokeColorWithColor(context, darker.CGColor);
CGContextSetLineWidth(context, baseWidth * 1.5);
CGContextSetLineCap(context, self.lineCap);
CGContextStrokePath(context);
// now draw the stroke color with the regular width
CGContextAddPath(context, self.path);
CGContextSetStrokeColorWithColor(context, self.strokeColor.CGColor);
CGContextSetLineWidth(context, baseWidth);
CGContextSetLineCap(context, self.lineCap);
CGContextStrokePath(context);
[super drawMapRect:mapRect zoomScale:zoomScale inContext:context];
}
- (void)createPath
{
// turn the polyline into a path
CGMutablePathRef path = CGPathCreateMutable();
BOOL pathIsEmpty = YES;
for (int i = 0; i < self.polyline.pointCount; i++) {
CGPoint point = [self pointForMapPoint:self.polyline.points[i]];
if (pathIsEmpty) {
CGPathMoveToPoint(path, nil, point.x, point.y);
pathIsEmpty = NO;
} else {
CGPathAddLineToPoint(path, nil, point.x, point.y);
}
}
self.path = path;
}

You can just add two MKPolyLineView objects with the same coordinates, but different thicknesses.
Add one with a lineWidth of 10 (or whatever) with strokeColor set to black.
Then add another with a lineWidth of 6 with strokeColor set to your other desired color.
You can use the same MKPolyLine for both MKPolyLineView objects.

MKPolylineView can only be used for stroking a designated path. You can use some of the properties in MKOverlayPathView to change their appearance but only some of them would apply, e.g. fillColor, strokeColor.
If you want to draw something more complex, you can use MKOverlayPathView. It is more generic and thus suited for more than just stroking paths. For drawing simple lines, the result would be identical to MKPolylineView (at least, according to the docs).
If you want to do more complex drawing, subclass MKOverlayPathView. What you're trying to do is non-trivial.

I use a subclass NamedOverlay that holds an overlay an a name:
NamedOverlay.h
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
#interface NamedOverlay : NSObject <MKOverlay>
#property (strong, readonly, nonatomic) NSString *name;
#property (strong, readonly, nonatomic) id<MKOverlay> overlay;
-(id)initWithOverlay:(id<MKOverlay>)overlay andName:(NSString *)name;
#end
NamedOverlay.m
#import "NamedOverlay.h"
#implementation NamedOverlay
- (id)initWithOverlay:(id<MKOverlay>)overlay andName:(NSString *)name
{
_name = name;
_overlay = overlay;
return self;
}
- (MKMapRect)boundingMapRect
{
return [_overlay boundingMapRect];
}
- (CLLocationCoordinate2D)coordinate
{
return [_overlay coordinate];
}
-(BOOL)intersectsMapRect:(MKMapRect)mapRect
{
return [_overlay intersectsMapRect:mapRect];
}
#end
and in the map controller I instantiate two overlays with different name, then in the MKMapViewDelegate I can identify which overlay I want to draw and do something like:
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id < MKOverlay >)overlay
{
NamedOverlay *namedOverlay = (NamedOverlay *) overlay;
MKPolyline *polyline = namedOverlay.overlay;
if ([namedOverlay.name isEqualToString:#"top"]) {
MKPolylineView *view1 = [[MKPolylineView alloc] initWithOverlay:polyline];
view1.strokeColor = [UIColor whiteColor];
view1.lineWidth = 25.0;
return view1;
} else {
MKPolylineView *view1 = [[MKPolylineView alloc] initWithOverlay:polyline];
view1.strokeColor = [UIColor blueColor];
view1.lineWidth = 15.0;
return view1;
}
}

I know that this may not match the pure approach you want, but why not using MKPolygon instead of a MKPolyLine ?
Create a MKPolygon instance that represents a kind of corridor around your route, and then , when you create the MKPolygonView that corresponds to the MKPolygon/corridor you've created, set the properties of the MKPolygonView to get a different fill color and strokeColor
myPolygonView.lineWidth=3;
myPolygonView.fillColor=[UIColor blueColor];
myPolygonView.strokeColor=[UIColor darkGrayColor];
I didn't try it myself but this should work. Only drawback is that when you zoom in / out, the 'width' of the 'route' will change.... :/

Related

Draw lines with centers without them overlap

I have a problem when i try to join lines. Here is a picture:
and I like that it looks like this:
and my code is:
- (void) drawRect:(CGRect)rect {
[super drawRect:rect];
CGContextRef context = UIGraphicsGetCurrentContext();
const CGFloat *components = CGColorGetComponents(self.lineColor.CGColor);
CGFloat red;
CGFloat green;
CGFloat blue;
CGFloat alpha;
if(CGColorGetNumberOfComponents(self.lineColor.CGColor) == 2)
{
red = 1;
green = 1;
blue = 1;
alpha = 1;
}
else
{
red = components[0];
green = components[1];
blue = components[2];
alpha = components[3];
if (alpha <= 0) alpha = 1;
}
// set the stroke color and width
CGContextSetRGBStrokeColor(context, red, green, blue, alpha);
CGContextSetLineWidth(context, 2.0);
if (self.points.count >0) {
BezierPoint *firstPoint = [self.points objectAtIndex:0];
CGContextMoveToPoint(context, firstPoint.center.x, firstPoint.center.y);
int index = 0;
for (BezierPoint *point in self.points ) {
if(index == 0){
index++;
continue;
}
CGContextAddLineToPoint(context, point.center.x, point.center.y);
}
CGContextAddLineToPoint(context, firstPoint.center.x, firstPoint.center.y);
}
CGContextSetRGBFillColor(context, 0.0, 0.0, 0.0, 0.0);
CGContextDrawPath(context, kCGPathFillStroke);
}
the problem I have is that for every point you add, the lines overlap and I would like that as I stay I add points for geometric figure that globulins do not overlap
If anyone can help me I will thank!!
I would move the logic that determines the order the lines are drawn to your view controller, and access the data needed to draw the view with from a delegate using a protocol. Views should not own their data. For example, in your .h try something like;
#import <UIKit/UIKit.h>
#protocol CowDrawViewDataSource
-(NSArray *) pointsToDraw;
#end
#interface CowDrawView : UIView
#property (nonatomic , weak) id <CowDrawViewDataSource> dataSource;
#end
Then, (and I hope this does answer you question) in the view controller, you set as your views delegate, construct the
-(NSArray *) pointsToDraw;
method, in such a way, to send your array of points in an order in from which they can be drawn. Say by finding the point the top/left point, then the top/right, then bottom/right, bottom/left. (although such an approach might not work so well with irregular polygons, and shapes that are contiguous, but not polygons.)
After you figure out how to get the array in the order you want, you can get it in your drawRect by sending a message to it's delegate such as
NSArray *points = [self.dataSource pointToDraw];
with little re-work in the drawRect it's self.
The easy way to correct your problem is first draw only single line segments between two points and see the results. Then you can form loops. I guess you are not assigning perfect coordinates points or your loop must consist of :
CGContextAddLineToPoint(context, point.center.x, point.center.y);
And then,
CGContextMoveToPoint(context, point.center.x, point.center.y);
This shows that first draw line and move to next point.
Hope this helps.

graphic context of rotated layers ignores any transformations

I'm composing a view by several layers, some of them rotated. The display of these layers works perfectly, however when I try to make an image, all rotations/transformations are ignored.
For example if I have a CALayer containing an image of an arrow pointing to the left, after a rotation around the z axis (using CATransform3DMakeRotation) it points to the top, and the layer is correctly displayed. However if I get an image of this layer (using renderInContext), the arrow still points to the left, ignoring any transformations.
Anyone any idea why, and what do I have to do to get my wanted result? :-)
Example code:
// ViewController.m
#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
// Don't forget to add the QuartzCore framework to your target.
#interface ViewController ()
#end
#implementation ViewController
- (void)loadView {
self.view = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.view.backgroundColor = [UIColor lightGrayColor];
}
- (void)viewDidLoad {
[super viewDidLoad];
// Get the arrow image (tip to the left, 50 x 50 pixels).
// (Download the image from here: http://www.4shared.com/photo/uQfor7vC/arrow.html )
UIImage *arrowImage = [UIImage imageNamed:#"arrow"];
// The arrow layer in it's starting orientation (tip to the left).
CGRect layerFrame = CGRectMake(50.0, 50.0, arrowImage.size.width, arrowImage.size.height);
CALayer *arrowLayerStart = [CALayer new];
arrowLayerStart.frame = layerFrame;
[arrowLayerStart setContents:(id)[arrowImage CGImage]];
[self.view.layer addSublayer:arrowLayerStart];
// The arrow layer rotated around the z axis by 90 degrees.
layerFrame.origin = CGPointMake(layerFrame.origin.x + layerFrame.size.width + 25.0, layerFrame.origin.y);
CALayer *arrowLayerZ90 = [CALayer new];
arrowLayerZ90.frame = layerFrame;
[arrowLayerZ90 setContents:(id)[arrowImage CGImage]];
arrowLayerZ90.transform = CATransform3DMakeRotation(M_PI/2.0, 0.0, 0.0, 1.0);
[self.view.layer addSublayer:arrowLayerZ90];
// Now make images of each of these layers and display them in a second row.
// The starting layer without any rotation.
UIGraphicsBeginImageContext(arrowLayerStart.frame.size);
[arrowLayerStart renderInContext:UIGraphicsGetCurrentContext()];
UIImage *arrowStartImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
layerFrame.origin = CGPointMake(arrowLayerStart.frame.origin.x,
arrowLayerStart.frame.origin.y + arrowLayerStart.frame.size.height + 25.0);
CALayer *arrowLayerStartCaptured = [CALayer new];
arrowLayerStartCaptured.frame = layerFrame;
[arrowLayerStartCaptured setContents:(id)[arrowStartImage CGImage]];
[self.view.layer addSublayer:arrowLayerStartCaptured];
// The second layer, rotated around the z axis by 90 degrees.
UIGraphicsBeginImageContext(arrowLayerZ90.frame.size);
[arrowLayerZ90 renderInContext:UIGraphicsGetCurrentContext()];
UIImage *arrowZ90Image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
layerFrame.origin = CGPointMake(arrowLayerZ90.frame.origin.x,
arrowLayerZ90.frame.origin.y + arrowLayerZ90.frame.size.height + 25.0);
CALayer *arrowLayerZ90Captured = [CALayer new];
arrowLayerZ90Captured.frame = layerFrame;
[arrowLayerZ90Captured setContents:(id)[arrowZ90Image CGImage]];
[self.view.layer addSublayer:arrowLayerZ90Captured];
}
#end
// ViewController.h
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController
#end
// AppDelegate.h
#import <UIKit/UIKit.h>
#interface AppDelegate : UIResponder <UIApplicationDelegate>
#property (strong, nonatomic) UIWindow *window;
#end
// AppDelegate.m
#import "AppDelegate.h"
#import "ViewController.h"
#implementation AppDelegate
#synthesize window = _window;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.window.rootViewController = [[ViewController alloc] init];
[self.window makeKeyAndVisible];
return YES;
}
#end
The documentation for -[CALayer renderInContext:] says:
The Mac OS X v10.5 implementation of this method does not
support the entire Core Animation composition model.
QCCompositionLayer, CAOpenGLLayer, and QTMovieLayer layers are not
rendered. Additionally, layers that use 3D transforms are not
rendered, nor are layers that specify backgroundFilters, filters,
compositingFilter, or a mask values. Future versions of Mac OS X may
add support for rendering these layers and properties.
(This limitation is present on iOS, too.)
The header CALayer.h also says:
* WARNING: currently this method does not implement the full
* CoreAnimation composition model, use with caution. */
Essentially, -renderInContext: is useful in a few simple cases, but will not work as expected in more complicated situations. You will need to find some other way to do your drawing. Quartz (Core Graphics) will work for 2D, but for 3D you're on your own.
You can use Core Graphaic instead of CATransform3DMakeRotation :)
CGAffineTransform flip = CGAffineTransformMakeScale(1.0, -1.0);

Problem with pdf while opening in UIWebView

I have a problem while opening a pdf in a UIWebView. Zoom in and Zoom out doesn't work and even double tap doesn't enlarge the pdf font size.
Guys is there any way to do that....
If not can anyone share some code ....
#import
#interface TiledPDFView : UIView {
CGPDFPageRef pdfPage;
CGFloat myScale;
}
- (id)initWithFrame:(CGRect)frame andScale:(CGFloat)scale;
- (void)setPage:(CGPDFPageRef)newPage;
#end
#import "TiledPDFView.h"
#import
#implementation TiledPDFView
// Create a new TiledPDFView with the desired frame and scale.
- (id)initWithFrame:(CGRect)frame andScale:(CGFloat)scale{
if ((self = [super initWithFrame:frame])) {
CATiledLayer *tiledLayer = (CATiledLayer *)[self layer];
tiledLayer.levelsOfDetail = 4;
tiledLayer.levelsOfDetailBias = 4;
tiledLayer.tileSize = CGSizeMake(512.0, 512.0);
myScale = scale;
}
return self;
}
// Set the layer's class to be CATiledLayer.
+ (Class)layerClass {
return [CATiledLayer class];
}
// Set the CGPDFPageRef for the view.
- (void)setPage:(CGPDFPageRef)newPage
{
CGPDFPageRelease(self->pdfPage);
self->pdfPage = CGPDFPageRetain(newPage);
}
-(void)drawRect:(CGRect)r
{
// UIView uses the existence of -drawRect: to determine if it should allow its CALayer
// to be invalidated, which would then lead to the layer creating a backing store and
// -drawLayer:inContext: being called.
// By implementing an empty -drawRect: method, we allow UIKit to continue to implement
// this logic, while doing our real drawing work inside of -drawLayer:inContext:
}
// Draw the CGPDFPageRef into the layer at the correct scale.
-(void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context
{
// First fill the background with white.
CGContextSetRGBFillColor(context, 1.0,1.0,1.0,0.5);
CGContextFillRect(context,self.bounds);
CGContextSaveGState(context);
// Flip the context so that the PDF page is rendered
// right side up.
CGContextTranslateCTM(context, 0.0, self.bounds.size.height);
// Scale the context so that the PDF page is rendered
// at the correct size for the zoom level.
CGContextScaleCTM(context, myScale,myScale);
CGContextDrawPDFPage(context, pdfPage);
CGContextRestoreGState(context);
}
// Clean up.
- (void)dealloc {
CGPDFPageRelease(pdfPage);
[super dealloc];
}
#end
Add this to your project...
Hope it helped....
Make sure that «Multiple Touch» is enabled for the UIWebView.

Inheriting UIView - Losing instance variables

Bit of an Objective-C rookie, I've looked around for an answer but haven't been able to find one so forgive me if this is an obvious question.
Basically, I need to draw on the screen segments of a circle (for instance, a 90 degree segment, where a horizontal and vertical line meet at the bottom left and an arc connects the end points). I've managed to achieve this in a custom class called CircleSegment that inherits UIView and overrides drawRect.
My issue is achieving this programatically; I need some way of creating my CircleSegment class and storing in it the desired angle before it draws the segment itself.
Here's what I have so far:
CircleSegment.h
#import <UIKit/UIKit.h>
#interface CircleSegment : UIView {
float angleSize;
UIColor *backColor;
}
-(float)convertDegToRad:(float)degrees;
-(float)convertRadToDeg:(float)radians;
#property (nonatomic) float angleSize;
#property (nonatomic, retain) UIColor *backColor;
#end
CircleSegment.m
#import "CircleSegment.h"
#implementation CircleSegment
#synthesize angleSize;
#synthesize backColor;
// INITIALISATION OVERRIDES
// ------------------------
- (id)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
self.opaque = NO;
self.backgroundColor = [UIColor clearColor];
}
return self;
}
- (void)setBackgroundColor:(UIColor *)newBGColor
{
// Ignore.
}
- (void)setOpaque:(BOOL)newIsOpaque
{
// Ignore.
}
// MATH FUNCTIONS
// --------------
// Converts degrees to radians.
-(float)convertDegToRad:(float)degrees {
return degrees * M_PI / 180;
}
// Converts radians to degrees.
-(float)convertRadToDeg:(float)radians {
return radians * 180 / M_PI;
}
// DRAWING CODE
// ------------
- (void)drawRect:(CGRect)rect {
float endAngle = 360 - angleSize;
UIBezierPath* aPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(100, 100)
radius:100
startAngle:[self convertDegToRad:270]
endAngle:[self convertDegToRad:endAngle]
clockwise:YES];
[aPath addLineToPoint:CGPointMake(100.0, 100.0)];
[aPath closePath];
CGContextRef aRef = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(aRef, backColor.CGColor);
CGContextSaveGState(aRef);
aPath.lineWidth = 1;
[aPath fill];
[aPath stroke];
//CGContextRestoreGState(aRef);
}
- (void)dealloc {
[super dealloc];
}
#end
Note that the .m file is a bit messy with various bits of test code...
So essentially I want to create an instance of CircleSegment, store an angle in the angleSize property, draw a segment based on that angle and then add that view to the main app view to display it...
To try and achieve that, I've add the following test code to viewDidLoad on my ViewController:
CircleSegment *seg1 = [[CircleSegment alloc] init];
seg1.backColor = [UIColor greenColor];
seg1.angleSize = 10;
[self.view addSubview:seg1];
It seems to store the UIColor and angleSize fine when I breakpoint those places, however if I place a breakpoint in drawRect which I overrode on CircleSegment.m, the values have reverted back to nil values (or whatever the correct term would be, feel free to correct me).
I'd really appreciate it if someone could point me in the right direction!
Thanks
It seems to store the UIColor and
angleSize fine when I breakpoint those
places, however if I place a
breakpoint in drawRect which I
overrode on CircleSegment.m, the
values have reverted back to nil
values (or whatever the correct term
would be, feel free to correct me).
Are you sure you are working with the same instance?
Drop in something like NSLog(#"%# %p", NSStringFromSelector(_cmd), self); in viewDidLoad and in drawRect and make sure you are actually dealing with the same instance.
(I've seen cases where a developer will alloc/initWithFrame: a view and then use the instance that was created during NIB loading, wondering why that instance wasn't properly initialized.)
Try using self.angleSize in your DrawRect method (same with backColor). In your test method, replace 10 with 10.0 (to ensure it's setting it as a float).
Also, don't forget to release backColor in your dealloc.

Effect like Interface Builder connection lines on iPhone

Hi I'd like little bit of help on something. In my app, I have a UITableView which is populated with custom cells that represent images. (i.e. selecting a row displays a picture in an image view).
What I would like to do, is have an icon in the custom cell that I could drag to one of a series of image views. Similar to the way you can drag a line in IB to set connections. Once the user releases their finger I will have it check what part of the screen they released it and if it is one one of these rects that represent the picture frames, it will populate the picture frame with the image and the line will disappear.
I have never drawn lines in my app before so thats not something I know how to do (so im just looking for a link to a tutorial or class definition) and second, what problems will I have since the start point of the line is in a UITableViewCell and the end point is in the main view?
I have actually done this before, so I can give you the exact code :D
This is only the drawing part, you implement the touch events (in a separate class, otherwise remove the self.userInteractionEnabled = NO; in .m file
The BOOL dragged tells the LineView if the line should be drawn.
LineView.h
#import <UIKit/UIKit.h>
#interface LineView : UIView
{
CGPoint start;
CGPoint end;
BOOL dragged;
}
#property CGPoint start, end;
#property BOOL dragged;
#end
LineView.m
#import "LineView.h"
#implementation LineView
#synthesize start, end, dragged;
- (id)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame: frame])
{
self.userInteractionEnabled = NO;
self.backgroundColor = [UIColor clearColor];
}
return self;
}
#pragma mark Setters
-(void) setStart: (CGPoint) s
{
start = s;
[self setNeedsDisplay];
}
-(void) setEnd: (CGPoint) e
{
end = e;
[self setNeedsDisplay];
}
#define LINE_COLOR [UIColor redColor]
#define CIRCLE_COLOR [UIColor redColor]
#define LINE_WIDTH 5
#define CIRCLE_RADIUS 10
- (void)drawRect:(CGRect)rect
{
CGContextRef c = UIGraphicsGetCurrentContext();
if(dragged) {
[LINE_COLOR setStroke];
CGContextMoveToPoint(c, start.x, start.y);
CGContextAddLineToPoint(c, end.x, end.y);
CGContextSetLineWidth(c, LINE_WIDTH);
CGContextClosePath(c);
CGContextStrokePath(c);
[CIRCLE_COLOR setFill];
CGContextAddArc(c, start.x, start.y, CIRCLE_RADIUS, 0, M_PI*2, YES);
CGContextClosePath(c);
CGContextFillPath(c);
CGContextAddArc(c, end.x, end.y, CIRCLE_RADIUS, 0, M_PI*2, YES);
CGContextClosePath(c);
CGContextFillPath(c);
}
}
- (void)dealloc
{
[super dealloc];
}
#end