Skip to content

Instantly share code, notes, and snippets.

@captainbarbosa
Last active June 10, 2020 23:19
Show Gist options
  • Save captainbarbosa/99129e0a50ac4480d900ee8f80fa3ac7 to your computer and use it in GitHub Desktop.
Save captainbarbosa/99129e0a50ac4480d900ee8f80fa3ac7 to your computer and use it in GitHub Desktop.
Drawing traffic with gradients
import UIKit
import MapboxDirections
import Turf
import Mapbox
class ViewController: UIViewController, MGLMapViewDelegate {
var mapView: MGLMapView!
typealias CongestionSegment = ([CLLocationCoordinate2D], CongestionLevel)
public let MBCongestionAttribute = "congestion"
public let MBCurrentLegAttribute = "isCurrentLeg"
let routeAlternateColor = UIColor.purple
let trafficLowColor = UIColor.green
let trafficModerateColor = UIColor.yellow
let trafficHeavyColor = UIColor.red
let trafficSevereColor = UIColor.black
let trafficUnknownColor = UIColor.blue
var showsGradientTraffic: Bool = true
lazy var toggleButton: UIButton = {
let button = UIButton(frame: CGRect(x: 15, y: 40, width: 150, height: 40))
button.backgroundColor = UIColor.darkGray
button.setTitle("Hide traffic gradient", for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 14.0)
button.layer.cornerRadius = 8.0
button.addTarget(self, action: #selector(toggleGradientTraffic), for: .touchUpInside)
return button
}()
var gradientTrafficLayer: MGLLineStyleLayer!
var congestionLineSegments = [MGLPolylineFeature]()
override func viewDidLoad() {
super.viewDidLoad()
mapView = MGLMapView(frame: self.view.bounds, styleURL: MGLStyle.lightStyleURL)
mapView.delegate = self
self.view.addSubview(mapView)
mapView.addSubview(toggleButton)
}
func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) {
// Uncomment to use real directions route
// generateRoute()
let mockRoute = MockRoute()
self.congestionLineSegments = mockRoute.congestionLineSegments
drawMockRoute(mockRoute)
}
func generateTrafficGradientExpression(route: MockRoute) -> [CGFloat: UIColor] {
typealias GradientStop = (percent: CGFloat, color: UIColor)
var stops = [GradientStop]()
var gradientDictionary = [CGFloat:UIColor]()
let routeLength = route.distance
var distanceTraveled: CLLocationDistance = 0.0
/**
To create the stops dictionary that represents the route line expressed
as gradients, for every congestion segment we need one pair of dictionary
entries to represent the color to be displayed between that range. Depending
on the index of the congestion segment, the pair's first or second key
will have a buffer value added or subtracted to make room for a gradient
transition between congestion segments.
green gradient red
transition
|-----------|~~~~~~~~~~~~|----------|
0 0.499 0.501 1.0
*/
for (index, line) in congestionLineSegments.enumerated() {
line.getCoordinates(line.coordinates, range: NSMakeRange(0, Int(line.pointCount)))
let buffPtr = UnsafeMutableBufferPointer(start: line.coordinates, count: Int(line.pointCount))
let lineCoordinates = Array(buffPtr)
// Get congestion color
let congestionLevel = line.attributes["congestion"] as! String
let congestionColor = getCongestionColor(for: congestionLevel)
// Measure the line length
let lineString = LineString(lineCoordinates)
let distance = lineString.distance()
let buffer = CGFloat(0.01)
if index == congestionLineSegments.startIndex {
let segmentStartPercentTraveled = CGFloat.zero
stops.append(GradientStop(percent: segmentStartPercentTraveled, color: congestionColor))
distanceTraveled = distanceTraveled + distance
let segmentEndPercentTraveled = CGFloat((distanceTraveled / routeLength))
stops.append(GradientStop(percent: segmentEndPercentTraveled - buffer, color: congestionColor))
continue
}
if index == congestionLineSegments.endIndex - 1 {
let segmentStartPercentTraveled = CGFloat((distanceTraveled / routeLength))
stops.append(GradientStop(percent: segmentStartPercentTraveled + buffer, color: congestionColor))
let segmentEndPercentTraveled = CGFloat(1.0)
stops.append(GradientStop(percent: segmentEndPercentTraveled, color: congestionColor))
continue
}
let segmentStartPercentTraveled = CGFloat((distanceTraveled / routeLength))
stops.append(GradientStop(percent: segmentStartPercentTraveled + buffer, color: congestionColor))
distanceTraveled = distanceTraveled + distance
let segmentEndPercentTraveled = CGFloat((distanceTraveled / routeLength))
stops.append(GradientStop(percent: segmentEndPercentTraveled - buffer, color: congestionColor))
}
for stop in stops {
gradientDictionary[stop.percent] = stop.color
}
return gradientDictionary
}
func getCongestionColor(for congestionLevel: String) -> UIColor {
switch congestionLevel {
case "low":
return UIColor.green
case "moderate":
return UIColor.yellow
case "heavy":
return UIColor.red
case "severe":
return UIColor.black
default:
// Unknown
return UIColor.blue
}
}
@objc func toggleGradientTraffic(_ sender: UIButton) {
showsGradientTraffic.toggle()
if showsGradientTraffic == false {
gradientTrafficLayer.isVisible = false
sender.setTitle("Show traffic gradient", for: .normal)
} else {
gradientTrafficLayer.isVisible = true
sender.setTitle("Hide traffic gradient", for: .normal)
}
}
// Uncomment to use real directions route
// func generateRoute() {
// let waypoints = [
// Waypoint(coordinate: CLLocationCoordinate2D(latitude: 34.059645958539654, longitude: -118.24825286865234), name: "Start"),
// Waypoint(coordinate: CLLocationCoordinate2D(latitude: 34.053352767594625, longitude: -118.24076414108275), name: "End"),
// ]
//
// let options = RouteOptions(waypoints: waypoints, profileIdentifier: .automobileAvoidingTraffic)
// options.attributeOptions = [.congestionLevel]
// options.includesSteps = true
// options.routeShapeResolution = .full
//
// _ = Directions.shared.calculate(options) { (session, result) in
// switch result {
// case .failure(let error):
// print("Error calculating directions: \(error)")
// case .success(let response):
// guard let route = response.routes?.first else { return }
// self.drawRoute(route)
// }
// }
// }
// func drawRoute(_ route: Route) {
// let contiguousRoute = MGLPolyline(coordinates: route.shape!.coordinates, count: UInt(route.shape!.coordinates.count))
// let continguousRouteSource = MGLShapeSource(identifier: "contiguous-route-source", shape: contiguousRoute, options: [MGLShapeSourceOption.lineDistanceMetrics:true])
// mapView.style?.addSource(continguousRouteSource)
//
// // Fit camera to route
// let currentCamera = mapView.camera
// let newCamera = mapView.camera(currentCamera, fitting: contiguousRoute, edgePadding: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10))
// mapView.setCamera(newCamera, animated: false)
//
// // Individual polylines associated with a congestion level
// guard let congestionSegments = addCongestion(to: route, legIndex: 0) else { return }
// congestionLineSegments = congestionSegments
// let congestionSource = MGLShapeSource(identifier: "congestion-source", features: congestionSegments, options: nil)
//
// let congestionLayer = MGLLineStyleLayer(identifier: "congestion-line", source: congestionSource)
// congestionLayer.lineWidth = NSExpression(forConstantValue: 20.0)
// congestionLayer.lineColor = NSExpression(format: "MGL_MATCH(congestion, 'low' , %@, 'moderate', %@, 'heavy', %@, 'severe', %@, %@)", trafficLowColor, trafficModerateColor, trafficHeavyColor, trafficSevereColor, trafficUnknownColor)
// mapView.style?.addSource(congestionSource)
// mapView.style?.addLayer(congestionLayer)
//
// // Experimental traffic gradient layer
// gradientTrafficLayer = MGLLineStyleLayer(identifier: "route-line-gradient", source: continguousRouteSource)
// gradientTrafficLayer.lineWidth = NSExpression(forConstantValue: 20.0)
// gradientTrafficLayer.lineColor = NSExpression(forConstantValue: UIColor.blue.withAlphaComponent(0.4))
// mapView.style?.addLayer(gradientTrafficLayer)
//
// let gradientStops = generateTrafficGradientExpression(route: route)
//
// gradientTrafficLayer.lineGradient = NSExpression(format: "mgl_interpolate:withCurveType:parameters:stops:($lineProgress, 'linear', nil, %@)", gradientStops)
//
// }
func drawMockRoute(_ mockRoute: MockRoute) {
let fullRoute = MGLPolyline(coordinates: mockRoute.shape.coordinates, count: UInt(mockRoute.shape.coordinates.count))
let newCamera = mapView.camera(mapView.camera, fitting: fullRoute, edgePadding: UIEdgeInsets(top: 60, left: 20, bottom: 60, right: 20))
mapView.setCamera(newCamera, animated: false)
let source = MGLShapeSource(identifier: "mock-route-source", shape: fullRoute, options: [MGLShapeSourceOption.lineDistanceMetrics:true])
mapView.style?.addSource(source)
let segmentSource = MGLShapeSource(identifier: "mock-segments-source", features: mockRoute.congestionLineSegments, options: nil)
mapView.style?.addSource(segmentSource)
// Hard color stops
let colorStopsLayer = MGLLineStyleLayer(identifier: "mock-route-layer", source: segmentSource)
colorStopsLayer.lineWidth = NSExpression(forConstantValue: 20.0)
colorStopsLayer.lineColor = NSExpression(format: "MGL_MATCH(congestion, 'low' , %@, 'moderate', %@, 'heavy', %@, 'severe', %@, %@)", trafficLowColor, trafficModerateColor, trafficHeavyColor, trafficSevereColor, trafficUnknownColor)
mapView.style?.addLayer(colorStopsLayer)
gradientTrafficLayer = MGLLineStyleLayer(identifier: "mock-gradient-layer", source: source)
let gradientStops = generateTrafficGradientExpression(route: mockRoute)
typealias GradientStop = (percent: CGFloat, value: UIColor)
let gradientResult = gradientStops.keys.sorted().map {
return GradientStop(percent: $0, value: gradientStops[$0]!)
}
for stop in gradientResult {
print(stop)
}
gradientTrafficLayer.lineGradient = NSExpression(format: "mgl_interpolate:withCurveType:parameters:stops:($lineProgress, 'linear', nil, %@)", gradientStops)
gradientTrafficLayer.lineWidth = NSExpression(forConstantValue: 20.0)
mapView.style?.addLayer(gradientTrafficLayer)
}
}
// NavigationMapView methods
extension ViewController {
func shape(for routes: [Route], legIndex: Int?) -> MGLShape? {
//segmentDistances(routes.first!)
guard let firstRoute = routes.first else { return nil }
guard let congestedRoute = addCongestion(to: firstRoute, legIndex: legIndex) else { return nil }
var altRoutes: [MGLPolylineFeature] = []
for route in routes.suffix(from: 1) {
let polyline = MGLPolylineFeature(coordinates: route.shape!.coordinates, count: UInt(route.shape!.coordinates.count))
polyline.attributes["isAlternateRoute"] = true
altRoutes.append(polyline)
}
return MGLShapeCollectionFeature(shapes: altRoutes + congestedRoute)
}
func addCongestion(to route: Route, legIndex: Int?) -> [MGLPolylineFeature]? {
guard let coordinates = route.shape?.coordinates else { return nil }
var linesPerLeg: [MGLPolylineFeature] = []
for (index, leg) in route.legs.enumerated() {
let lines: [MGLPolylineFeature]
// If there is no congestion, don't try and add it
if let legCongestion = leg.segmentCongestionLevels, legCongestion.count < coordinates.count {
// The last coord of the preceding step, is shared with the first coord of the next step, we don't need both.
let legCoordinates: [CLLocationCoordinate2D] = leg.steps.enumerated().reduce([]) { allCoordinates, current in
let index = current.offset
let step = current.element
let stepCoordinates = step.shape!.coordinates
return index == 0 ? stepCoordinates : allCoordinates + stepCoordinates.suffix(from: 1)
}
let mergedCongestionSegments = combine(legCoordinates, with: legCongestion)
lines = mergedCongestionSegments.map { (congestionSegment: CongestionSegment) -> MGLPolylineFeature in
let polyline = MGLPolylineFeature(coordinates: congestionSegment.0, count: UInt(congestionSegment.0.count))
polyline.attributes[MBCongestionAttribute] = String(describing: congestionSegment.1)
return polyline
}
} else {
lines = [MGLPolylineFeature(coordinates: route.shape!.coordinates, count: UInt(route.shape!.coordinates.count))]
}
for line in lines {
line.attributes["isAlternateRoute"] = false
if let legIndex = legIndex {
line.attributes[MBCurrentLegAttribute] = index == legIndex
} else {
line.attributes[MBCurrentLegAttribute] = index == 0
}
}
linesPerLeg.append(contentsOf: lines)
}
return linesPerLeg
}
func combine(_ coordinates: [CLLocationCoordinate2D], with congestions: [CongestionLevel]) -> [CongestionSegment] {
var segments: [CongestionSegment] = []
segments.reserveCapacity(congestions.count)
for (index, congestion) in congestions.enumerated() {
let congestionSegment: ([CLLocationCoordinate2D], CongestionLevel) = ([coordinates[index], coordinates[index + 1]], congestion)
let coordinates = congestionSegment.0
let congestionLevel = congestionSegment.1
if segments.last?.1 == congestionLevel {
segments[segments.count - 1].0 += coordinates
} else {
segments.append(congestionSegment)
}
}
return segments
}
}
struct MockRoute {
var shape: LineString {
get {
let coordinates = [
CLLocationCoordinate2D(latitude: 0, longitude: 0),
CLLocationCoordinate2D(latitude: 1, longitude: 0),
CLLocationCoordinate2D(latitude: 2, longitude: 0),
CLLocationCoordinate2D(latitude: 3, longitude: 0),
CLLocationCoordinate2D(latitude: 4, longitude: 0),
CLLocationCoordinate2D(latitude: 5, longitude: 0)
]
return LineString(coordinates)
}
}
var distance: CLLocationDirection {
get {
return shape.distance()
}
}
var congestionLineSegments: [MGLPolylineFeature] {
let polyline1Coords = [
CLLocationCoordinate2D(latitude: 0, longitude: 0),
CLLocationCoordinate2D(latitude: 1, longitude: 0)
]
let polyline1 = MGLPolylineFeature(coordinates: polyline1Coords, count: UInt(polyline1Coords.count))
polyline1.attributes["congestion"] = "low"
let polyline2Coords = [
CLLocationCoordinate2D(latitude: 1, longitude: 0),
CLLocationCoordinate2D(latitude: 2, longitude: 0)
]
let polyline2 = MGLPolylineFeature(coordinates: polyline2Coords, count: UInt(polyline2Coords.count))
polyline2.attributes["congestion"] = "heavy"
let polyline3Coords = [
CLLocationCoordinate2D(latitude: 2, longitude: 0),
CLLocationCoordinate2D(latitude: 5, longitude: 0)
]
let polyline3 = MGLPolylineFeature(coordinates: polyline3Coords, count: UInt(polyline3Coords.count))
polyline3.attributes["congestion"] = "unknown"
return [polyline1, polyline2, polyline3]
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment