Skip to content

Commit

Permalink
Extend Example with ability to edit waypoints
Browse files Browse the repository at this point in the history
  • Loading branch information
S2Ler committed Jul 22, 2021
1 parent f55782c commit b9545d0
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 100 deletions.
4 changes: 3 additions & 1 deletion Directions Example/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import SwiftUI
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView(vm: DirectionsViewModel())
NavigationView {
ContentView(vm: DirectionsViewModel())
}
}
}
}
131 changes: 34 additions & 97 deletions Directions Example/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,16 @@ import Combine
import MapboxDirections

final class DirectionsViewModel: ObservableObject {
private let distanceFormatter: LengthFormatter = .init()
private let travelTimeFormatter: DateComponentsFormatter = .init()

@Published
var routes: [Route] = []

init() {
travelTimeFormatter.unitsStyle = .short

}

func loadRoutes() {
let startPoint = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 38.9131752, longitude: -77.0324047),
name: "Mapbox")
let stopPoint = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 38.89065720, longitude: -77.0090701),
name: "Capitol")
let endPoint = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 38.8977, longitude: -77.0365),
name: "White House")
let options = RouteOptions(waypoints: [startPoint, stopPoint, endPoint])
func loadRoutes(for waypoints: [Waypoint]) {
let options = RouteOptions(waypoints: waypoints.map(\.native))
print("Calculating route for \(options.waypoints)")
options.includesSteps = true
options.routeShapeResolution = .full
options.attributeOptions = [.congestionLevel, .maximumSpeedLimit]
Expand All @@ -35,101 +27,46 @@ final class DirectionsViewModel: ObservableObject {
}
}
}

func formattedDistance(for route: Route) -> String {
return distanceFormatter.string(fromMeters: route.distance)
}

func formattedTravelTime(for route: Route) -> String {
return travelTimeFormatter.string(from: route.expectedTravelTime)!
}

func formattedTypicalTravelTime(for route: Route) -> String {
if let typicalTravelTime = route.typicalTravelTime,
let formattedTypicalTravelTime = travelTimeFormatter.string(from: typicalTravelTime) {
return formattedTypicalTravelTime
}
else {
return "Not available"
}
}

func stepDescriptions(for step: RouteStep) -> String {
var description: String = ""
let direction = step.maneuverDirection?.rawValue ?? "none"
description.append("\(step.instructions) [\(step.maneuverType) \(direction)]")
if step.distance > 0 {
let formattedDistance = distanceFormatter.string(fromMeters: step.distance)
description.append(" (\(step.transportType) for \(formattedDistance))")
}
return description
}
}

struct ContentView: View {
@ObservedObject
var vm: DirectionsViewModel

@State
private var waypoints: [Waypoint] = .defaultWaypoints

@State
private var showRoutes: Bool = false

var body: some View {
ScrollView {
LazyVStack(spacing: 10, content: {
ForEach(vm.routes, id: \.distance) { route in
VStack(alignment: .leading, spacing: 3) {
headerView(for: route)
ForEach(0..<route.legs.count, id: \.self) { legIdx in
if let source = route.legs[legIdx].source?.name,
let destination = route.legs[legIdx].destination?.name {
Text("From '\(source)' to '\(destination)'").font(.title2)
}
else {
Text("Steps:").font(.title2)
}
stepsView(for: route.legs[legIdx])
}
VStack {
WaypointsEditor(waypoints: $waypoints)
NavigationLink(
destination: RoutesView(routes: vm.routes),
isActive: .init(get: {
!vm.routes.isEmpty
}, set: { isActive in
if !isActive {
vm.routes.removeAll()
}
}
})
}
.padding(5)
.onAppear { vm.loadRoutes() }
}

@ViewBuilder
private func headerView(for route: Route) -> some View {
VStack(alignment: .leading, spacing: 10) {
HStack {
Text("Route: ").fontWeight(.bold)
Text(route.description)
.fixedSize(horizontal: false, vertical: true)
}
HStack {
Text("Distance: ").fontWeight(.bold)
Text(vm.formattedDistance(for:route))
}
HStack {
Text("ETA: ").fontWeight(.bold)
Text(vm.formattedTravelTime(for: route))
}
HStack {
Text("Typical travel time: ").fontWeight(.bold)
Text(vm.formattedTypicalTravelTime(for: route))
}
Divider()
}),
label: {
Button("Calculate") {
vm.loadRoutes(for: waypoints)
}
})
}
.navigationTitle("Edit Route Waypoints")
}
}

@ViewBuilder
private func stepsView(for leg: RouteLeg) -> some View {
LazyVStack(alignment: .leading, spacing: 5, content: {
ForEach(0..<leg.steps.count, id: \.self) { stepIdx in
HStack {
Text("\(stepIdx + 1). ").fontWeight(.bold)
Text(vm.stepDescriptions(for: leg.steps[stepIdx]))
}
.padding([.top, .bottom], 3)

Divider()
}
})
private extension Array where Element == Waypoint {
static var defaultWaypoints: Self {
[
.init(latitude: 38.9131752, longitude: -77.0324047, name: "Mapbox"),
.init(latitude: 38.8906572, longitude: -77.0090701, name: "Capitol"),
.init(latitude: 38.8977000, longitude: -77.0365000, name: "White House"),
]
}
}
106 changes: 106 additions & 0 deletions Directions Example/RoutesView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import Foundation
import SwiftUI
import MapboxDirections

struct RoutesView: View {
private static let distanceFormatter: LengthFormatter = .init()
private static let travelTimeFormatter: DateComponentsFormatter = {
let f = DateComponentsFormatter()
f.unitsStyle = .short
return f
}()

let routes: [Route]

var body: some View {
ScrollView {
LazyVStack(spacing: 10, content: {
ForEach(routes, id: \.distance) { route in
VStack(alignment: .leading, spacing: 3) {
headerView(for: route)
ForEach(0..<route.legs.count, id: \.self) { legIdx in
if let source = route.legs[legIdx].source?.name,
let destination = route.legs[legIdx].destination?.name {
Text("From '\(source)' to '\(destination)'").font(.title2)
}
else {
Text("Steps:").font(.title2)
}
stepsView(for: route.legs[legIdx])
}
}
}
})
}
.navigationTitle("Routes")
.padding(5)
}

@ViewBuilder
private func headerView(for route: Route) -> some View {
VStack(alignment: .leading, spacing: 10) {
HStack {
Text("Route: ").fontWeight(.bold)
Text(route.description)
.fixedSize(horizontal: false, vertical: true)
}
HStack {
Text("Distance: ").fontWeight(.bold)
Text(formattedDistance(for:route))
}
HStack {
Text("ETA: ").fontWeight(.bold)
Text(formattedTravelTime(for: route))
}
HStack {
Text("Typical travel time: ").fontWeight(.bold)
Text(formattedTypicalTravelTime(for: route))
}
Divider()
}
}

@ViewBuilder
private func stepsView(for leg: RouteLeg) -> some View {
LazyVStack(alignment: .leading, spacing: 5, content: {
ForEach(0..<leg.steps.count, id: \.self) { stepIdx in
HStack {
Text("\(stepIdx + 1). ").fontWeight(.bold)
Text(stepDescriptions(for: leg.steps[stepIdx]))
}
.padding([.top, .bottom], 3)

Divider()
}
})
}

func formattedDistance(for route: Route) -> String {
return Self.distanceFormatter.string(fromMeters: route.distance)
}

func formattedTravelTime(for route: Route) -> String {
return Self.travelTimeFormatter.string(from: route.expectedTravelTime)!
}

func formattedTypicalTravelTime(for route: Route) -> String {
if let typicalTravelTime = route.typicalTravelTime,
let formattedTypicalTravelTime = Self.travelTimeFormatter.string(from: typicalTravelTime) {
return formattedTypicalTravelTime
}
else {
return "Not available"
}
}

func stepDescriptions(for step: RouteStep) -> String {
var description: String = ""
let direction = step.maneuverDirection?.rawValue ?? "none"
description.append("\(step.instructions) [\(step.maneuverType) \(direction)]")
if step.distance > 0 {
let formattedDistance = Self.distanceFormatter.string(fromMeters: step.distance)
description.append(" (\(step.transportType) for \(formattedDistance))")
}
return description
}
}
102 changes: 102 additions & 0 deletions Directions Example/WaypointsEditor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import Foundation
import SwiftUI
import CoreLocation
import MapboxDirections

struct Waypoint: Identifiable, Hashable {
let id: UUID = UUID()
var latitude: CLLocationDegrees = 0
var longitude: CLLocationDegrees = 0
var name: String = ""

var native: MapboxDirections.Waypoint {
.init(coordinate: .init(latitude: latitude, longitude: longitude), name: name)
}
}

struct WaypointsEditor: View {
@Binding
var waypoints: [Waypoint]

var body: some View {
List {
ForEach(waypoints) { waypoint in
HStack {
WaypointView(waypoint: .init(get: {
self.waypoints.first(where: { $0.id == waypoint.id })!
}, set: { newValue in
self.waypoints.firstIndex(of: waypoint).map {
waypoints[$0] = newValue
}
}))

Menu {
Button("Insert Above") {
addNewWaypoint(before: waypoint)
}
Button("Insert Below") {
addNewWaypoint(after: waypoint)
}
} label: {
Image(systemName: "ellipsis")
.frame(width: 30, height: 30, alignment: .center)
}
}
}
.onMove { indices, newOffset in
waypoints.move(fromOffsets: indices, toOffset: newOffset)
}
.onDelete { indexSet in
waypoints.remove(atOffsets: indexSet)
}
}
.listStyle(InsetGroupedListStyle())
.toolbar {
EditButton()
}
}

private func addNewWaypoint(after waypoint: Waypoint) {
guard let waypointIndex = waypoints.firstIndex(of: waypoint) else {
preconditionFailure("Waypoint is in the array of waypoints")
}
let insertionIndex = waypoints.index(after: waypointIndex)
waypoints.insert(Waypoint(), at: insertionIndex)
}
private func addNewWaypoint(before waypoint: Waypoint) {
guard let insertionIndex = waypoints.firstIndex(of: waypoint) else {
preconditionFailure("Waypoint is in the array of waypoints")
}
waypoints.insert(Waypoint(), at: insertionIndex)
}
}

struct WaypointView: View {
@Binding
var waypoint: Waypoint

@State
private var latitudeString: String

@State
private var longitudeString: String

init(waypoint: Binding<Waypoint>) {
_waypoint = waypoint
_latitudeString = State<String>(initialValue: waypoint.wrappedValue.latitude.description)
_longitudeString = .init(initialValue: waypoint.wrappedValue.longitude.description)
}

var body: some View {
HStack {
TextField("Name", text: $waypoint.name)
TextField("Lat", text: $latitudeString, onEditingChanged: { _ in
waypoint.latitude = .init(latitudeString) ?? 0
})

TextField("Lon", text: $longitudeString, onEditingChanged: { _ in
waypoint.longitude = .init(longitudeString) ?? 0
})
}
}
}
Loading

0 comments on commit b9545d0

Please sign in to comment.