Reputation: 126
I am drawing lines on an MkMapView and I want to specify their width in meters.
I tried something like this:
-(MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id <MKOverlay>) overlay
{
if([overlay isKindOfClass:[MKPolyline class]])
{
MKPolylineRenderer *renderer = [[MKPolylineRenderer alloc] initWithOverlay:overlay];
double ppm = MKMapPointsPerMeterAtLatitude(myLatitude);
renderer.lineWidth = ppm * myMetersValue;
...
This produces a line that is much wider than myMetersValue and it does not scale when I zoom in and out.
ppm remains constant at approx. 9.49 even when I zoom the map in and out. I have verified myLatitude to be a valid lat (approx. 45 degrees). I have verified myMetersValue to be a reasonable # (approx. 18m).
(edit)
I have discovered that the MapPoints returned by MKMapPointsPerMeterAtLatitude are not screen points, so I can't use that function.
So my basic problem is how to convert meters to screen points in the mkMapView. More succinctly, how do I get a value for ppm in my code? I tried this (found elsewhere on SO):
MKCoordinateRegion myRegion = MKCoordinateRegionMakeWithDistance(mapView.centerCoordinate, 400, 0);
CGRect myRect = [mapView convertRegion: myRegion toRectToView: nil];
double ppm = myRect.size.width / 400.0;
That did not work.
Then I tried this:
CLLocationCoordinate2D l1 = [mapView convertPoint:CGPointMake(0,0) toCoordinateFromView:mapView];
CLLocation *ll1 = [[CLLocation alloc] initWithLatitude:l1.latitude longitude:l1.longitude];
CLLocationCoordinate2D l2 = [mapView convertPoint:CGPointMake(0,500) toCoordinateFromView:mapView];
CLLocation *ll2 = [[CLLocation alloc] initWithLatitude:l2.latitude longitude:l2.longitude];
double ppm = 500.0 / [ll1 distanceFromLocation:ll2];
The idea here is to get the lat/lon for two points that are 500 screen points apart. Then get the distance in meters between them so I can calculate ppm. This sort of worked, but it looks like the resulting ppm is not quite right so I am not convinced that this is correct. Also, when I zoom the map the existing lines on the map are not re-rendered so the stay the same width after the zoom (but that is a different problem).
(edit)
It now looks like my calculation of ppm is correct. The problem is that the renderer is called before the user finishes zooming so the lines are not rendered at the final zoom scale. When I force re-rendering once the final zoom scale is determined then ppm is correct and my code works.
Upvotes: 2
Views: 2281
Reputation: 11
I tried Verticon's answer above. While it achieved a desired effect, unfortunately it crashed me out os simulating several times. My objective was to mainly zoom to almost the width of the road after zooming in a certain distance.
I managed it by: first creating my custom MKPolyLine
class which sets a width
variable among others for customization.
first including an extension for MKMapView
I used elsewhere:
extension MKMapView {
var zoomLevel: Double {
let maxZoomLevel = log2(Double(self.frame.size.width) / 256.0)
return maxZoomLevel - log2(self.region.span.longitudeDelta)
}
}
Then including a clause in my mapView(regionDidChangeAnimated)
function. I append the polylines to an array after I add them to the map.
if mapView.zoomLevel > 7.75 {
lines_busroutes.forEach {
let renderer = mapView.renderer(for: $0) as! MKPolylineRenderer
renderer.lineWidth = $0.outlineWidth + pow(mapView.zoomLevel,1.6)-pow(7.75,1.6)
renderer.setNeedsDisplay()
}
} else if mapView.zoomLevel < 7.75 {
lines_busroutes.forEach {
let renderer = mapView.renderer(for: $0) as! MKPolylineRenderer
renderer.lineWidth = $0.outlineWidth
renderer.setNeedsDisplay()
}
}
This works ok and different calculations and initial values can probably be integrated to it for other scales. The maximum zoomLevel
I'd get using my extension was ~12.5 so I played around with values to get that.
Upvotes: 1
Reputation: 2513
Combining the information provided by Bruce and Anna, I wrote the following Polyline Renderer subclass.
public extension MKMapView {
public func metersToPoints(meters: Double) -> Double {
let deltaPoints = 500.0
let point1 = CGPoint(x: 0, y: 0)
let coordinate1 = convert(point1, toCoordinateFrom: self)
let location1 = CLLocation(latitude: coordinate1.latitude, longitude: coordinate1.longitude)
let point2 = CGPoint(x: 0, y: deltaPoints)
let coordinate2 = convert(point2, toCoordinateFrom: self)
let location2 = CLLocation(latitude: coordinate2.latitude, longitude: coordinate2.longitude)
let deltaMeters = location1.distance(from: location2)
let pointsPerMeter = deltaPoints / deltaMeters
return meters * pointsPerMeter
}
}
public class ZoomingPolylineRenderer : MKPolylineRenderer {
private var mapView: MKMapView!
private var polylineWidth: Double! // Meters
convenience public init(polyline: MKPolyline, mapView: MKMapView, polylineWidth: Double) {
self.init(polyline: polyline)
self.mapView = mapView
self.polylineWidth = polylineWidth
}
override public func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
self.lineWidth = CGFloat(mapView.metersToPoints(meters: polylineWidth))
super.draw(mapRect, zoomScale: zoomScale, in: context)
}
}
Upvotes: 2
Reputation: 126
So, the following is one method for drawing an MkPolyline of a specific width in meters. Beware that once the line is drawn on the map it may not get re-rendered if the user zooms. Or, if it does, it may get re-rendered before the user finishes zooming. In this case the width after zooming will no longer reflect your desired width in meters. One way to deal with this is to override regionDidChangeAnimated and remove the overlay and add it back. This method works OK in my specific case, but I think you should look at Anna's suggested solution because it may be a better general approach.
-(MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id <MKOverlay>) overlay
{
if([overlay isKindOfClass:[MKPolyline class]])
{
// the following are two methods of calculating PPM (screen points per meter).
// method 1: requires that you have access to a latitude that is near to the line you are drawing
CLLocationCoordinate2D l1 = CLLocationCoordinate2DMake(myLatitude, 0);
CLLocationCoordinate2D l2 = CLLocationCoordinate2DMake(myLatitude + 1.0, 0);
CGPoint p1 = [mapView convertCoordinate:l1 toPointToView:mapView];
CGPoint p2 = [mapView convertCoordinate:l2 toPointToView:mapView];
double ppm = (p1.y - p2.y) / (60.0 * METERS_PER_NAUTICALMILE);
// method 2:
CLLocationCoordinate2D l1 = [mapView convertPoint:CGPointMake(0,0) toCoordinateFromView:mapView];
CLLocation *ll1 = [[CLLocation alloc] initWithLatitude:l1.latitude longitude:l1.longitude];
CLLocationCoordinate2D l2 = [mapView convertPoint:CGPointMake(0,500) toCoordinateFromView:mapView];
CLLocation *ll2 = [[CLLocation alloc] initWithLatitude:l2.latitude longitude:l2.longitude];
ppm = 500.0 / [ll1 distanceFromLocation:ll2];
MKPolyline *l = (MKPolyline *) overlay;
MKPolylineRenderer *renderer = [[MKPolylineRenderer alloc] initWithOverlay:overlay];
renderer.lineWidth = ppm * myMetersValue;
renderer.strokeColor = [UIColor colorWithRed:0.0 green:1.0 blue:0.0 alpha:.3];
renderer.lineCap = CGLineCap::kCGLineCapButt;
return renderer;
}
return nil;
}
Upvotes: 0