Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

StopViewController SwiftUI #648

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions OBAKit/Mapping/MapSnapshotter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,31 @@ class MapSnapshotter: NSObject {
return options
}

public func snapshot(stop: Stop, traitCollection: UITraitCollection) async throws -> UIImage {
let options = snapshotOptions(stop: stop, traitCollection: traitCollection)

let snapshotter = MKMapSnapshotter(options: options)
let snapshot = try await snapshotter.start()

// Generate the stop icon.
let stopIcon = self.stopIconFactory.buildIcon(for: stop, isBookmarked: false, traits: traitCollection)

// Calculate the point at which to draw the stop icon.
// It needs to be shifted up by 1/2 the stop icon height
// in order to draw it at the proper location.
var point = snapshot.point(for: stop.coordinate)
point.y -= (stopIcon.size.height / 2.0)

// Render the composited image.
var annotatedImage = UIImage.draw(image: stopIcon, onto: snapshot.image, at: point)

if traitCollection.userInterfaceStyle == .light {
annotatedImage = annotatedImage.darkened()
}

return annotatedImage
}

public func snapshot(stop: Stop, traitCollection: UITraitCollection, completion: @escaping MapSnapshotterCompletionHandler) {
let options = snapshotOptions(stop: stop, traitCollection: traitCollection)

Expand Down
22 changes: 22 additions & 0 deletions OBAKit/Mapping/MapSnapshotterEnvironmentKey.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// MapSnapshotterEnvironmentKey.swift
// OBAKit
//
// Created by Alan Chu on 2/22/23.
//

import SwiftUI
import OBAKitCore

private struct StopIconFactoryKey: EnvironmentKey {
static public let defaultValue: StopIconFactory = {
return StopIconFactory(iconSize: ThemeMetrics.defaultMapAnnotationSize, themeColors: ThemeColors.shared)
}()
}

extension EnvironmentValues {
var stopIconFactory: StopIconFactory {
get { self[StopIconFactoryKey.self] }
set { self[StopIconFactoryKey.self] = newValue }
}
}
2 changes: 1 addition & 1 deletion OBAKit/Reporting/ReportProblemViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ class ReportProblemViewController: TaskController<StopArrivals>,
}

func onSelectArrivalDeparture(_ arrivalDepartureItem: ArrivalDepartureItem) {
guard let arrDep = data?.arrivalsAndDepartures.first(where: { $0.id == arrivalDepartureItem.arrivalDepartureID }) else { return }
guard let arrDep = data?.arrivalsAndDepartures.first(where: { $0.id == arrivalDepartureItem.id }) else { return }
let controller = VehicleProblemViewController(application: self.application, arrivalDeparture: arrDep)
self.navigationController?.pushViewController(controller, animated: true)
}
Expand Down
84 changes: 84 additions & 0 deletions OBAKit/Stops/ArrivalDepartureController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// ArrivalDepartureController.swift
// OBAKit
//
// Created by Alan Chu on 2/11/23.
//

import SwiftUI
import OBAKitCore

class ArrivalDepartureController: ObservableObject {
@MainActor @Published private(set) var arrivalDepartures: [ArrivalDepartureViewObject] = []
private var stopArrivals: StopArrivals?

@Published var minutesBefore: UInt = 10
@Published var minutesAfter: UInt = 60

private(set) var dateInterval: DateInterval?
private(set) var lastUpdated: Date? {
didSet {
guard let lastUpdated else {
return
}

dateInterval = DateInterval(
start: lastUpdated.addingTimeInterval(-Double(minutesBefore * 60)),
end: lastUpdated.addingTimeInterval(Double(minutesAfter * 60))
)
}
}

let application: Application
let stopID: StopID

init(application: Application, stopID: StopID) {
self.application = application
self.stopID = stopID
}

private var lock: Bool = false
func load() async {
guard !lock else {
return
}

lock = true
defer {
lock = false
}

guard let apiService = application.apiService else {
return
}

let stopArrivals = try? await apiService.getArrivalsAndDeparturesForStop(id: stopID, minutesBefore: minutesBefore, minutesAfter: minutesAfter).entry

await MainActor.run {
lastUpdated = .now
}

guard let stopArrivals else {
return
}

let newArrivalDepartures = stopArrivals.arrivalsAndDepartures.map(ArrivalDepartureViewObject.init)

// todo: use OrderedDictionary?
await MainActor.run {
// Do simple diffing
for new in newArrivalDepartures {
let existingIndex = self.arrivalDepartures.firstIndex {
$0.id == new.id
}

if let existingIndex {
self.arrivalDepartures[existingIndex].update(with: new)
} else {
// TODO: Insert by date
self.arrivalDepartures.append(new)
}
}
}
}
}
53 changes: 53 additions & 0 deletions OBAKit/Stops/ArrivalDepartureView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// TripArrivalView.swift
// OBAKit
//
// Created by Alan Chu on 2/9/23.
//

import SwiftUI
import OBAKitCore

/// A view that tracks an `ArrivalDeparture`.
struct ArrivalDepartureView: View {
@ObservedObject var viewObject: ArrivalDepartureViewObject

@State var deemphasizePastTrips: Bool = true

var body: some View {
HStack {
VStack(alignment: .leading) {
Text(viewObject.routeAndHeadsign)
.font(.headline)
StopArrivalExplanationView(
arrivalDepartureDate: viewObject.arrivalDepartureDate,
scheduleStatus: viewObject.scheduleStatus,
temporalState: viewObject.temporalState,
arrivalDepartureStatus: viewObject.arrivalDepartureStatus,
scheduleDeviationInMinutes: viewObject.scheduleDeviationInMinutes
)
.font(.subheadline)
}

Spacer()

DepartureTimeBadgeView(
date: $viewObject.arrivalDepartureDate,
temporalState: $viewObject.temporalState,
scheduleStatus: $viewObject.scheduleStatus)
}
.opacity(deemphasizePastTrips && viewObject.temporalState == .past ? 0.3 : 1.0)
}
}

#if DEBUG
struct TripArrivalVieww_Previews: PreviewProvider {
static var previews: some View {
List {
ForEach(ArrivalDepartureViewObject.all) { tripArrival in
ArrivalDepartureView(viewObject: tripArrival)
}
}
}
}
#endif
122 changes: 122 additions & 0 deletions OBAKit/Stops/ArrivalDepartureViewObject.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//
// ArrivalDepartureViewObject.swift
// OBAKit
//
// Created by Alan Chu on 2/12/23.
//

import SwiftUI
import OBAKitCore

class ArrivalDepartureViewObject: Identifiable, ObservableObject/*, Comparable*/ {
let id: ArrivalDeparture.Identifier

@Published var routeAndHeadsign: String

@Published var arrivalDepartureDate: Date
@Published var arrivalDepartureStatus: ArrivalDepartureStatus
@Published var temporalState: TemporalState
@Published var scheduleStatus: ScheduleStatus
@Published var scheduleDeviationInMinutes: Int

init(_ arrivalDeparture: ArrivalDeparture) {
self.id = arrivalDeparture.id

self.routeAndHeadsign = arrivalDeparture.routeAndHeadsign
self.arrivalDepartureDate = arrivalDeparture.arrivalDepartureDate
self.arrivalDepartureStatus = arrivalDeparture.arrivalDepartureStatus
self.temporalState = arrivalDeparture.temporalState
self.scheduleStatus = arrivalDeparture.scheduleStatus
self.scheduleDeviationInMinutes = arrivalDeparture.deviationFromScheduleInMinutes
}

init(
id: ArrivalDeparture.Identifier,
routeAndHeadsign: String,
arrivalDepartureDate: Date,
arrivalDepartureStatus: ArrivalDepartureStatus,
temporalState: TemporalState,
scheduleStatus: ScheduleStatus,
scheduleDeviationInMinutes: Int
) {
self.id = id
self.routeAndHeadsign = routeAndHeadsign
self.arrivalDepartureDate = arrivalDepartureDate
self.arrivalDepartureStatus = arrivalDepartureStatus
self.temporalState = temporalState
self.scheduleStatus = scheduleStatus
self.scheduleDeviationInMinutes = scheduleDeviationInMinutes
}

@MainActor
func update(with newValues: ArrivalDepartureViewObject) {
precondition(newValues.id == self.id)

self.routeAndHeadsign = newValues.routeAndHeadsign
self.arrivalDepartureDate = newValues.arrivalDepartureDate
self.arrivalDepartureStatus = newValues.arrivalDepartureStatus
self.temporalState = newValues.temporalState
self.scheduleStatus = newValues.scheduleStatus
self.scheduleDeviationInMinutes = newValues.scheduleDeviationInMinutes
}

@objc func debugQuickLookObject() -> Any? {
"""
Route & Headsign: \(routeAndHeadsign)
Date: \(arrivalDepartureDate)
Status: \(arrivalDepartureStatus)
State: \(temporalState)
Schedule Status: \(scheduleStatus)
Schedule Deviation: \(scheduleDeviationInMinutes) mins
"""
}
}

// MARK: - Xcode Previews

#if DEBUG

extension ArrivalDepartureViewObject {
static var all: [ArrivalDepartureViewObject] {
return [
.pastArrivingEarly,
.presentDepartingDelayed
].sorted(by: \.arrivalDepartureDate)
}

private static var startOfToday: Date {
return Calendar.current.startOfDay(for: .now)
}

static func nowOffsetBy(minutes: Int) -> Date {
return .now.addingTimeInterval(60 * Double(minutes))
}

static var pastArrivingEarly: ArrivalDepartureViewObject {
let id = ArrivalDeparture.Identifier(serviceDate: startOfToday, stopID: "1_1234", routeID: "1_4567", tripID: "1_987654321", stopSequence: 0)
return .init(
id: id,
routeAndHeadsign: "4567 - Past Arrived Early",
arrivalDepartureDate: nowOffsetBy(minutes: -5),
arrivalDepartureStatus: .arriving,
temporalState: .past,
scheduleStatus: .early,
scheduleDeviationInMinutes: 3
)
}

static var presentDepartingDelayed: ArrivalDepartureViewObject {
let id = ArrivalDeparture.Identifier(serviceDate: startOfToday, stopID: "1_1234", routeID: "1_4321", tripID: "1_123454321", stopSequence: 0)
return .init(
id: id,
routeAndHeadsign: "4321 - NOW Departing Late",
arrivalDepartureDate: nowOffsetBy(minutes: 1),
arrivalDepartureStatus: .departing,
temporalState: .present,
scheduleStatus: .delayed,
scheduleDeviationInMinutes: 3
)
}
}

#endif
7 changes: 2 additions & 5 deletions OBAKit/Stops/Sections/StopArrival/StopArrivalItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ struct ArrivalDepartureItem: OBAListViewItem {
var bookmarkAction: OBAListViewAction<ArrivalDepartureItem>?
var shareAction: OBAListViewAction<ArrivalDepartureItem>?

let id: UUID = UUID()
let arrivalDepartureID: String
let id: ArrivalDeparture.Identifier
let routeID: RouteID
let stopID: StopID

Expand Down Expand Up @@ -103,7 +102,7 @@ struct ArrivalDepartureItem: OBAListViewItem {
bookmarkAction: OBAListViewAction<ArrivalDepartureItem>? = nil,
shareAction: OBAListViewAction<ArrivalDepartureItem>? = nil) {

self.arrivalDepartureID = arrivalDeparture.id
self.id = arrivalDeparture.id
self.routeID = arrivalDeparture.routeID
self.stopID = arrivalDeparture.stopID
self.name = arrivalDeparture.routeAndHeadsign
Expand Down Expand Up @@ -132,7 +131,6 @@ struct ArrivalDepartureItem: OBAListViewItem {

func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(arrivalDepartureID)
hasher.combine(routeID)
hasher.combine(stopID)
hasher.combine(name)
Expand All @@ -143,7 +141,6 @@ struct ArrivalDepartureItem: OBAListViewItem {

static func == (lhs: ArrivalDepartureItem, rhs: ArrivalDepartureItem) -> Bool {
return lhs.id == rhs.id &&
lhs.arrivalDepartureID == rhs.arrivalDepartureID &&
lhs.routeID == rhs.routeID &&
lhs.stopID == rhs.stopID &&
lhs.name == rhs.name &&
Expand Down
Loading