Draw Polyline With Border Mapbox, iOS - swift

I'm using Mapbox iOS SDK and trying to draw a polyline without geojson. I tried to get the route with this method:
func calculateRoute() {
...
let options = NavigationRouteOptions(waypoints: [origin, destination], profileIdentifier: .automobileAvoidingTraffic)
Directions.shared.calculate(options) { (waypoints, routes, error) in
guard let route = routes?.first else { return }
self.showPreview(route: route)
}
}
Then I tried to draw a route.
func showPreview(route: Route) {
guard let steps = route.legs.first?.steps else { return }
var points = [CLLocationCoordinate2D]()
for step in steps {
points.append(step.maneuverLocation)
}
let line = MGLPolyline(coordinates: &points, count: UInt(points.count))
mapView?.addAnnotation(line)
}
It draws a polyline on the map view. I could change the color and the width of the polyline with two delegate methods (MGLMapViewDelegate):
func mapView(_ mapView: MGLMapView, lineWidthForPolylineAnnotation annotation: MGLPolyline) -> CGFloat {
return 10
}
func mapView(_ mapView: MGLMapView, strokeColorForShapeAnnotation annotation: MGLShape) -> UIColor {
return .blue
}
but I can't find a method to set a border width and border color around the polyline. Is there any way to do that?

It looks like I had a similar use case to you (i.e. not using geojson) and ended up with something like this. By associating your route with an MGLLineStyleLayer you can control the visual parameters of the line.
func showPreview(route: Route) {
guard route.coordinateCount > 0 else { return }
// Convert the route’s coordinates into a polyline
var routeCoordinates = route.coordinates!
let polyline = MGLPolylineFeature(coordinates: &routeCoordinates, count: route.coordinateCount)
// If there's already a route line on the map, reset its shape to the new route
if let source = mapView.style?.source(withIdentifier: "route-source") as? MGLShapeSource {
source.shape = polyline
} else {
let source = MGLShapeSource(identifier: "route-source", features: [polyline], options: nil)
// Customize the route line color and width
let lineStyle = MGLLineStyleLayer(identifier: "route-style", source: source)
lineStyle.lineColor = NSExpression(forConstantValue: UIColor.blue)
lineStyle.lineWidth = NSExpression(forConstantValue: 3)
// Add the source and style layer of the route line to the map
mapView.style?.addSource(source)
mapView.style?.addLayer(lineStyle)
}
}
You want to add a border and control how that looks. If you take a look at this example on the Mapbox website: Line style Example they do what you want by creating a second MGLLineStyleLayer and inserting it below the first one. They call the second layer casingLayer. This is their code so you can see it is formed the same way as the first layer.
let casingLayer = MGLLineStyleLayer(identifier: "polyline-case", source: source)
// Add your formatting attributes here. See example on website.
Then they insert it below the first line and because it has a wider width, shows as a border.
style.insertLayer(casingLayer, below: lineStyle)
Hope this helps.

Related

How Do I Draw a String in an MKOverlayRender

The use case I have is one where I want to draw and label counties in a state. Annotations don't seem like the right approach to solve this problem. First of all, the label refers to region rather than a point. Second, there are far too many; so, I would have to selectively show and hide annotations based on zoom level (actually something more like the size of the MKCoordinateRegion span). Lastly, county labels are not all that relevant unless the user starts zooming in.
Just as a side note, county boundaries may be present in map tiles, but they are not emphasized. Moreover, there are a multitude of other boundaries I might want to draw that are completely absent from map tiles.
Ultimately, what I want to do is create an overlay for each county shape (counties are clickable and I can navigate to details) and another set of overlays for the labels. I separate county shapes and labels because county shapes are messy and I just use the center of the county. There is no guarantee with this approach that labels will not draw outside of county shapes, which means labels could end up getting clipped when other counties are drawn.
Drawing the county shapes was relatively easy or at least relatively well documented. I do not include any code on rendering shapes. Drawing text on the other hand is not straight forward, not well documented, and most of the posts on the subject are ancient. The lack of recent posts on the subject as well as the fact that most posts posit solutions that no longer work, use deprecated APIs, or only solve a part of the problem motivates this post. Of course, the lack of activity on this problem could be because my strategy is mind numbingly stupid.
I have posted a complete solution to the problem. If you can improve on the solution below or believe there is a better way, I would appreciate the feedback. Alternatively, if you are trying to find a solution to this problem, you will find this post more helpful than the dozens I have looked at, which on the whole got me to where I am now.
Below is a complete solution that can be run in an Xcode single view Playground. I am running Xcode 14.2. The most important bit of code is the overridden draw function of LabelOverlayRenderer. That bit of code is what I struggled to craft for more than a day. I almost gave up. Another key point is when drawing text, one uses CoreText. The APIs pertaining to drawing and managing text are many and most have had a lot of name changes and deprecation.
import UIKit
import MapKit
import SwiftUI
class LabelOverlayRenderer: MKOverlayRenderer {
let title: String
let center: CLLocationCoordinate2D
init(overlay: LabelOverlay) {
center = overlay.coordinate
title = overlay.title!
super.init(overlay: overlay)
}
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
context.saveGState()
// Set Drawing mode
context.setTextDrawingMode(.fillStroke)
// If I don't do this, the text is upside down.
context.textMatrix = CGAffineTransformMakeScale(1.0, -1.0);
// Text size is crazy big because label has to be miles across
// to be visible.
var attrs = [ NSAttributedString.Key : Any]()
attrs[NSAttributedString.Key.font] = UIFont(name: "Helvetica", size: 128000.0)!
attrs[NSAttributedString.Key.foregroundColor] = UIColor(Color.red)
let attributedString = NSAttributedString(string: title, attributes: attrs)
let line = CTLineCreateWithAttributedString(attributedString)
// Get the size of the whole string, so the string can
// be centered. CGSize is huge because I don't want
// to clip or wrap the string. The range setting
// is just cut and paste. Looks like a place holder.
// Ideally, it is the range of that portion
// of the string for which I want the size.
let frameSetter = CTFramesetterCreateWithAttributedString(attributedString)
let size = CTFramesetterSuggestFrameSizeWithConstraints(frameSetter, CFRangeMake(0, 0), nil, CGSize(width: 1000000, height: 1000000), nil)
// Center is lat-lon, but map is in meters (maybe? definitely
// not lat-lon). Center string and draw.
var p = point(for: MKMapPoint(center))
p.x -= size.width/2
p.y += size.height/2
// There is no "at" on CTLineDraw. The string
// is positioned in the context.
context.textPosition = p
CTLineDraw(line, context)
context.restoreGState()
}
}
class LabelOverlay: NSObject, MKOverlay {
let title: String?
let coordinate: CLLocationCoordinate2D
let boundingMapRect: MKMapRect
init(title: String, coordinate: CLLocationCoordinate2D, boundingMapRect: MKMapRect) {
self.title = title
self.coordinate = coordinate
self.boundingMapRect = boundingMapRect
}
}
class MapViewCoordinator: NSObject, MKMapViewDelegate {
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let overlay = overlay as? LabelOverlay {
return LabelOverlayRenderer(overlay: overlay)
}
fatalError("Unknown overlay type!")
}
}
struct MyMapView: UIViewRepresentable {
func makeCoordinator() -> MapViewCoordinator {
return MapViewCoordinator()
}
func updateUIView(_ view: MKMapView, context: Context){
// Center on Georgia
let center = CLLocationCoordinate2D(latitude: 32.6793, longitude: -83.62245)
let span = MKCoordinateSpan(latitudeDelta: 4.875, longitudeDelta: 5.0003)
let region = MKCoordinateRegion(center: center, span: span)
view.setRegion(region, animated: true)
view.delegate = context.coordinator
let coordinate = CLLocationCoordinate2D(latitude: 32.845084, longitude: -84.3742)
let mapRect = MKMapRect(x: 70948460.0, y: 107063759.0, width: 561477.0, height: 613908.0)
let overlay = LabelOverlay(title: "Hello World!", coordinate: coordinate, boundingMapRect: mapRect)
view.addOverlay(overlay)
}
func makeUIView(context: Context) -> MKMapView {
// Create a map with constrained zoom gestures only
let mapView = MKMapView(frame: .zero)
mapView.isPitchEnabled = false
mapView.isRotateEnabled = false
let zoomRange = MKMapView.CameraZoomRange(
minCenterCoordinateDistance: 160000,
maxCenterCoordinateDistance: 1400000
)
mapView.cameraZoomRange = zoomRange
return mapView
}
}
struct ContentView: View {
var body: some View {
VStack {
MyMapView()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

NSTextFinder and its drawIncrementalMatchHighlight method that doesn't seem to work

NSTextFinder has a class method drawIncrementalMatchHighlight. Its description makes me think that the method should be used to highlight text on my own with the Apple's default animation of the highlighting. In other words, I should be able to replicate the showFindIndicator(for:) method of the AppKit NSTextView.
But it does not seem to work for me. I did exactly what the documentation tells me:
func showFindIndicator(for charRange: NSRange) {
// get the text range from the NSRange (I'm using TextKit 2)
guard let textRange = textLayoutManager.textContentManager?.textRange(from: charRange) else { return }
// get the screen rect for the text range
var rangeRect: CGRect = .zero
textLayoutManager.enumerateTextSegments(in: textRange, type: .selection, options: .rangeNotRequired, using: { _, rect, _, _ in
rangeRect = rect
return false
})
// create the graphics context to draw the highlighting into and get the layout fragment that draws the text
guard let bitmapRep = bitmapImageRepForCachingDisplay(in: rangeRect),
let context = NSGraphicsContext(bitmapImageRep: bitmapRep),
let layoutFragment = textLayoutManager.textLayoutFragment(for: textRange.location)
else { return }
// make the context current
NSGraphicsContext.current = context
// draw the background
self.backgroundColor?.setFill()
context.cgContext.fill(rangeRect)
// draw the highlight
NSTextFinder.drawIncrementalMatchHighlight(in: rangeRect)
// and finally draw the text
let origin = layoutFragment.layoutFragmentFrame.origin
layoutFragment.draw(at: origin, in: context.cgContext)
}
But all of this produces nothing. I cannot see any feedback on the screen.
What am I doing wrong with the drawIncrementalMatchHighlight method?

How can I draw two MKPolygons on MapView without having them connect?

For some reason, when I try to draw two MKPolygons on a mapView (MKMapView) I end up with the two polygons connected. Drawing each polygon individually works fine. And I've verified that each of the polygons don't contain any of the coordinates to form the connection between the two. I've attached an image with the two polygons connected
For reference, here's where I call to add the polygons.
func addPeakTimePolygon(from coordinatesArray: [CLLocationCoordinate2D], title: Int){
let polygon = MKPolygon(coordinates: coordinatesArray, count: coordinatesArray.count)
polygon.title = String(title)
//Should refactor to use .contains(where:
var shouldAdd = true
for polygon in self.currentPolygons{
if polygon.title == String(title){
shouldAdd = false
}
}
if shouldAdd{
self.currentPolygons.append(polygon)
self.mapView.add(polygon)
}
}
And here's my rendererFor code:
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if overlay is MKPolyline {
let renderer = MKPolylineRenderer(overlay: overlay)
renderer.strokeColor = #colorLiteral(red: 0, green: 0.6862745098, blue: 0.7607843137, alpha: 1)
renderer.lineWidth = 5.0
return renderer
}
else if overlay is MKPolygon {
let renderer = MKPolygonRenderer(overlay: overlay)
renderer.fillColor = UIColor.red.withAlphaComponent(0.5)
renderer.strokeColor = UIColor.red
renderer.lineWidth = 2
return renderer
}
return MKOverlayRenderer()
}
It seems like you're making one overlay consisting of two polygons. You can't do that with an MKPolygonRenderer; you will get one polygon, as you are observing.
You will need separate overlays, one for each polygon. Unless you are using iOS 13! In that case, you are in luck: New in iOS 13, multiple polygons or polylines can be combined into an MKMultiPolygon or MKMultiPolyline and drawn by an MKMultiPolygonRenderer or MKMultiPolylineRenderer.
I forgot to check / post the code that was calling addPeakTimePolygon. Here is the problematic code below:
var locationList: [CLLocationCoordinate2D] = []
var title = 0
if let peakTimeCampaignList = data["PeakTimeRewardCampaignList"] as? [[AnyHashable:Any]]{
for campaign in peakTimeCampaignList{
if let polygonPoints = campaign["CampaignPolygon"] as? [[AnyHashable:Any]]{
for polygonPoint in polygonPoints{
let polygonPoint = CLLocationCoordinate2D(latitude: polygonPoint["Latitude"] as! CLLocationDegrees, longitude: polygonPoint["Longitude"] as! CLLocationDegrees)
locationList.append(polygonPoint)
}
}
if let id = campaign["Id"] as? Int{
title = id
}
mapBundle.addPeakTimePolygon(from: locationList, title: title)
}
}
As you can see locationList wasn't being cleared out within the loop, causing whatever we sent over to addPeakTimePolygon to have coordinates from two polygons and MapKit was trying it's best to form a polygon between them.
This was a dumb mistake, but hoping someone else sees this with the same problem!

How to delay callout from showing when annotation selected in MKMapView? Swift 4

(This is my first stack overflow question haha)
UPDATE:
From this link - Center MKMapView BEFORE displaying callout
I implemented the solution from Thermometer, however selecting and deselecting the annotation makes it look like my application is glitching.
I think the best way would be to delay the callOut (detail pop up) from appearing by a few seconds so the map has time to move first.
Here is my code:
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
guard let annotation = view.annotation else {
return
}
let currentAnnotation = view.annotation as? MapMarker
Global.currentAnnotation = currentAnnotation
findRelatinoshipLines()
if Global.showLifeStoryLines {
var locations = lifeStoryAnnotations.map { $0.coordinate }
let polyline = MKPolyline(coordinates: &locations, count: locations.count)
Global.finalLineColor = Global.lifeStoryColor
mapView.addOverlay(polyline)
}
if Global.showFatherLines {
var fatherLocations = fatherTreeAnnotations.map { $0.coordinate }
let fatherPolyline = MKPolyline(coordinates: &fatherLocations, count: fatherLocations.count)
Global.finalLineColor = Global.fatherLineageColor
mapView.addOverlay(fatherPolyline)
}
if Global.showMotherLines {
var motherLocations = motherTreeAnnotations.map { $0.coordinate }
let motherPolyline = MKPolyline(coordinates: &motherLocations, count: motherLocations.count)
Global.finalLineColor = Global.motherLineageColor
mapView.addOverlay(motherPolyline)
}
if Global.showSpouseLines {
var locations = spouseAnnotations.map { $0.coordinate }
let polyline = MKPolyline(coordinates: &locations, count: locations.count)
Global.finalLineColor = Global.spouseColor
mapView.addOverlay(polyline)
}
if Global.zoomChange == true {
Global.zoomChange = false
} else {
let currentRegion = mapView.region
let span = currentRegion.span
let location = currentAnnotation!.coordinate
let region = MKCoordinateRegion(center: location, span: span)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
mapView.setCenter(annotation.coordinate, animated: true)
//mapView.setRegion(region, animated: true)
}
}
}
CONTINUED:
Basically I'm working on a family genealogy application that displays events from relatives on a map.
When I click an annotation (event) the details (who event belongs to, where and when, etc) pops up above with an information button to show the selected person.
I have it set up to set the MKMapView region so that the selected annotation is centered each time a new annotation is clicked.
The problem is when I click an event that is on the edge of the screen, my annotation title/description pops up off centered so that it fits on my screen because it doesn't know that I plan on re-centering the map view around said annotation.
I was wondering if there was any way to make the title/description appear centered directly above the selected annotation so that when I move the map everything is centered and fits on the screen.
Here are some screenshots of what I'm talking about:
Before and After
Solved it by calling setCenter with a slight delay in mapView(_:didSelect:):
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
guard let annotation = view.annotation else {
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
mapView.setCenter(annotation.coordinate, animated: true)
}
}

How add description to MKPolyline & MKPolygon?

How add annotations to polyline and polygon in Swift & MapKit? By Point is simple.
S.,
I'm not sure what you're asking here, but I assume you want to display an annotation somewhere on the polyline.
First the intro how to get the the polyline:
So, lets assume you have an array of CLLocation objects that will draw the polyline on the map. We call this array of location objects: myLocations and it's of type [CLLocation]. Now somewhere in your app you call a method that creates the polyline, we call this method createOverlayObject(locations: [CLLocation]) -> MKPolyline.
Your call could look like this:
let overlayPolyline = createOverlayObject(myLocations)
The method you called then could look like this:
func createOverlayObject(locations: [CLLocation]) -> MKPolyline {
//This method creates the polyline overlay that you want to draw.
var mapCoordinates = [CLLocationCoordinate2D]()
for overlayLocation in locations {
mapCoordinates.append(overlayLocation.coordinate)
}
let polyline = MKPolyline(coordinates: &mapCoordinates[0], count: mapCoordinates.count)
return polyline
}
This was the first part, don't forget to implement the mapView(_: rendererForOverlay overlay:) to get the line rendered. this part could look something like this:
func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer {
//This function creatss the renderer for the polyline overlay. This makes the polyline actually display on screen.
let renderer = MKPolylineRenderer(overlay: overlay)
renderer.strokeColor = mapLineColor //The color you want your polyline to be.
renderer.lineWidth = self.lineWidth
return renderer
}
Now the second part get the annotation somewhere on the map. This is actually straight forward if you know what the coordinates are where you want to put your annotation. creating and displaying the annotation is straightforward again, assuming you have defined a map view called myNiceMapView:
func createAnnotation(myCoordinate: CLLocationCoordinate2D) {
let myAnnotation = MKPointAnnotation()
myAnnotation.title = "My nice title"
startAnnotation.coordinate = myCoordinate
self.myNiceMapView.addAnnotations([myAnnotation])
}
Don't forget to implement mapView(_: MKMapView, viewForAnnotation annotation:) -> MKAnnotationView? method, which might look like:
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
//This is the mapview delegate method that adjusts the annotation views.
if annotation.isKindOfClass(MKUserLocation) {
//We don't do anything with the user location, so ignore an annotation that has to do with the user location.
return nil
}
let identifier = "customPin"
let trackAnnotation = MKAnnotationView.init(annotation: annotation, reuseIdentifier: identifier)
trackAnnotation.canShowCallout = true
if annotation.title! == "Some specific title" { //Display a different image
trackAnnotation.image = UIImage(named: "StartAnnotation")
let offsetHeight = (trackAnnotation.image?.size.height)! / 2.0
trackAnnotation.centerOffset = CGPointMake(0, -offsetHeight)
} else { //Display a standard image.
trackAnnotation.image = UIImage(named: "StopAnnotation")
let offsetHeight = (trackAnnotation.image?.size.height)! / 2.0
trackAnnotation.centerOffset = CGPointMake(0, -offsetHeight)
}
return trackAnnotation
}
Now the challenges is finding the right coordinate where to put your annotation. I can't find anything better than that you have a CLLocationCoordinate2D that references the location you want to put the annotation. Then with a for-in loop find the location where you want to put your annotation, something like this:
for location in myLocations {
if (location.latitude == myReferenceCoordinate.latitude) && (location.longitude == myReferenceCoordinate.longitude) {
self.createAnnotation(location: CLLOcationCoordinate2D)
}
}
Hope this answers your question.