Skip to content

Commit

Permalink
feat: Add alert icons for stops on the map (#111)
Browse files Browse the repository at this point in the history
* refactor: look up nearby stop IDs in global response

* move global data fetch up from map to ContentView

* resources: Add new icons for stops on map

* feat: Display stop icons with alert status

* test: Fix existing tests

* test: Add tests for stop alert status

* test: Fix failing test

* refactor: Move stop icon details out into class

* fix: Changes based on PR feedback

* fix: Fix iOS tests

---------

Co-authored-by: Melody Horn <[email protected]>
  • Loading branch information
EmmaSimon and boringcactus authored Apr 4, 2024
1 parent a96b8b9 commit 7a6ed82
Show file tree
Hide file tree
Showing 29 changed files with 907 additions and 133 deletions.
4 changes: 4 additions & 0 deletions iosApp/iosApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
9A8B34B02B892C810018412C /* Polyline in Frameworks */ = {isa = PBXBuildFile; productRef = 9A8B34AF2B892C810018412C /* Polyline */; };
9A9E05F42B6D6DEA0086B437 /* SearchResultFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A9E05F32B6D6DEA0086B437 /* SearchResultFetcher.swift */; };
9A9E05F62B6D6EF70086B437 /* NearbyFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A9E05F52B6D6EF70086B437 /* NearbyFetcher.swift */; };
9AB446B02BBDDCAF00D8C920 /* StopIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AB446AF2BBDDCAF00D8C920 /* StopIcons.swift */; };
9AB44A112B8FC43E00E8FFB3 /* IconCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AB44A102B8FC43E00E8FFB3 /* IconCard.swift */; };
9AB44A132B911E6400E8FFB3 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AB44A122B911E6400E8FFB3 /* DateExtension.swift */; };
9AC10BDA2B80067400EA4605 /* ColorHexExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AC10BD92B80067400EA4605 /* ColorHexExtension.swift */; };
Expand Down Expand Up @@ -193,6 +194,7 @@
9A8B34AC2B88E5080018412C /* RailRouteShapeFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RailRouteShapeFetcher.swift; sourceTree = "<group>"; };
9A9E05F32B6D6DEA0086B437 /* SearchResultFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultFetcher.swift; sourceTree = "<group>"; };
9A9E05F52B6D6EF70086B437 /* NearbyFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyFetcher.swift; sourceTree = "<group>"; };
9AB446AF2BBDDCAF00D8C920 /* StopIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopIcons.swift; sourceTree = "<group>"; };
9AB44A102B8FC43E00E8FFB3 /* IconCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconCard.swift; sourceTree = "<group>"; };
9AB44A122B911E6400E8FFB3 /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = "<group>"; };
9AC10BD92B80067400EA4605 /* ColorHexExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorHexExtension.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -432,6 +434,7 @@
9A5B275B2BB237DE009A6FC6 /* RecenterButton.swift */,
9A5B27552BB221C1009A6FC6 /* RouteLayerGenerator.swift */,
9A5B27532BB1EF53009A6FC6 /* RouteSourceGenerator.swift */,
9AB446AF2BBDDCAF00D8C920 /* StopIcons.swift */,
9A5B27592BB22D91009A6FC6 /* StopLayerGenerator.swift */,
9A5B27512BB1EF45009A6FC6 /* StopSourceGenerator.swift */,
);
Expand Down Expand Up @@ -849,6 +852,7 @@
9A5B27582BB22BF9009A6FC6 /* MapLayerManager.swift in Sources */,
9A03F3662BA9E68500DA40DC /* Debouncer.swift in Sources */,
9A8B34AD2B88E5090018412C /* RailRouteShapeFetcher.swift in Sources */,
9AB446B02BBDDCAF00D8C920 /* StopIcons.swift in Sources */,
9A2005CB2B97B68700F562E1 /* UpcomingTripView.swift in Sources */,
9AB44A132B911E6400E8FFB3 /* DateExtension.swift in Sources */,
6EE745862B965C130052227E /* Message.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images": [
{
"filename": "bus-stop-issues.svg",
"idiom": "universal",
"scale": "1x"
},
{
"idiom": "universal",
"scale": "2x"
},
{
"idiom": "universal",
"scale": "3x"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images": [
{
"filename": "bus-stop-no-service.svg",
"idiom": "universal",
"scale": "1x"
},
{
"idiom": "universal",
"scale": "2x"
},
{
"idiom": "universal",
"scale": "3x"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images": [
{
"filename": "t-station-issues.svg",
"idiom": "universal",
"scale": "1x"
},
{
"idiom": "universal",
"scale": "2x"
},
{
"idiom": "universal",
"scale": "3x"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images": [
{
"filename": "t-station-no-service.svg",
"idiom": "universal",
"scale": "1x"
},
{
"idiom": "universal",
"scale": "2x"
},
{
"idiom": "universal",
"scale": "3x"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions iosApp/iosApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ struct ContentView: View {
Text("Location access state unknown")
}
HomeMapView(
alertsFetcher: alertsFetcher,
globalFetcher: globalFetcher,
nearbyFetcher: nearbyFetcher,
railRouteShapeFetcher: railRouteShapeFetcher,
Expand Down
9 changes: 9 additions & 0 deletions iosApp/iosApp/Fetchers/GlobalFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import shared

class GlobalFetcher: ObservableObject {
var response: GlobalResponse?
var globalStaticData: GlobalStaticData?
@Published var stops: [String: Stop]
@Published var routes: [String: Route]
let backend: any BackendProtocol
Expand All @@ -27,8 +28,16 @@ class GlobalFetcher: ObservableObject {
self.response = response
stops = response.stops
routes = response.routes
globalStaticData = GlobalStaticData(globalData: response)
} catch {
print("Failed to load global data: \(error)")
}
}

func getRealtimeAlertsByStop(
alerts: AlertsStreamDataResponse?,
filterAtTime: Instant
) -> [String: AlertAssociatedStop] {
globalStaticData?.withRealtimeAlertsByStop(alerts: alerts, filterAtTime: filterAtTime) ?? [:]
}
}
66 changes: 52 additions & 14 deletions iosApp/iosApp/Pages/Map/HomeMapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import SwiftUI
struct HomeMapView: View {
private let cameraDebouncer = Debouncer(delay: 0.25)

@ObservedObject var alertsFetcher: AlertsFetcher
@ObservedObject var globalFetcher: GlobalFetcher
@ObservedObject var nearbyFetcher: NearbyFetcher
@ObservedObject var railRouteShapeFetcher: RailRouteShapeFetcher
Expand All @@ -23,16 +24,22 @@ struct HomeMapView: View {
@State private var layerManager: MapLayerManager?
@StateObject private var locationDataManager: LocationDataManager
@State private var recenterButton: ViewAnnotation?
@State private var now = Date.now
@State private var currentStopAlerts: [String: AlertAssociatedStop] = [:]
@Binding var sheetHeight: CGFloat

let timer = Timer.publish(every: 300, on: .main, in: .common).autoconnect()

init(
alertsFetcher: AlertsFetcher,
globalFetcher: GlobalFetcher,
nearbyFetcher: NearbyFetcher,
railRouteShapeFetcher: RailRouteShapeFetcher,
locationDataManager: LocationDataManager = .init(distanceFilter: 1),
viewportProvider: ViewportProvider,
sheetHeight: Binding<CGFloat>
) {
self.alertsFetcher = alertsFetcher
self.globalFetcher = globalFetcher
self.nearbyFetcher = nearbyFetcher
self.railRouteShapeFetcher = railRouteShapeFetcher
Expand All @@ -58,12 +65,8 @@ struct HomeMapView: View {
.mapStyle(.light)
.onCameraChanged { change in handleCameraChange(change) }
.ornamentOptions(.init(scaleBar: .init(visibility: .hidden)))
.onLayerTapGesture(StopLayerGenerator.getStopLayerId(.stop)) { _, _ in
// Each stop feature has the stop ID as the identifier
// We can also set arbitrary JSON properties if we need to
// print(feature.feature.identifier)
true
}
.onLayerTapGesture(StopLayerGenerator.getStopLayerId(.stop), perform: handleStopLayerTap)
.onLayerTapGesture(StopLayerGenerator.getStopLayerId(.station), perform: handleStopLayerTap)
.additionalSafeAreaInsets(.bottom, sheetHeight)
.accessibilityIdentifier("transitMap")
.onAppear { handleAppear(location: proxy.location, map: proxy.map) }
Expand All @@ -74,6 +77,22 @@ struct HomeMapView: View {
Task { viewportProvider.follow(animation: .easeInOut(duration: 0)) }
}
}
.onChange(of: alertsFetcher.alerts) { nextAlerts in
currentStopAlerts = globalFetcher.getRealtimeAlertsByStop(
alerts: nextAlerts,
filterAtTime: now.toKotlinInstant()
)
}
.onReceive(timer) { input in
now = input
currentStopAlerts = globalFetcher.getRealtimeAlertsByStop(
alerts: alertsFetcher.alerts,
filterAtTime: now.toKotlinInstant()
)
}
.onChange(of: currentStopAlerts) { nextStopAlerts in
handleStopAlertChange(alertsByStop: nextStopAlerts)
}
.overlay(alignment: .topTrailing) {
if !viewportProvider.viewport.isFollowing, locationDataManager.currentLocation != nil {
RecenterButton { Task { viewportProvider.follow() } }
Expand Down Expand Up @@ -108,7 +127,7 @@ struct HomeMapView: View {

func handleCameraChange(_ change: CameraChanged) {
guard let layerManager else { return }
layerManager.updateStopLayers(change.cameraState.zoom)
layerManager.updateStopLayerZoom(change.cameraState.zoom)
cameraDebouncer.debounce {
viewportProvider.cameraState = change.cameraState
}
Expand All @@ -126,19 +145,38 @@ struct HomeMapView: View {
let layerManager = MapLayerManager(map: map)

let routeSourceGenerator = RouteSourceGenerator(routeData: routeResponse)
let stopSourceGenerator = StopSourceGenerator(
stops: stops,
routeSourceDetails: routeSourceGenerator.routeSourceDetails
layerManager.addSources(
routeSourceGenerator: routeSourceGenerator,
stopSourceGenerator: StopSourceGenerator(
stops: stops,
routeSourceDetails: routeSourceGenerator.routeSourceDetails,
alertsByStop: currentStopAlerts
)
)
layerManager.addSources(sources: routeSourceGenerator.routeSources + stopSourceGenerator.stopSources)

let routeLayerGenerator = RouteLayerGenerator(routeData: routeResponse)
let stopLayerGenerator = StopLayerGenerator(stopLayerTypes: MapLayerManager.stopLayerTypes)
layerManager.addLayers(layers: routeLayerGenerator.routeLayers + stopLayerGenerator.stopLayers)
layerManager.addLayers(
routeLayerGenerator: RouteLayerGenerator(routeData: routeResponse),
stopLayerGenerator: StopLayerGenerator(stopLayerTypes: MapLayerManager.stopLayerTypes)
)

self.layerManager = layerManager
}

func handleStopAlertChange(alertsByStop: [String: AlertAssociatedStop]) {
let updatedSources = StopSourceGenerator(
stops: globalFetcher.stops,
routeSourceDetails: layerManager?.routeSourceGenerator?.routeSourceDetails,
alertsByStop: alertsByStop
)
layerManager?.updateSourceData(stopSourceGenerator: updatedSources)
}

func handleStopLayerTap(feature _: QueriedFeature, _: MapContentGestureContext) -> Bool {
// Each stop feature has the stop ID as the identifier
// We can also set arbitrary JSON properties if we need to
true
}

func isNearbyNotFollowing() -> Bool {
nearbyFetcher.loadedLocation != nil
&& nearbyFetcher.loadedLocation != locationDataManager.currentLocation?.coordinate
Expand Down
Loading

0 comments on commit 7a6ed82

Please sign in to comment.