How to create a static, actual width MKOverlayPathRenderer subclass? - ios

I've been working on a simple path overlay to an existing MKMapView. It takes a CGMutablePath and draws it onto the map. The goals is that the path is drawn representing an actual width. e.g. The subclass takes a width in meters and converts that width into a representative line width. Here is the one version of the code that calculates the line width:
- (void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context
float mapPoints = meterWidth * MKMapPointsPerMeterAtLatitude(self.averageLatitude);
float screenPoints = mapPoints * zoomScale; // dividing would keep the apparent width constant
self.lineWidth = ceilf(screenPoints * 2);
CGContextAddPath(context, _mutablePath);
[self applyStrokePropertiesToContext:context atZoomScale:zoomScale];
CGContextStrokePath(context);
Here we first find the number of map points that correspond to our line width and then convert that to screen points. We do the conversion based on the header comments in MKGeometry.h:
// MKZoomScale provides a conversion factor between MKMapPoints and screen points.
// When MKZoomScale = 1, 1 screen point = 1 MKMapPoint. When MKZoomScale is
// 0.5, 1 screen point = 2 MKMapPoints.
Finally, we add the path to the context, apply the stroking properties to the path and stroke it.
However this gives exceedingly flakey results. The renderer often draws random fragments of line in various places outside the expected live area or doesn't draw some tiles at all. Sometimes the CPU is pegged redrawing multiple version of the same tile (as best I can tell) over and over. The docs aren't much help in this case.
I do have a working version, but it doesn't seem like the correct solution as it completely ignores zoomScale and doesn't use -applyStrokePropertiesToContext:atZoomScale:
float mapPoints = meterWidth * MKMapPointsPerMeterAtLatitude(self.averageLatitude);
self.lineWidth = ceilf(mapPoints * 2);
CGContextAddPath(context, _mutablePath);
CGContextSetStrokeColorWithColor(context, self.strokeColor.CGColor);
CGContextSetLineWidth(context, self.lineWidth);
CGContextSetLineJoin(context, kCGLineJoinRound);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextStrokePath(context);
Anyone have pointers on what is wrong with this implementation?

Related

Efficiently draw a large scrollable area

I'm working on a simple game which uses a hexagonal grid layout. The grid is very large (a few thousand pixels in width and height). I need to be able to scroll and zoom it within a scrollView, and there are a lot of individual hexagons. I have written the drawing code in CoreGraphics. The hexagons are drawn in the drawRect: method of their view. This drawing code is called for each of the hexagons:
- (void)drawInContext:(CGContextRef)context colour:(UIColor *)colour size:(CGSize)size {
CGFloat width = size.width;
CGFloat height = size.height;
CGFloat x = self.offset.x;
CGFloat y = self.offset.y;
CGContextMoveToPoint(context, (width/2)+x, y);
CGContextAddLineToPoint(context, width+x, (height / 4)+y);
CGContextAddLineToPoint(context, width+x, (height * 3 / 4)+y);
CGContextAddLineToPoint(context, (width / 2)+x, height+y);
CGContextAddLineToPoint(context, x, (height * 3 / 4)+y);
CGContextAddLineToPoint(context, x, (height / 4)+y);
CGContextClosePath(context);
CGContextSetFillColorWithColor(context, colour.CGColor);
CGContextSetStrokeColorWithColor(context, [[UIColor whiteColor] CGColor]);
CGContextDrawPath(context, kCGPathFillStroke);
NSString *text = [NSString stringWithFormat:#"I:%ld\nR:%ld\nC:%ld", self.creationIndex, self.row, self.column];
[text drawAtPoint:CGPointMake(self.offset.x+20, self.offset.y+20) withAttributes:#{NSForegroundColorAttributeName: [UIColor blackColor], NSFontAttributeName: [UIFont systemFontOfSize:[UIFont systemFontSize]]}];
}
I call setNeedsDisplay on the view when a change is needed (like a hexagon changing colour). The problem is that this seems very inefficient. It takes approximately half a second for the map to redraw, which makes everything feel sluggish.
I have tried the following:
Calculate the visible rect of the scrollView and only draw that part of it. This causes problems when zooming to a different rect, as only the destination rect is drawn, causing black space to be displayed in the part being scrolled across.
Set a flag on the hexagons to indicate that they require an update, and only drawing the hexagons which have changed. This resulted in only the changed hexagons being visible, since drawRect: seems to fill the view in black before carrying out the drawing operation, rather than leaving the previous image there and drawing the changed hexagons over the top.
Using UIKit to build the grid of hexagons. This was simply too slow, as there were hundreds of individual views.
To summarise, my question is if there is a way of optimising CoreGraphics drawing, or if there is an alternative way of drawing which is more efficient.
There should not be any need to calc the visible rect, this is done by UIScrollView.
See Scrollview Programming Guide
Furthermore from the class documentation : The object that manages the drawing of content displayed in a scroll view should tile the content’s subviews so that no view exceeds the size of the screen. As users scroll in the scroll view, this object should add and remove subviews as necessary.

How to LIGHTLY erase drawn path in cgcontext?

I managed to implement erase drawings on CGContext
UIImageView *maskImgView = [self.view viewWithTag:K_MASKIMG];
UIGraphicsBeginImageContext(maskImgView.image.size);
[maskImgView.image drawAtPoint:CGPointMake(0,0)];
float alp = 0.5;
UIImage *oriBrush = [UIImage imageNamed:_brushName];
//sets the style for the endpoints of lines drawn in a graphics context
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGFloat eraseSize = oriBrush.size.width*_brushSize/_zoomCurrentFactor;
CGContextSetLineCap(ctx, kCGLineCapRound);
CGContextSetLineJoin(ctx, kCGLineJoinRound);
CGContextSetLineWidth(ctx,eraseSize);
CGContextSetRGBStrokeColor(ctx, 1, 1, 1, alp);
CGContextSetBlendMode(ctx, kCGBlendModeClear);
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, lastPoint.x,lastPoint.y);
CGPoint vector = CGPointMake(currentPoint.x - lastPoint.x, currentPoint.y - lastPoint.y);
CGFloat distance = hypotf(vector.x, vector.y);
vector.x /= distance;
vector.y /= distance;
for (CGFloat i = 0; i < distance; i += 1.0f) {
CGPoint p = CGPointMake(lastPoint.x + i * vector.x, lastPoint.y + i * vector.y);
CGContextAddLineToPoint(ctx, p.x, p.y);
}
CGContextStrokePath(ctx);
maskImgView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
lastPoint = currentPoint;
Problem is, this TOTALLY erase anything. The alpha set in this function ( CGContextSetRGBStrokeColor(ctx, 1, 1, 1, alp);) seems to be ignored totally.
I want to erase just lightly and repeated erasing will then totally removes the drawing.
Any ideas?
EDIT: As per request, I add more details about this code:
alp=_brushAlpha is a property delcared in this ViewController class. It ranges from 0.1 to 1.0. At testing I set it to 0.5. This drawing code is triggered by pan gesture recognizer (change state). It is basically following the finger (draw/erase by finger).
You've set the blending mode to clear. That ignores stroke color. You should play with the various modes a bit, but I suspect you want something like sourceAtop or maybe screen. See the CGBlendMode docs for full details.
You have a flag named clearsContextBeforeDrawing in UIView. if you set it to YES it will clear it before every draw.
according to documentation
A Boolean value that determines whether the view’s bounds should be automatically cleared before drawing.
When set to YES, the drawing buffer is automatically cleared to transparent black before the drawRect: method is called. This behavior ensures that there are no visual artifacts left over when the view’s contents are redrawn. If the view’s opaque property is also set to YES, the backgroundColor property of the view must not be nil or drawing errors may occur. The default value of this property is YES.
If you set the value of this property to NO, you are responsible for ensuring the contents of the view are drawn properly in your drawRect: method. If your drawing code is already heavily optimized, setting this property is NO can improve performance, especially during scrolling when only a portion of the view might need to be redrawn.

Line width in other angle

I draw line like this:
- (void)drawRect:(CGRect)rect {
_lineColor = [UIColor blackColor];
[_lineColor setStroke];
[_lineColor setFill];
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextClearRect(c, rect);
[[UIColor clearColor] setFill];
CGContextAddRect(c, rect);
CGContextSetLineWidth(c, 1);
CGContextDrawPath(c, kCGPathFill);
CGContextBeginPath(c);
CGContextMoveToPoint(c, 0, 0);
CGContextAddLineToPoint(c, x1, y1);
CGContextStrokePath(c);
}
but, my line is have different width which angle is 90 or 45 degrees. How I can draw line with same width
I whipped up something that may make the effect here more visible. Here's the code:
- (void)drawRect:(CGRect)rect
{
CGContextRef c = UIGraphicsGetCurrentContext();
// Get us a gray background
CGContextSetFillColorWithColor(c, [[UIColor grayColor] CGColor]);
CGContextFillRect(c, CGRectInfinite);
CGContextSetStrokeColorWithColor(c, [[UIColor blackColor] CGColor]);
CGContextSetLineWidth(c, 1.0);
CGContextBeginPath(c);
// Draw some lines at various angles
for (CGFloat i = -10.; i <= 10.; i += 1.0)
{
CGContextMoveToPoint(c, CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
CGContextAddLineToPoint(c, CGRectGetMidX(self.bounds) + i * 10., CGRectGetMidY(self.bounds) + 100);
}
CGContextStrokePath(c);
}
Here's the output from that code on a retina device: (This should display at 100% in the standard StackOverflow format, but you can look at it full size to be sure)
Now here's a piece of that blown up:
What you're seeing here is anti-aliasing at work. For starters the vertical line is 2.0 pixels across, all the time, with no anti-aliasing (assuming you draw it on a pixel boundary). Now think about a 45deg line drawn using the same pixel grid, and employ the Pythagorean theorem. Here's another diagram:
At it's narrowest (i.e. in the dimension perpendicular to the line itself), a 45 deg line will appear 1.414px wide, and at it's widest opaque section (not counting the mostly transparent pixel that's bridging the space in the jaggy gaps) it's going to appear 2.828px across. When blown up, you can see how the work that's being done to anti-alias these lines is effecting the optical appearance of the lines.
Someone is probably going to come along and suggest that you turn off anti-aliasing, but for reference, that makes the optical effect even worse (because then the rasterizer is going to make every pixel with coverage completely opaque):
In short, this is expected behavior, and if you need to adjust how you draw each line to achieve some desired optical appearance that the default anti-aliasing code doesn't provide, then you just have to do that work. There's not a "make all my lines optically similar" setting in CoreGraphics -- they've done the best they can for the general case, and if you need something more specific that's up to you. FWIW, many many applications simply use the default, and people seem pretty satisfied with the results, so you might ask yourself if this is really the place in your app where you want to put in a bunch of extra work.
It occurred to me: I'm not sure you'll like the effect, but one possible approach for achieving optical similarity might be to draw your vertical lines not on pixel boundaries. This will cause them to appear anti-aliased too, which (depending on how you look at it) may make them appear more consistent with the angled, anti-aliased lines. Here's what that looks like:
What you lose doing this is a certain "crispness" to the lines. Drawing off pixel-boundaries can most easily be achieved by translating the context by 0.25pt in the X direction (for retina -- for non-retina, 0.5pt will have a similar effect) like this:
CGContextTranslateCTM(c, 0.25, 0);
Hope this helps.

cgcontext rotate rectangle

guys!
I need to draw some image to CGContext.This is the relevant code:
CGContextSaveGState(UIGraphicsGetCurrentContext());
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGRect rect = r;
CGContextRotateCTM(ctx, DEGREES_TO_RADIANS(350));
[image drawInRect:r];
CGContextRestoreGState(UIGraphicsGetCurrentContext());
Actually,the rectangle is rotate and display on a area what is not my purpose.I just want to
rotate the image and display on the same position.
Any ideas ?????
Rotation is about the context's origin, which is the same point that rectangles are relative to. If you imagine a sheet of graph paper in the background, you can see what's going on more clearly:
The line is the “bottom” (y=0) of your window/view/layer/context. Of course, you can draw below the bottom if you want, and if your context is transformed the right way, you might even be able to see it.
Anyway, I'm assuming that what you want to do is rotate the rectangle in place, relative to an unrotated world, not rotate the world and everything in it.
The only way to rotate anything is to rotate the world, so that's how you need to do it:
Save the graphics state.
Translate the origin to the point where you want to draw the rectangle. (You probably want to translate to its center point, not the rectangle's origin.)
Rotate the context.
Draw the rectangle centered on the origin. In other words, your rectangle's origin point should be negative half its width and negative half its height (i.e., (CGPoint){ width / -2.0, height / -2.0 })—don't use the origin it had before, because you already used that in the translate step.
Restore the gstate so that future drawing isn't rotated.
What worked for me was to first use a rotation matrix to calculate the amount of translation required to keep your image centered. Below I assume you've already calculated centerX and centerY to be the center of your drawing frame and 'theta' is your desired rotation angle in radians.
let newX = centerX*cos(theta) - centerY*sin(theta)
let newY = centerX*sin(theta) + centerY*cos(theta)
CGContextTranslateCTM(context,newX,newY)
CGContextRotateCTM(context,theta)
<redraw your image here>
Worked just fine for me. Hope it helps.
use following code to rotate your image
// convert degrees to Radians
CGFloat DegreesToRadians(CGFloat degrees)
{
return degrees * M_PI / 180;
};
write it in drawRect method
// create new context
CGContextRef context = UIGraphicsGetCurrentContext();
// define rotation angle
CGContextRotateCTM(context, DegreesToRadians(45));
// get your UIImage
UIImage *img = [UIImage imageNamed:#"yourImageName"];
// Draw your image at rect
CGContextDrawImage(context, CGRectMake(100, 0, 100, 100), [img CGImage]);
// draw context
UIGraphicsGetImageFromCurrentImageContext();

Efficient way to draw a graph line by line in CALayer

I need to draw a line chart from values that come to me every half a seconds. I've come up with my custom CALayer for this graph which stores all the previous lines and every two seconds redraws all previous lines and adds one new line. I find this solution non-optimal because there's only need to draw one additional line to the layer, no reason to redraw potentially thousands of previous lines.
What do you think would be the best solution in this case?
Use your own NSBitmapContext or UIImage as a backing store. Whenever new data comes in draw to this context and set your layer's contents property to the context's image.
I am looking at an identical implementation. Graph updates every 500 ms. Similarly I felt uncomfortable drawing the entire graph each iteration. I implemented a solution 'similar' to what Nikolai Ruhe proposed as follows:
First some declarations:
#define TIME_INCREMENT 10
#property (nonatomic) UIImage *lastSnapshotOfPlot;
and then the drawLayer:inContext method of my CALayer delegate
- (void) drawLayer:( CALayer*)layer inContext:(CGContextRef)ctx
{
// Restore the image of the layer from the last time through, if it exists
if( self.lastSnapshotOfPlot )
{
// For some reason the image is being redrawn upside down!
// This block of code adjusts the context to correct it.
CGContextSaveGState(ctx);
CGContextTranslateCTM(ctx, 0, layer.bounds.size.height);
CGContextScaleCTM(ctx, 1.0, -1.0);
// Now we can redraw the image right side up but shifted over a little bit
// to allow space for the new data
CGRect r = CGRectMake( -TIME_INCREMENT, 0, layer.bounds.size.width, layer.bounds.size.height );
CGContextDrawImage(ctx, r, self.lastSnapshotOfPlot.CGImage );
// And finally put the context back the way it was
CGContextRestoreGState(ctx);
}
CGContextStrokePath(ctx);
CGContextSetLineWidth(ctx, 2.0);
CGContextSetStrokeColorWithColor(ctx, [UIColor blueColor].CGColor );
CGContextBeginPath( ctx );
// This next section is where I draw the line segment on the extreme right end
// which matches up with the stored graph on the image. This part of the code
// is application specific and I have only left it here for
// conceptual reference. Basically I draw a tiny line segment
// from the last value to the new value at the extreme right end of the graph.
CGFloat ppy = layer.bounds.size.height - _lastValue / _displayRange * layer.bounds.size.height;
CGFloat cpy = layer.bounds.size.height - self.sensorData.currentvalue / _displayRange * layer.bounds.size.height;
CGContextMoveToPoint(ctx,layer.bounds.size.width - TIME_INCREMENT, ppy ); // Move to the previous point
CGContextAddLineToPoint(ctx, layer.bounds.size.width, cpy ); // Draw to the latest point
CGContextStrokePath(ctx);
// Finally save the entire current layer to an image. This will include our latest
// drawn line segment
UIGraphicsBeginImageContext(layer.bounds.size);
[layer renderInContext: UIGraphicsGetCurrentContext()];
self.lastSnapshotOfPlot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
Is this the most efficient way?
I have not been programming in ObjectiveC long enough to know so all suggestions/improvements welcome.

Resources