From 61ac3f0b4bdaf0dfc1b57ffec929453868bec8ea Mon Sep 17 00:00:00 2001 From: KaylaBrady <31781298+KaylaBrady@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:28:46 -0400 Subject: [PATCH] feat(HomeMapView): Show alerting route segments as dashed lines --- iosApp/iosApp/Pages/Map/HomeMapView.swift | 27 ++++-- .../Pages/Map/RouteLayerGenerator.swift | 2 +- .../Pages/Map/RouteSourceGenerator.swift | 21 +++-- .../Pages/Map/MapTestDataHelper.swift | 23 ++++- .../Pages/Map/RouteLayerGeneratorTests.swift | 10 ++- .../Pages/Map/RouteSourceGeneratorTests.swift | 86 ++++++++++++++++++- .../Pages/Map/StopSourceGeneratorTests.swift | 9 +- 7 files changed, 158 insertions(+), 20 deletions(-) diff --git a/iosApp/iosApp/Pages/Map/HomeMapView.swift b/iosApp/iosApp/Pages/Map/HomeMapView.swift index 53da4edcd..022eeef87 100644 --- a/iosApp/iosApp/Pages/Map/HomeMapView.swift +++ b/iosApp/iosApp/Pages/Map/HomeMapView.swift @@ -63,15 +63,24 @@ struct HomeMapView: View { } .gestureOptions(.init(rotateEnabled: false, pitchEnabled: false)) .mapStyle(.light) - .onCameraChanged { change in handleCameraChange(change) } + .onCameraChanged { change in handleCameraChange(change) + } .ornamentOptions(.init(scaleBar: .init(visibility: .hidden))) .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) } - .onChange(of: globalFetcher.response) { _ in handleTryLayerInit(map: proxy.map) } - .onChange(of: railRouteShapeFetcher.response) { _ in handleTryLayerInit(map: proxy.map) } + .onChange(of: globalFetcher.response) { _ in + handleTryLayerInit(map: proxy.map) + currentStopAlerts = globalFetcher.getRealtimeAlertsByStop( + alerts: alertsFetcher.alerts, + filterAtTime: now.toKotlinInstant() + ) + } + .onChange(of: railRouteShapeFetcher.response) { _ in + handleTryLayerInit(map: proxy.map) + } .onChange(of: locationDataManager.authorizationStatus) { status in if status == .authorizedAlways || status == .authorizedWhenInUse { Task { viewportProvider.follow(animation: .easeInOut(duration: 0)) } @@ -152,7 +161,8 @@ struct HomeMapView: View { ) { let layerManager = MapLayerManager(map: map) - let routeSourceGenerator = RouteSourceGenerator(routeData: routeResponse, stopsById: stops, alertsByStop: currentStopAlerts) + let routeSourceGenerator = RouteSourceGenerator(routeData: routeResponse, stopsById: stops, + alertsByStop: currentStopAlerts) layerManager.addSources( routeSourceGenerator: routeSourceGenerator, stopSourceGenerator: StopSourceGenerator( @@ -171,12 +181,17 @@ struct HomeMapView: View { } func handleStopAlertChange(alertsByStop: [String: AlertAssociatedStop]) { - let updatedSources = StopSourceGenerator( + let updatedStopSources = StopSourceGenerator( stops: globalFetcher.stops, routeSourceDetails: layerManager?.routeSourceGenerator?.routeSourceDetails, alertsByStop: alertsByStop ) - layerManager?.updateSourceData(stopSourceGenerator: updatedSources) + layerManager?.updateSourceData(stopSourceGenerator: updatedStopSources) + if let railResponse = railRouteShapeFetcher.response { + let updatedRouteSources = RouteSourceGenerator(routeData: railResponse, stopsById: globalFetcher.stops, + alertsByStop: alertsByStop) + layerManager?.updateSourceData(routeSourceGenerator: updatedRouteSources) + } } func handleStopLayerTap(feature _: QueriedFeature, _: MapContentGestureContext) -> Bool { diff --git a/iosApp/iosApp/Pages/Map/RouteLayerGenerator.swift b/iosApp/iosApp/Pages/Map/RouteLayerGenerator.swift index 18fee7e51..bbfc7141d 100644 --- a/iosApp/iosApp/Pages/Map/RouteLayerGenerator.swift +++ b/iosApp/iosApp/Pages/Map/RouteLayerGenerator.swift @@ -42,7 +42,7 @@ class RouteLayerGenerator { source: RouteSourceGenerator.getRouteSourceId(route.id) ) alertingLayer.filter = Exp(.get) { RouteSourceGenerator.propIsAlertingKey } - alertingLayer.lineDasharray = .constant([2.0, 2.0]) + alertingLayer.lineDasharray = .constant([2.0, 3.0]) alertingLayer.lineWidth = .constant(4.0) alertingLayer.lineColor = .constant(StyleColor(UIColor.white)) diff --git a/iosApp/iosApp/Pages/Map/RouteSourceGenerator.swift b/iosApp/iosApp/Pages/Map/RouteSourceGenerator.swift index 948984b42..f8527d489 100644 --- a/iosApp/iosApp/Pages/Map/RouteSourceGenerator.swift +++ b/iosApp/iosApp/Pages/Map/RouteSourceGenerator.swift @@ -50,12 +50,14 @@ class RouteSourceGenerator { init(routeData: MapFriendlyRouteResponse, stopsById: [String: Stop], alertsByStop: [String: AlertAssociatedStop]) { self.routeData = routeData - routeSourceDetails = Self.generateRouteSources(routeData: routeData, stopsById: stopsById, alertsByStop: alertsByStop) + routeSourceDetails = Self.generateRouteSources(routeData: routeData, stopsById: stopsById, + alertsByStop: alertsByStop) routeSources = routeSourceDetails.map(\.source) } static func generateRouteSources(routeData: MapFriendlyRouteResponse, - stopsById: [String: Stop], alertsByStop: [String: AlertAssociatedStop]) -> [RouteSourceData] { + stopsById: [String: Stop], + alertsByStop: [String: AlertAssociatedStop]) -> [RouteSourceData] { routeData.routesWithSegmentedShapes .map { generateRouteSource(routeId: $0.routeId, routeShapes: $0.segmentedShapes, @@ -64,8 +66,10 @@ class RouteSourceGenerator { } static func generateRouteSource(routeId: String, routeShapes: [SegmentedRouteShape], - stopsById: [String: Stop], alertsByStop: [String: AlertAssociatedStop]) -> RouteSourceData { - let routeLines = generateRouteLines(routeId: routeId, routeShapes: routeShapes, stopsById: stopsById, alertsByStop: alertsByStop) + stopsById: [String: Stop], + alertsByStop: [String: AlertAssociatedStop]) -> RouteSourceData { + let routeLines = generateRouteLines(routeId: routeId, routeShapes: routeShapes, stopsById: stopsById, + alertsByStop: alertsByStop) let routeFeatures: [Feature] = routeLines.map { lineData in var feature = Feature(geometry: lineData.line) var featureProps = JSONObject() @@ -79,15 +83,18 @@ class RouteSourceGenerator { } static func generateRouteLines(routeId _: String, routeShapes: [SegmentedRouteShape], - stopsById: [String: Stop], alertsByStop: [String: AlertAssociatedStop]) -> [RouteLineData] { + stopsById: [String: Stop], + alertsByStop: [String: AlertAssociatedStop]) -> [RouteLineData] { routeShapes .flatMap { routePatternShape in - routeShapeToLineData(routePatternShape: routePatternShape, stopsById: stopsById, alertsByStop: alertsByStop) + routeShapeToLineData(routePatternShape: routePatternShape, stopsById: stopsById, + alertsByStop: alertsByStop) } } private static func routeShapeToLineData(routePatternShape: SegmentedRouteShape, - stopsById: [String: Stop], alertsByStop: [String: AlertAssociatedStop]) -> [RouteLineData] { + stopsById: [String: Stop], + alertsByStop: [String: AlertAssociatedStop]) -> [RouteLineData] { guard let polyline = routePatternShape.shape.polyline, let coordinates = Polyline(encodedPolyline: polyline).coordinates else { diff --git a/iosApp/iosAppTests/Pages/Map/MapTestDataHelper.swift b/iosApp/iosAppTests/Pages/Map/MapTestDataHelper.swift index 0e14790a5..7394956b1 100644 --- a/iosApp/iosAppTests/Pages/Map/MapTestDataHelper.swift +++ b/iosApp/iosAppTests/Pages/Map/MapTestDataHelper.swift @@ -37,6 +37,27 @@ enum MapTestDataHelper { stop.locationType = LocationType.station } + static let stopPorter = objects.stop { stop in + stop.id = "place-porter" + stop.latitude = 42.3884 + stop.longitude = -71.119149 + stop.locationType = LocationType.station + } + + static let stopHarvard = objects.stop { stop in + stop.id = "place-harsq" + stop.latitude = 42.373362 + stop.longitude = -71.118956 + stop.locationType = LocationType.station + } + + static let stopCentral = objects.stop { stop in + stop.id = "place-cntsq" + stop.latitude = 42.365486 + stop.longitude = -71.103802 + stop.locationType = LocationType.station + } + static let stopAssembly = objects.stop { stop in stop.id = "place-astao" stop.latitude = 42.392811 @@ -126,7 +147,7 @@ enum MapTestDataHelper { routeSegments: [RouteSegment(id: "segment2", sourceRoutePatternId: patternRed30.id, sourceRouteId: patternRed30.routeId, - stopIds: [stopAlewife.id, stopDavis.id], + stopIds: [stopPorter.id, stopHarvard.id, stopCentral.id], otherPatternsByStopId: [:])], shape: shapeRedC1), ]), diff --git a/iosApp/iosAppTests/Pages/Map/RouteLayerGeneratorTests.swift b/iosApp/iosAppTests/Pages/Map/RouteLayerGeneratorTests.swift index d69299df8..e89d6fa6d 100644 --- a/iosApp/iosAppTests/Pages/Map/RouteLayerGeneratorTests.swift +++ b/iosApp/iosAppTests/Pages/Map/RouteLayerGeneratorTests.swift @@ -22,10 +22,18 @@ final class RouteLayerGeneratorTests: XCTestCase { MapTestDataHelper.routeOrange.id: MapTestDataHelper.routeOrange]) let routeLayers = routeLayerGenerator.routeLayers - XCTAssertEqual(routeLayers.count, 2) + // 2 layers per route - alerting & non-alerting + XCTAssertEqual(routeLayers.count, 4) let redRouteLayer = routeLayers.first { $0.id == RouteLayerGenerator.getRouteLayerId(MapTestDataHelper.routeRed.id) } XCTAssertNotNil(redRouteLayer) guard let redRouteLayer else { return } XCTAssertEqual(redRouteLayer.lineColor, .constant(StyleColor(.init(hex: MapTestDataHelper.routeRed.color)))) + + let alertingRedLayer = routeLayers.first { $0.id == RouteLayerGenerator + .getRouteLayerId("\(MapTestDataHelper.routeRed.id)-alerting") + } + + XCTAssertNotNil(alertingRedLayer) + XCTAssertNotNil(alertingRedLayer!.lineDasharray) } } diff --git a/iosApp/iosAppTests/Pages/Map/RouteSourceGeneratorTests.swift b/iosApp/iosAppTests/Pages/Map/RouteSourceGeneratorTests.swift index 791f9bd9d..b5f986f3d 100644 --- a/iosApp/iosAppTests/Pages/Map/RouteSourceGeneratorTests.swift +++ b/iosApp/iosAppTests/Pages/Map/RouteSourceGeneratorTests.swift @@ -21,8 +21,12 @@ final class RouteSourceGeneratorTests: XCTestCase { let routeSourceGenerator = RouteSourceGenerator(routeData: MapTestDataHelper.routeResponse, stopsById: [MapTestDataHelper.stopAlewife.id: MapTestDataHelper.stopAlewife, MapTestDataHelper.stopDavis.id: MapTestDataHelper.stopDavis, + MapTestDataHelper.stopPorter.id: MapTestDataHelper.stopPorter, + MapTestDataHelper.stopHarvard.id: MapTestDataHelper.stopHarvard, + MapTestDataHelper.stopCentral.id: MapTestDataHelper.stopCentral, MapTestDataHelper.stopAssembly.id: MapTestDataHelper.stopAssembly, - MapTestDataHelper.stopSullivan.id: MapTestDataHelper.stopSullivan]) + MapTestDataHelper.stopSullivan.id: MapTestDataHelper.stopSullivan], + alertsByStop: [:]) XCTAssertEqual(routeSourceGenerator.routeSources.count, 2) @@ -52,4 +56,84 @@ final class RouteSourceGeneratorTests: XCTestCase { XCTFail("Orange route source had no features") } } + + func testAlertingSourcesCreated() { + let now = Date.now + + let objects = ObjectCollectionBuilder() + + let redAlert = objects.alert { alert in + alert.id = "a1" + alert.effect = .shuttle + alert.activePeriod(start: now.addingTimeInterval(-1).toKotlinInstant(), end: nil) + alert.informedEntity(activities: [.board], directionId: nil, facility: nil, + route: MapTestDataHelper.routeRed.id, routeType: .heavyRail, + stop: MapTestDataHelper.stopPorter.id, trip: nil) + alert.informedEntity(activities: [.board], directionId: nil, facility: nil, + route: MapTestDataHelper.routeRed.id, routeType: .heavyRail, + stop: MapTestDataHelper.stopHarvard.id, trip: nil) + } + let alertsByStop = [ + MapTestDataHelper.stopPorter.id: AlertAssociatedStop( + stop: MapTestDataHelper.stopPorter, + relevantAlerts: [redAlert], + routePatterns: [MapTestDataHelper.patternRed30], + childStops: [:], + childAlerts: [:] + ), + MapTestDataHelper.stopHarvard.id: AlertAssociatedStop( + stop: MapTestDataHelper.stopHarvard, + relevantAlerts: [redAlert], + routePatterns: [MapTestDataHelper.patternRed30], + childStops: [:], + childAlerts: [:] + ), + ] + + let routeSourceGenerator = RouteSourceGenerator(routeData: MapTestDataHelper.routeResponse, + stopsById: [MapTestDataHelper.stopAlewife.id: MapTestDataHelper.stopAlewife, + MapTestDataHelper.stopDavis.id: MapTestDataHelper.stopDavis, + MapTestDataHelper.stopPorter.id: MapTestDataHelper.stopPorter, + MapTestDataHelper.stopHarvard.id: MapTestDataHelper.stopHarvard, + MapTestDataHelper.stopCentral.id: MapTestDataHelper.stopCentral], + alertsByStop: alertsByStop) + + // RL & OL + XCTAssertEqual(routeSourceGenerator.routeSources.count, 2) + + let redSource = routeSourceGenerator.routeSources.first + XCTAssertNotNil(redSource) + if case let .featureCollection(collection) = redSource!.data.unsafelyUnwrapped { + // Alewife - Davis (normal), Harvard - Porter (alerting), Porter - Central (normal) + XCTAssertEqual(collection.features.count, 3) + XCTAssertEqual( + collection.features[0].properties![RouteSourceGenerator.propIsAlertingKey]!, JSONValue(Bool(false)) + ) + XCTAssertEqual( + collection.features[0].geometry, + .lineString(LineString(Polyline(encodedPolyline: MapTestDataHelper.shapeRedC1.polyline!).coordinates!) + .sliced(from: MapTestDataHelper.stopAlewife.coordinate, to: MapTestDataHelper.stopDavis.coordinate)!) + ) + + XCTAssertEqual( + collection.features[1].properties![RouteSourceGenerator.propIsAlertingKey]!, JSONValue(Bool(true)) + ) + XCTAssertEqual( + collection.features[1].geometry, + .lineString(LineString(Polyline(encodedPolyline: MapTestDataHelper.shapeRedC1.polyline!).coordinates!) + .sliced(from: MapTestDataHelper.stopPorter.coordinate, to: MapTestDataHelper.stopHarvard.coordinate)!) + ) + XCTAssertEqual( + collection.features[2].properties![RouteSourceGenerator.propIsAlertingKey]!, JSONValue(Bool(false)) + ) + XCTAssertEqual( + collection.features[2].geometry, + .lineString(LineString(Polyline(encodedPolyline: MapTestDataHelper.shapeRedC1.polyline!).coordinates!) + .sliced(from: MapTestDataHelper.stopHarvard.coordinate, to: MapTestDataHelper.stopCentral.coordinate)!) + ) + + } else { + XCTFail("Red route source had no features") + } + } } diff --git a/iosApp/iosAppTests/Pages/Map/StopSourceGeneratorTests.swift b/iosApp/iosAppTests/Pages/Map/StopSourceGeneratorTests.swift index d80533556..180c1ed95 100644 --- a/iosApp/iosAppTests/Pages/Map/StopSourceGeneratorTests.swift +++ b/iosApp/iosAppTests/Pages/Map/StopSourceGeneratorTests.swift @@ -84,7 +84,8 @@ final class StopSourceGeneratorTests: XCTestCase { XCTAssertNotNil(stopSource) if case let .featureCollection(collection) = stopSource!.data.unsafelyUnwrapped { XCTAssertEqual(collection.features.count, 2) - if case let .string(serviceStatus) = collection.features[0].properties![StopSourceGenerator.propServiceStatusKey] { + if case let .string(serviceStatus) = collection.features[0] + .properties![StopSourceGenerator.propServiceStatusKey] { XCTAssertEqual(serviceStatus, String(describing: StopServiceStatus.normal)) } else { XCTFail("Source status property was not set correctly") @@ -105,8 +106,10 @@ final class StopSourceGeneratorTests: XCTestCase { MapTestDataHelper.stopDavis.id: MapTestDataHelper.stopDavis, ] - let routeSourceGenerator = RouteSourceGenerator(routeData: MapTestDataHelper.routeResponse, stopsById: stops) - let stopSourceGenerator = StopSourceGenerator(stops: stops, routeSourceDetails: routeSourceGenerator.routeSourceDetails) + let routeSourceGenerator = RouteSourceGenerator(routeData: MapTestDataHelper.routeResponse, stopsById: stops, + alertsByStop: [:]) + let stopSourceGenerator = StopSourceGenerator(stops: stops, + routeSourceDetails: routeSourceGenerator.routeSourceDetails) let sources = stopSourceGenerator.stopSources let snappedStopCoordinates = CLLocationCoordinate2D(latitude: 42.3961623851223, longitude: -71.14129664101432)