Skip to content

Commit

Permalink
feat(HomeMapView): Draw alerting segments as dashed lines (#123)
Browse files Browse the repository at this point in the history
* WIP

* feat(HomeMapView): Show alerting route segments as dashed lines

* refactor(RouteLayerGenerator): rename createRouteLayer / createRouteLayers
  • Loading branch information
KaylaBrady authored Apr 11, 2024
1 parent f8b4d33 commit 2b54ead
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 49 deletions.
27 changes: 21 additions & 6 deletions iosApp/iosApp/Pages/Map/HomeMapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,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)) }
Expand Down Expand Up @@ -159,7 +168,8 @@ struct HomeMapView: View {
) {
let layerManager = MapLayerManager(map: map)

let routeSourceGenerator = RouteSourceGenerator(routeData: routeResponse, stopsById: stops)
let routeSourceGenerator = RouteSourceGenerator(routeData: routeResponse, stopsById: stops,
alertsByStop: currentStopAlerts)
layerManager.addSources(
routeSourceGenerator: routeSourceGenerator,
stopSourceGenerator: StopSourceGenerator(
Expand All @@ -178,12 +188,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 {
Expand Down
50 changes: 35 additions & 15 deletions iosApp/iosApp/Pages/Map/RouteLayerGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,52 @@ class RouteLayerGenerator {
init(mapFriendlyRoutesResponse: MapFriendlyRouteResponse, routesById: [String: Route]) {
self.mapFriendlyRoutesResponse = mapFriendlyRoutesResponse
self.routesById = routesById
routeLayers = Self.createRouteLayers(routesWithShapes: mapFriendlyRoutesResponse.routesWithSegmentedShapes,
routesById: routesById)
routeLayers = Self.createAllRouteLayers(routesWithShapes: mapFriendlyRoutesResponse.routesWithSegmentedShapes,
routesById: routesById)
}

static func createRouteLayers(routesWithShapes: [MapFriendlyRouteResponse.RouteWithSegmentedShapes],
routesById: [String: Route]) -> [LineLayer] {
static func createAllRouteLayers(routesWithShapes: [MapFriendlyRouteResponse.RouteWithSegmentedShapes],
routesById: [String: Route]) -> [LineLayer] {
routesWithShapes
.filter { routesById[$0.routeId] != nil }
.sorted {
// Sort by reverse sort order so that lowest ordered routes are drawn first/lowest
routesById[$0.routeId]!.sortOrder >= routesById[$1.routeId]!.sortOrder
}
.map { createRouteLayer(route: routesById[$0.routeId]!) }
.flatMap { createRouteLayers(route: routesById[$0.routeId]!) }
}

static func createRouteLayer(route: Route) -> LineLayer {
var routeLayer = LineLayer(
id: Self.getRouteLayerId(route.id),
/**
Define the line layers for styling the route's line shapes.
Returns a list of 2 LineLayers - one with a styling to be applied to the entirety of all shapes in the route,
and a second that is applied only to the portions of the lines that are alerting.
*/
static func createRouteLayers(route: Route) -> [LineLayer] {
var alertingLayer = LineLayer(
id: Self.getRouteLayerId("\(route.id)-alerting"),
source: RouteSourceGenerator.getRouteSourceId(route.id)
)
routeLayer.lineWidth = .constant(4.0)
routeLayer.lineColor = .constant(StyleColor(UIColor(hex: route.color)))
routeLayer.lineBorderWidth = .constant(1.0)
routeLayer.lineBorderColor = .constant(StyleColor(.white))
routeLayer.lineJoin = .constant(.round)
routeLayer.lineCap = .constant(.round)
return routeLayer
alertingLayer.filter = Exp(.get) { RouteSourceGenerator.propIsAlertingKey }
alertingLayer.lineDasharray = .constant([2.0, 3.0])

alertingLayer.lineWidth = .constant(4.0)
alertingLayer.lineColor = .constant(StyleColor(UIColor.white))
alertingLayer.lineBorderWidth = .constant(1.0)
alertingLayer.lineBorderColor = .constant(StyleColor(.white))
alertingLayer.lineJoin = .constant(.round)
alertingLayer.lineCap = .constant(.round)

var nonAlertingLayer = LineLayer(
id: Self.getRouteLayerId("\(route.id)"),
source: RouteSourceGenerator.getRouteSourceId(route.id)
)

nonAlertingLayer.lineWidth = .constant(4.0)
nonAlertingLayer.lineColor = .constant(StyleColor(UIColor(hex: route.color)))
nonAlertingLayer.lineBorderWidth = .constant(1.0)
nonAlertingLayer.lineBorderColor = .constant(StyleColor(.white))
nonAlertingLayer.lineJoin = .constant(.round)
nonAlertingLayer.lineCap = .constant(.round)
return [nonAlertingLayer, alertingLayer]
}
}
64 changes: 42 additions & 22 deletions iosApp/iosApp/Pages/Map/RouteSourceGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ class RouteLineData {
let sourceRoutePatternId: String
let line: LineString
let stopIds: [String]
let isAlerting: Bool

init(id: String, sourceRoutePatternId: String, line: LineString, stopIds: [String]) {
init(id: String, sourceRoutePatternId: String, line: LineString, stopIds: [String], isAlerting: Bool) {
self.id = id
self.sourceRoutePatternId = sourceRoutePatternId
self.line = line
self.stopIds = stopIds
self.isAlerting = isAlerting
}
}

Expand All @@ -38,73 +40,91 @@ class RouteSourceData {

class RouteSourceGenerator {
let routeData: MapFriendlyRouteResponse

let routeSourceDetails: [RouteSourceData]
let routeSources: [GeoJSONSource]

static let routeSourceId = "route-source"
static func getRouteSourceId(_ routeId: String) -> String { "\(routeSourceId)-\(routeId)" }

init(routeData: MapFriendlyRouteResponse, stopsById: [String: Stop]) {
static let propIsAlertingKey = "isAlerting"

init(routeData: MapFriendlyRouteResponse, stopsById: [String: Stop], alertsByStop: [String: AlertAssociatedStop]) {
self.routeData = routeData
routeSourceDetails = Self.generateRouteSources(routeData: routeData, stopsById: stopsById)
routeSourceDetails = Self.generateRouteSources(routeData: routeData, stopsById: stopsById,
alertsByStop: alertsByStop)
routeSources = routeSourceDetails.map(\.source)
}

static func generateRouteSources(routeData: MapFriendlyRouteResponse,
stopsById: [String: Stop]) -> [RouteSourceData] {
stopsById: [String: Stop],
alertsByStop: [String: AlertAssociatedStop]) -> [RouteSourceData] {
routeData.routesWithSegmentedShapes
.map { Self.generateRouteSource(routeId: $0.routeId,
routeShapes: $0.segmentedShapes,
stopsById: stopsById) }
.map { generateRouteSource(routeId: $0.routeId,
routeShapes: $0.segmentedShapes,
stopsById: stopsById,
alertsByStop: alertsByStop) }
}

static func generateRouteSource(routeId: String, routeShapes: [SegmentedRouteShape],
stopsById: [String: Stop]) -> RouteSourceData {
let routeLines = Self.generateRouteLines(routeId: routeId, routeShapes: routeShapes, stopsById: stopsById)
let routeFeatures: [Feature] = routeLines.map { Feature(geometry: $0.line) }
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()
featureProps[Self.propIsAlertingKey] = JSONValue(Bool(lineData.isAlerting))
feature.properties = featureProps
return feature
}
var routeSource = GeoJSONSource(id: Self.getRouteSourceId(routeId))
routeSource.data = .featureCollection(FeatureCollection(features: routeFeatures))
return .init(routeId: routeId, lines: routeLines, source: routeSource)
}

static func generateRouteLines(routeId _: String, routeShapes: [SegmentedRouteShape],
stopsById: [String: Stop]) -> [RouteLineData] {
stopsById: [String: Stop],
alertsByStop: [String: AlertAssociatedStop]) -> [RouteLineData] {
routeShapes
.flatMap { routePatternShape in
self.routeShapeToLineData(routePatternShape: routePatternShape, stopsById: stopsById)
routeShapeToLineData(routePatternShape: routePatternShape, stopsById: stopsById,
alertsByStop: alertsByStop)
}
}

private static func routeShapeToLineData(routePatternShape: SegmentedRouteShape,
stopsById: [String: Stop]) -> [RouteLineData] {
stopsById: [String: Stop],
alertsByStop: [String: AlertAssociatedStop]) -> [RouteLineData] {
guard let polyline = routePatternShape.shape.polyline,
let coordinates = Polyline(encodedPolyline: polyline).coordinates
else {
return []
}

let fullLineString = LineString(coordinates)
return routePatternShape.routeSegments.compactMap { routeSegment in
routeSegmentToRouteLineData(routeSegment: routeSegment, fullLineString: fullLineString,
let alertAwareSegments = routePatternShape.routeSegments.flatMap { segment in
segment.splitAlertingSegments(alertsByStop: alertsByStop)
}
return alertAwareSegments.compactMap { segment in
routeSegmentToRouteLineData(segment: segment, fullLineString: fullLineString,
stopsById: stopsById)
}
}

private static func routeSegmentToRouteLineData(routeSegment: RouteSegment, fullLineString: LineString,
private static func routeSegmentToRouteLineData(segment: AlertAwareRouteSegment, fullLineString: LineString,
stopsById: [String: Stop]) -> RouteLineData? {
guard let firstStopId = routeSegment.stopIds.first,
guard let firstStopId = segment.stopIds.first,
let firstStop = stopsById[firstStopId],
let lastStopId = routeSegment.stopIds.last,
let lastStopId = segment.stopIds.last,
let lastStop = stopsById[lastStopId],
let lineSegment = fullLineString.sliced(from: firstStop.coordinate,
to: lastStop.coordinate)
else {
return nil
}
return RouteLineData(id: routeSegment.id,
sourceRoutePatternId: routeSegment.sourceRoutePatternId,
return RouteLineData(id: segment.id,
sourceRoutePatternId: segment.sourceRoutePatternId,
line: lineSegment,
stopIds: routeSegment.stopIds)
stopIds: segment.stopIds, isAlerting: segment.isAlerting)
}
}
23 changes: 22 additions & 1 deletion iosApp/iosAppTests/Pages/Map/MapTestDataHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
]),
Expand Down
10 changes: 9 additions & 1 deletion iosApp/iosAppTests/Pages/Map/RouteLayerGeneratorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading

0 comments on commit 2b54ead

Please sign in to comment.