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

feat(HomeMapView): Draw alerting segments as dashed lines #123

Merged
merged 3 commits into from
Apr 11, 2024
Merged
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
27 changes: 21 additions & 6 deletions iosApp/iosApp/Pages/Map/HomeMapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)) }
Expand Down Expand Up @@ -152,7 +161,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 @@ -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 {
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
Loading