-
-
Save captainbarbosa/99129e0a50ac4480d900ee8f80fa3ac7 to your computer and use it in GitHub Desktop.
Drawing traffic with gradients
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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