diff --git a/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/map/HomeMapViewTests.kt b/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/map/HomeMapViewTests.kt index e802ace151..c546626ee5 100644 --- a/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/map/HomeMapViewTests.kt +++ b/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/map/HomeMapViewTests.kt @@ -25,6 +25,7 @@ import com.mbta.tid.mbta_app.repositories.MockErrorBannerStateRepository import com.mbta.tid.mbta_app.repositories.MockGlobalRepository import com.mbta.tid.mbta_app.repositories.MockRailRouteShapeRepository import com.mbta.tid.mbta_app.repositories.MockSentryRepository +import com.mbta.tid.mbta_app.repositories.MockSettingsRepository import com.mbta.tid.mbta_app.repositories.MockStopRepository import com.mbta.tid.mbta_app.repositories.MockTripRepository import com.mbta.tid.mbta_app.routes.SheetRoutes @@ -63,6 +64,7 @@ class HomeMapViewTests { MockGlobalRepository(), MockRailRouteShapeRepository(), MockSentryRepository(), + MockSettingsRepository(), MockStopRepository(), MockTripRepository(), Clock.System, @@ -105,6 +107,7 @@ class HomeMapViewTests { MockGlobalRepository(), MockRailRouteShapeRepository(), MockSentryRepository(), + MockSettingsRepository(), MockStopRepository(), MockTripRepository(), Clock.System, @@ -153,6 +156,7 @@ class HomeMapViewTests { MockGlobalRepository(), MockRailRouteShapeRepository(), MockSentryRepository(), + MockSettingsRepository(), MockStopRepository(), MockTripRepository(), Clock.System, @@ -201,6 +205,7 @@ class HomeMapViewTests { MockGlobalRepository(), MockRailRouteShapeRepository(), MockSentryRepository(), + MockSettingsRepository(), MockStopRepository(), MockTripRepository(), Clock.System, @@ -241,6 +246,7 @@ class HomeMapViewTests { MockGlobalRepository(), MockRailRouteShapeRepository(), MockSentryRepository(), + MockSettingsRepository(), MockStopRepository(), MockTripRepository(), Clock.System, @@ -282,6 +288,7 @@ class HomeMapViewTests { MockGlobalRepository(), MockRailRouteShapeRepository(), MockSentryRepository(), + MockSettingsRepository(), MockStopRepository(), MockTripRepository(), Clock.System, @@ -323,6 +330,7 @@ class HomeMapViewTests { MockGlobalRepository(), MockRailRouteShapeRepository(), MockSentryRepository(), + MockSettingsRepository(), MockStopRepository(), MockTripRepository(), Clock.System, @@ -417,6 +425,7 @@ class HomeMapViewTests { MockGlobalRepository(), MockRailRouteShapeRepository(), MockSentryRepository(), + MockSettingsRepository(), MockStopRepository(), MockTripRepository(), Clock.System, @@ -462,6 +471,7 @@ class HomeMapViewTests { MockGlobalRepository(), MockRailRouteShapeRepository(), MockSentryRepository(), + MockSettingsRepository(), MockStopRepository(), MockTripRepository(), Clock.System, @@ -504,6 +514,7 @@ class HomeMapViewTests { MockGlobalRepository(), MockRailRouteShapeRepository(), MockSentryRepository(), + MockSettingsRepository(), MockStopRepository(), MockTripRepository(), Clock.System, diff --git a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/map/HomeMapView.kt b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/map/HomeMapView.kt index 4c5a69a2fe..09a83d4128 100644 --- a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/map/HomeMapView.kt +++ b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/map/HomeMapView.kt @@ -39,6 +39,7 @@ import com.mapbox.geojson.Point import com.mapbox.maps.CameraBoundsOptions import com.mapbox.maps.ViewAnnotationAnchor import com.mapbox.maps.ViewAnnotationOptions +import com.mapbox.maps.debugoptions.MapViewDebugOptions import com.mapbox.maps.extension.compose.DisposableMapEffect import com.mapbox.maps.extension.compose.MapEffect import com.mapbox.maps.extension.compose.MapboxMap @@ -193,6 +194,7 @@ fun HomeMapView( val locationProvider = remember { PassthroughLocationProvider() } MapEffect { map -> + map.debugOptions = setOf(MapViewDebugOptions.CAMERA) map.mapboxMap.addOnMapClickListener { point -> map.getStopIdAt(point) { handleStopNavigation(it) diff --git a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/map/MapLayerManager.kt b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/map/MapLayerManager.kt index 46d6916a32..f17ec59820 100644 --- a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/map/MapLayerManager.kt +++ b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/map/MapLayerManager.kt @@ -24,6 +24,7 @@ import com.mbta.tid.mbta_app.map.StopIcons import com.mbta.tid.mbta_app.map.StopLayerGenerator import com.mbta.tid.mbta_app.model.response.GlobalResponse import com.mbta.tid.mbta_app.model.response.MapFriendlyRouteResponse +import com.mbta.tid.mbta_app.repositories.Settings import com.mbta.tid.mbta_app.utils.IMapLayerManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.sync.Mutex @@ -63,12 +64,14 @@ class MapLayerManager(val map: MapboxMap, val context: Context) : IMapLayerManag state: StopLayerGenerator.State, globalResponse: GlobalResponse, colorPalette: ColorPalette, + settings: Map, ) { addLayers( mapFriendlyRouteResponse.routesWithSegmentedShapes, state, globalResponse, colorPalette, + settings, ) } @@ -77,13 +80,13 @@ class MapLayerManager(val map: MapboxMap, val context: Context) : IMapLayerManag state: StopLayerGenerator.State, globalResponse: GlobalResponse, colorPalette: ColorPalette, + settings: Map, ) { val routeLayers = - RouteLayerGenerator.createAllRouteLayers(routes, globalResponse, colorPalette).map { - it.toMapbox() - } + RouteLayerGenerator.createAllRouteLayers(routes, globalResponse, colorPalette, settings) + .map { it.toMapbox() } val stopLayers = - StopLayerGenerator.createStopLayers(colorPalette, state).map { it.toMapbox() } + StopLayerGenerator.createStopLayers(colorPalette, state, settings).map { it.toMapbox() } setLayers(routeLayers, stopLayers) } diff --git a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/more/MoreSectionView.kt b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/more/MoreSectionView.kt index f7cc6088c7..3d143099f0 100644 --- a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/more/MoreSectionView.kt +++ b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/more/MoreSectionView.kt @@ -98,7 +98,7 @@ fun MoreSectionView( MoreLink( item.label.value, item.callback, - item.note, + item.note?.value, isKey = section.id == MoreSection.Category.Feedback, ) is MoreItem.Phone -> diff --git a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/util/SharedStringLocalization.kt b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/util/SharedStringLocalization.kt index 1bf038eabd..c8bef9eb56 100644 --- a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/util/SharedStringLocalization.kt +++ b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/util/SharedStringLocalization.kt @@ -26,6 +26,10 @@ val SharedString.value: String SharedString.RouteSearch -> stringResource(R.string.feature_flag_route_search) SharedString.SendAppFeedback -> stringResource(R.string.feedback_link_form) SharedString.SettingsSection -> stringResource(R.string.more_section_settings) + SharedString.ShiftingDisabled -> "Shifting: Disabled" + SharedString.ShiftingIncludeStops -> "Shifting: Include Stops" + SharedString.ShiftingScaleWithZoom -> "Shifting: Scale With Zoom" + SharedString.ShiftingUseTranslate -> "Shifting: Use Translate for Shapes" SharedString.SoftwareLicenses -> stringResource(R.string.software_licenses) SharedString.StationAccessibilityInfo -> stringResource(R.string.setting_station_accessibility) diff --git a/iosApp/iosApp/Pages/Map/MapLayerManager.swift b/iosApp/iosApp/Pages/Map/MapLayerManager.swift index 57faeb85e1..507952dac5 100644 --- a/iosApp/iosApp/Pages/Map/MapLayerManager.swift +++ b/iosApp/iosApp/Pages/Map/MapLayerManager.swift @@ -18,7 +18,8 @@ protocol IMapLayerManager { routes: [MapFriendlyRouteResponse.RouteWithSegmentedShapes], state: StopLayerGenerator.State, globalResponse: GlobalResponse, - colorScheme: ColorScheme + colorScheme: ColorScheme, + settings: [Settings: KotlinBoolean], ) func resetPuckPosition() func updateSourceData(routeData: [RouteSourceData]) @@ -30,13 +31,15 @@ extension iosApp.IMapLayerManager { mapFriendlyRouteResponse: MapFriendlyRouteResponse, state: StopLayerGenerator.State, globalResponse: GlobalResponse, - colorScheme: ColorScheme + colorScheme: ColorScheme, + settings: [Settings: KotlinBoolean], ) { addLayers( routes: mapFriendlyRouteResponse.routesWithSegmentedShapes, state: state, globalResponse: globalResponse, - colorScheme: colorScheme + colorScheme: colorScheme, + settings: settings, ) } } @@ -130,7 +133,8 @@ class MapLayerManager: iosApp.IMapLayerManager { routes: [MapFriendlyRouteResponse.RouteWithSegmentedShapes], state: StopLayerGenerator.State, globalResponse: GlobalResponse, - colorScheme: ColorScheme + colorScheme: ColorScheme, + settings: [Settings: KotlinBoolean], ) { Task { let colorPalette = getColorPalette(colorScheme: colorScheme) @@ -138,12 +142,14 @@ class MapLayerManager: iosApp.IMapLayerManager { let routeLayers = try await RouteLayerGenerator.shared.createAllRouteLayers( routesWithShapes: routes, globalResponse: globalResponse, - colorPalette: colorPalette + colorPalette: colorPalette, + settings: settings, ) .map { $0.toMapbox() } let stopLayers = try await StopLayerGenerator.shared.createStopLayers( colorPalette: colorPalette, - state: state + state: state, + settings: settings, ) .map { $0.toMapbox() } @@ -245,13 +251,15 @@ extension MapLayerManager: Shared.IMapLayerManager { mapFriendlyRouteResponse: MapFriendlyRouteResponse, state: StopLayerGenerator.State, globalResponse: GlobalResponse, - colorPalette: ColorPalette + colorPalette: ColorPalette, + settings: [Settings: KotlinBoolean], ) async throws { addLayers( mapFriendlyRouteResponse: mapFriendlyRouteResponse, state: state, globalResponse: globalResponse, - colorScheme: colorPalette.colorScheme + colorScheme: colorPalette.colorScheme, + settings: settings, ) } @@ -260,13 +268,15 @@ extension MapLayerManager: Shared.IMapLayerManager { routes: [MapFriendlyRouteResponse.RouteWithSegmentedShapes], state: StopLayerGenerator.State, globalResponse: GlobalResponse, - colorPalette: ColorPalette + colorPalette: ColorPalette, + settings: [Settings: KotlinBoolean], ) async throws { addLayers( routes: routes, state: state, globalResponse: globalResponse, - colorScheme: colorPalette.colorScheme + colorScheme: colorPalette.colorScheme, + settings: settings, ) } diff --git a/iosApp/iosApp/Utils/Extensions/SharedStringLocalization.swift b/iosApp/iosApp/Utils/Extensions/SharedStringLocalization.swift index d506fdbc43..51bac9474c 100644 --- a/iosApp/iosApp/Utils/Extensions/SharedStringLocalization.swift +++ b/iosApp/iosApp/Utils/Extensions/SharedStringLocalization.swift @@ -83,6 +83,10 @@ extension SharedString { "Settings", comment: "More page section header, includes settings that the user can configure" ) + case .shiftingDisabled: "Shifting: Disabled" + case .shiftingIncludeStops: "Shifting: Include Stops" + case .shiftingScaleWithZoom: "Shifting: Scale With Zoom" + case .shiftingUseTranslate: "Shifting: Use Translate for Shapes" case .softwareLicenses: NSLocalizedString( "Software Licenses", diff --git a/iosApp/iosAppTests/Mocks/MockLayerManager.swift b/iosApp/iosAppTests/Mocks/MockLayerManager.swift index f2deca9224..638bc60cf6 100644 --- a/iosApp/iosAppTests/Mocks/MockLayerManager.swift +++ b/iosApp/iosAppTests/Mocks/MockLayerManager.swift @@ -40,7 +40,8 @@ class MockLayerManager: iosApp.IMapLayerManager { routes _: [MapFriendlyRouteResponse.RouteWithSegmentedShapes], state _: StopLayerGenerator.State, globalResponse _: GlobalResponse, - colorScheme: ColorScheme + colorScheme: ColorScheme, + settings _: [Settings: KotlinBoolean], ) { currentScheme = colorScheme addLayersCallback() diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/MapExp.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/MapExp.kt index f9dc07bfa6..61f57939d2 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/MapExp.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/MapExp.kt @@ -5,6 +5,8 @@ import com.mbta.tid.mbta_app.map.style.Exp import com.mbta.tid.mbta_app.map.style.Interpolation import com.mbta.tid.mbta_app.map.style.LetVariable import com.mbta.tid.mbta_app.model.MapStopRoute +import com.mbta.tid.mbta_app.model.Route +import com.mbta.tid.mbta_app.repositories.Settings internal object MapExp { // Get the array of MapStopRoute string values from a stop feature @@ -26,7 +28,7 @@ internal object MapExp { singleRouteTypeExp, Exp.eq( Exp(1), - Exp.length(Exp.get(topRouteExp, Exp.get(StopFeaturesBuilder.propRouteIdsKey))), + Exp.length(Exp.get(topRouteExp, Exp.get(StopFeaturesBuilder.propRouteIdsByTypeKey))), ), ) @@ -81,28 +83,60 @@ internal object MapExp { // MapBox requires the value at the base of every case in this expression to be a literal length // 2 array of Doubles, since that's the type that iconOffset expects. This means that we can't // use expressions for each Double, which is why this function is so convoluted. - fun offsetAlertExp(closeZoom: Boolean, index: Int): Exp> { + fun offsetAlertExp( + closeZoom: Boolean, + index: Int, + settings: Map, + ): Exp> { + val shiftScale = + if (closeZoom || settings[Settings.ShiftingScaleWithZoom] != true) 1.0 + else 0.5 // TODO figure out why 1/18 doesn’t work val doubleRouteHeight = if (closeZoom) 13 else 8 val tripleRouteHeight = if (closeZoom) 26 else 16 return Exp.step( Exp.length(routesExp), // At stops only serving one type of route, the height doesn't need to be offset at all - offsetAlertPairExp(closeZoom = closeZoom, height = 0), + offsetAlertPairExp(closeZoom = closeZoom, height = 0, shiftScale, settings = settings), // At stops serving two different types of route, position the first upwards and the // second downwards. The third value is unused, so is set to 0. Exp(2) to listOf( - offsetAlertPairExp(closeZoom = closeZoom, height = -doubleRouteHeight), - offsetAlertPairExp(closeZoom = closeZoom, height = doubleRouteHeight), - xyExp(0, 0), + offsetAlertPairExp( + closeZoom = closeZoom, + height = -doubleRouteHeight, + shiftScale, + settings = settings, + ), + offsetAlertPairExp( + closeZoom = closeZoom, + height = doubleRouteHeight, + shiftScale, + settings = settings, + ), + xyExp(0, 0, shiftScale, settings), )[index], // At stops serving 3 routes, the first and third are moved up and down, and the center // one remains at the same height. Exp(3) to listOf( - offsetAlertPairExp(closeZoom = closeZoom, height = -tripleRouteHeight), - offsetAlertPairExp(closeZoom = closeZoom, height = 0), - offsetAlertPairExp(closeZoom = closeZoom, height = tripleRouteHeight), + offsetAlertPairExp( + closeZoom = closeZoom, + height = -tripleRouteHeight, + shiftScale, + settings = settings, + ), + offsetAlertPairExp( + closeZoom = closeZoom, + height = 0, + shiftScale, + settings = settings, + ), + offsetAlertPairExp( + closeZoom = closeZoom, + height = tripleRouteHeight, + shiftScale, + settings = settings, + ), )[index], ) } @@ -110,7 +144,12 @@ internal object MapExp { // The provided height is determined by the position in the route type array, this function // determines the width to offset the alert icon depending on the width of the type of icon that // the stop is being displayed with. - fun offsetAlertPairExp(closeZoom: Boolean, height: Int): Exp> { + fun offsetAlertPairExp( + closeZoom: Boolean, + height: Int, + shiftScale: Double, + settings: Map, + ): Exp> { val pillWidth = 26 val railStopWidth = if (closeZoom) pillWidth else 12 val busStopWidth = if (closeZoom) 18 else 12 @@ -128,42 +167,64 @@ internal object MapExp { branchedRouteExp to Exp.case( Exp.get(StopFeaturesBuilder.propIsTerminalKey) to - xyExp(branchTerminalWidth, height), - xyExp(branchStopWidth, height), + xyExp(branchTerminalWidth, height, shiftScale, settings), + xyExp(branchStopWidth, height, shiftScale, settings), ), // Ferry terminals have a special larger icon at the wide zoom level Exp.all( Exp.eq(topRouteExp, Exp(MapStopRoute.FERRY.name)), Exp.get(StopFeaturesBuilder.propIsTerminalKey), - ) to xyExp(terminalFerryWidth, height), + ) to xyExp(terminalFerryWidth, height, shiftScale, settings), // Buses have an extra small dot at wide zoom, and at close zoom, the height is // slightly repositioned to center the alert on the tombstone, ignoring the small // pole rectangle at the bottom Exp.eq(topRouteExp, Exp(MapStopRoute.BUS.name)) to - xyExp(busStopWidth, height - (if (closeZoom) 2 else 0)), + xyExp(busStopWidth, height - (if (closeZoom) 2 else 0), shiftScale, settings), // Rail terminals at wide zoom have a special pill icon rather than the usual dot Exp.get(StopFeaturesBuilder.propIsTerminalKey) to - xyExp(terminalRailStopWidth, height), + xyExp(terminalRailStopWidth, height, shiftScale, settings), // Regular rail stops, have a basic pill at close zoom and a dot at wide - fallback = xyExp(railStopWidth, height), + fallback = xyExp(railStopWidth, height, shiftScale, settings), ), // If the stop is a transfer stop, all routes are displayed with a basic pill and dot - Exp(2) to xyExp(railStopWidth, height), + Exp(2) to xyExp(railStopWidth, height, shiftScale, settings), ) } - fun offsetTransferExp(closeZoom: Boolean, index: Int): Exp> { + fun offsetTransferExp( + closeZoom: Boolean, + index: Int, + settings: Map, + ): Exp> { + val shiftScale = + if (closeZoom || settings[Settings.ShiftingScaleWithZoom] != true) 1.0 + else 0.5 // TODO figure out why 1/40 doesn’t work val doubleRouteOffset = if (closeZoom) 13 else 8 val tripleRouteOffset = if (closeZoom) 26 else 16 return Exp.step( Exp.length(routesExp), - xyExp(0, 0), - Exp(2) to xyExp(0, listOf(-doubleRouteOffset, doubleRouteOffset, 0)[index]), - Exp(3) to xyExp(0, listOf(-tripleRouteOffset, 0, tripleRouteOffset)[index]), + xyExp(0, 0, shiftScale, settings), + Exp(2) to + xyExp( + 0, + listOf(-doubleRouteOffset, doubleRouteOffset, 0)[index], + shiftScale, + settings, + ), + Exp(3) to + xyExp( + 0, + listOf(-tripleRouteOffset, 0, tripleRouteOffset)[index], + shiftScale, + settings, + ), ) } - fun offsetPinExp(closeZoom: Boolean): Exp> { + fun offsetPinExp(closeZoom: Boolean, settings: Map): Exp> { + val shiftScale = + if (closeZoom || settings[Settings.ShiftingScaleWithZoom] != true) 1.0 + else 0.5 // TODO figure out why 1/40 doesn’t work val singleRouteOffset = if (closeZoom) 38 else 33 val doubleRouteOffset = if (closeZoom) 52 else 42 val tripleRouteOffset = if (closeZoom) 65 else 50 @@ -174,48 +235,55 @@ internal object MapExp { Exp.all( Exp.eq(topRouteExp, Exp(MapStopRoute.FERRY.name)), Exp.get(StopFeaturesBuilder.propIsTerminalKey), - ) to xyExp(0, -singleRouteOffset - (if (closeZoom) 0 else 2)), + ) to xyExp(0, -singleRouteOffset - (if (closeZoom) 0 else 2), shiftScale, settings), // Buses have an extra small dot at wide zoom, and at close zoom, the height is // slightly repositioned to center the alert on the tombstone, ignoring the small // pole rectangle at the bottom Exp.eq(topRouteExp, Exp(MapStopRoute.BUS.name)) to - xyExp(0, -singleRouteOffset - (if (closeZoom) 2 else 0)), + xyExp(0, -singleRouteOffset - (if (closeZoom) 2 else 0), shiftScale, settings), // Rail terminals at wide zoom have a special pill icon rather than the usual dot Exp.get(StopFeaturesBuilder.propIsTerminalKey) to - xyExp(0, -singleRouteOffset - (if (closeZoom) 0 else 2)), + xyExp(0, -singleRouteOffset - (if (closeZoom) 0 else 2), shiftScale, settings), // Regular rail stops, have a basic pill at close zoom and a dot at wide - fallback = xyExp(0, -singleRouteOffset), + fallback = xyExp(0, -singleRouteOffset, shiftScale, settings), ), - Exp(2) to xyExp(0, -doubleRouteOffset), - Exp(3) to xyExp(0, -tripleRouteOffset), + Exp(2) to xyExp(0, -doubleRouteOffset, shiftScale, settings), + Exp(3) to xyExp(0, -tripleRouteOffset, shiftScale, settings), ) } // Similar to offsetAlertExp, the labels are set to different height and width offsets based on // the type of icon that they're next to. Text offsets are measured in em. - val labelOffsetExp = - Exp.interpolate( + // TODO figure out if shiftScale = 1.0/13 actually works + fun labelOffsetExp(settings: Map): Exp> { + val shiftScale = 1.0 / 13 + return Exp.interpolate( Interpolation.Exponential(1.5), Exp.zoom(), Exp(MapDefaults.midZoomThreshold) to Exp.step( Exp.length(Exp.get(StopFeaturesBuilder.propMapRoutesKey)), Exp.case( - branchedRouteExp to xyExp(1.15, 0.75), - Exp.get(StopFeaturesBuilder.propIsTerminalKey) to xyExp(1, 0.75), - fallback = xyExp(0.75, 0.5), + branchedRouteExp to xyExp(1.15, 0.75, shiftScale * 0.5, settings), + Exp.get(StopFeaturesBuilder.propIsTerminalKey) to + xyExp(1, 0.75, shiftScale * 0.5, settings), + fallback = xyExp(0.75, 0.5, shiftScale * 0.5, settings), ), - Exp(2) to xyExp(0.5, 1.25), - Exp(3) to xyExp(0.5, 1.5), + Exp(2) to xyExp(0.5, 1.25, shiftScale * 0.5, settings), + Exp(3) to xyExp(0.5, 1.5, shiftScale * 0.5, settings), ), Exp(MapDefaults.closeZoomThreshold) to Exp.step( Exp.length(Exp.get(StopFeaturesBuilder.propMapRoutesKey)), - Exp.case(branchedRouteExp to xyExp(2.5, 1.5), xyExp(2, 1.5)), - Exp(2) to xyExp(2, 2), - Exp(3) to xyExp(2, 2.5), + Exp.case( + branchedRouteExp to xyExp(2.5, 1.5, shiftScale, settings), + xyExp(2, 1.5, shiftScale, settings), + ), + Exp(2) to xyExp(2, 2, shiftScale, settings), + Exp(3) to xyExp(2, 2.5, shiftScale, settings), ), ) + } // The modeResize array must contain 3 entries for [BUS, COMMUTER, fallback] fun withMultipliers( @@ -233,9 +301,71 @@ internal object MapExp { ) } - // Mapbox only accepts iconOffset values if they're wrapped in this array literal expression - fun xyExp(x: Number, y: Number): Exp> { - return Exp.array(ArrayType.Number, 2, listOf(Exp(x), Exp(y))) + /** + * Hardcoding offsets based on route properties to minimize the occurrences of overlapping rail + * lines when drawn on the map + */ + fun lineShiftEast( + scale: Double, + routeIds: Exp>, + mapRoutes: Exp>, + settings: Map, + ): Exp { + if (settings[Settings.ShiftingDisabled] == true) return Exp(0.0) + val maxLineWidth = 6.0 + val greenOverlappingCR = setOf(Route.Id("CR-Lowell"), Route.Id("CR-Fitchburg")) + val redOverlappingCR = + setOf( + Route.Id("CR-Greenbush"), + Route.Id("CR-Kingston"), + Route.Id("CR-Middleborough"), + Route.Id("CR-NewBedford"), + ) + + val offset = + Exp.case( + // These CR routes overlap with GL, GL is offset below, so do nothing + Exp.any( + *greenOverlappingCR.map { Exp.`in`(Exp(it.idText), routeIds) }.toTypedArray() + ) to Exp(0.0), + // These CR routes overlap with RL. RL is offset below, shift West + Exp.any( + *redOverlappingCR.map { Exp.`in`(Exp(it.idText), routeIds) }.toTypedArray() + ) to Exp(maxLineWidth * 1.5 * scale), + // Some CR routes overlap with OL and should shift East. + // Shift the rest east too so they scale proportionally + Exp.`in`(Exp(MapStopRoute.COMMUTER.name), mapRoutes) to Exp(-maxLineWidth * scale), + // Account for overlapping North Station - Haymarket + // Offset to the East + Exp.any( + *listOf("Green-B", "Green-C", "Green-D", "Green-E", "line-Green") + .map { Exp.`in`(Exp(it), routeIds) } + .toTypedArray() + ) to Exp(maxLineWidth * scale), + fallback = Exp(0.0), + ) + + return offset + } + + fun xyExp( + x: Number, + y: Number, + shiftScale: Double, + settings: Map, + ): Exp> { + if ( + settings[Settings.ShiftingDisabled] == true || + settings[Settings.ShiftingIncludeStops] != true + ) + return Exp.array(ArrayType.Number, 2, listOf(Exp(x), Exp(y))) + val routeIds = Exp.get(StopFeaturesBuilder.propRouteIdsKey) + val mapRoutes = Exp.get(StopFeaturesBuilder.propMapRoutesKey) + return Exp.array( + ArrayType.Number, + 2, + listOf(lineShiftEast(shiftScale, routeIds, mapRoutes, settings), Exp(y)), + ) } // For the separate bus only stop and alert layers, this takes any arbitrary diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/RouteLayerGenerator.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/RouteLayerGenerator.kt index e92dee9738..0d9f6ad310 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/RouteLayerGenerator.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/RouteLayerGenerator.kt @@ -1,14 +1,18 @@ package com.mbta.tid.mbta_app.map +import com.mbta.tid.mbta_app.map.style.ArrayType import com.mbta.tid.mbta_app.map.style.Exp +import com.mbta.tid.mbta_app.map.style.Interpolation import com.mbta.tid.mbta_app.map.style.LineJoin import com.mbta.tid.mbta_app.map.style.LineLayer +import com.mbta.tid.mbta_app.map.style.TranslateAnchor import com.mbta.tid.mbta_app.map.style.downcastToColor +import com.mbta.tid.mbta_app.model.MapStopRoute import com.mbta.tid.mbta_app.model.Route -import com.mbta.tid.mbta_app.model.RouteType import com.mbta.tid.mbta_app.model.SegmentAlertState import com.mbta.tid.mbta_app.model.response.GlobalResponse import com.mbta.tid.mbta_app.model.response.MapFriendlyRouteResponse +import com.mbta.tid.mbta_app.repositories.Settings import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -25,12 +29,15 @@ public object RouteLayerGenerator { routesWithShapes: List, globalResponse: GlobalResponse, colorPalette: ColorPalette, - ): List = createAllRouteLayers(routesWithShapes, globalResponse.routes, colorPalette) + settings: Map, + ): List = + createAllRouteLayers(routesWithShapes, globalResponse.routes, colorPalette, settings) private suspend fun createAllRouteLayers( routesWithShapes: List, routesById: Map, colorPalette: ColorPalette, + settings: Map, ): List = withContext(Dispatchers.Default) { val sortedRoutes = @@ -42,16 +49,16 @@ public object RouteLayerGenerator { -routesById[it.routeId]!!.sortOrder } - sortedRoutes.map { createRouteLayer(routesById[it.routeId]!!) } + + sortedRoutes.map { createRouteLayer(routesById[it.routeId]!!, settings) } + // Draw all alerting layers on top so they are not covered by any overlapping route // shape sortedRoutes.flatMap { - createAlertingRouteLayers(routesById[it.routeId]!!, colorPalette) + createAlertingRouteLayers(routesById[it.routeId]!!, colorPalette, settings) } } - internal fun createRouteLayer(route: Route): LineLayer { - val layer = baseRouteLayer(getRouteLayerId(route.id), route) + internal fun createRouteLayer(route: Route, settings: Map): LineLayer { + val layer = baseRouteLayer(getRouteLayerId(route.id), route, settings) layer.lineWidth = Exp.step(Exp.zoom(), Exp(3), Exp(closeZoomCutoff) to Exp(4)) return layer } @@ -65,8 +72,9 @@ public object RouteLayerGenerator { internal fun createAlertingRouteLayers( route: Route, colorPalette: ColorPalette, + settings: Map, ): List { - val shuttledLayer = baseRouteLayer(getRouteLayerId(route.id, "shuttled"), route) + val shuttledLayer = baseRouteLayer(getRouteLayerId(route.id, "shuttled"), route, settings) shuttledLayer.filter = Exp.eq( Exp.get(RouteFeaturesBuilder.propAlertStateKey), @@ -75,7 +83,7 @@ public object RouteLayerGenerator { shuttledLayer.lineWidth = Exp.step(Exp.zoom(), Exp(4), Exp(closeZoomCutoff) to Exp(6)) shuttledLayer.lineDasharray = listOf(2.0, 1.33) - val suspendedLayer = baseRouteLayer(getRouteLayerId(route.id, "suspended"), route) + val suspendedLayer = baseRouteLayer(getRouteLayerId(route.id, "suspended"), route, settings) suspendedLayer.filter = Exp.eq( Exp.get(RouteFeaturesBuilder.propAlertStateKey), @@ -85,7 +93,8 @@ public object RouteLayerGenerator { suspendedLayer.lineDasharray = listOf(1.33, 2.0) suspendedLayer.lineColor = Exp(colorPalette.deemphasized).downcastToColor() - val alertBackgroundLayer = baseRouteLayer(getRouteLayerId(route.id, "alerting-bg"), route) + val alertBackgroundLayer = + baseRouteLayer(getRouteLayerId(route.id, "alerting-bg"), route, settings) alertBackgroundLayer.filter = Exp.`in`( Exp.get(RouteFeaturesBuilder.propAlertStateKey), @@ -98,53 +107,52 @@ public object RouteLayerGenerator { return listOf(alertBackgroundLayer, shuttledLayer, suspendedLayer) } - internal fun baseRouteLayer(layerId: String, route: Route): LineLayer { + internal fun baseRouteLayer( + layerId: String, + route: Route, + settings: Map, + ): LineLayer { val layer = LineLayer(id = layerId, source = RouteFeaturesBuilder.getRouteSourceId(route.id)) layer.lineColor = Exp("#${route.color}").downcastToColor() layer.lineJoin = LineJoin.Round - layer.lineOffset = Exp(lineOffset(route)) + if (settings[Settings.ShiftingDisabled] != true) { + if (settings[Settings.ShiftingUseTranslate] == true) { + layer.lineTranslate = + lineOffset( + route, + settings, + consumeShift = { Exp.array(ArrayType.Number, 2, listOf(it, Exp(0))) }, + ) + layer.lineTranslateAnchor = TranslateAnchor.MAP + } else { + layer.lineOffset = lineOffset(route, settings, consumeShift = { it }) + } + } + checkNotNull(layer.id) return layer } - /** - * Hardcoding offsets based on route properties to minimize the occurences of overlapping rail - * lines when drawn on the map - */ - private fun lineOffset(route: Route): Double { - val maxLineWidth = 6.0 - val greenOverlappingCR = setOf(Route.Id("CR-Lowell"), Route.Id("CR-Fitchburg")) - val redOverlappingCR = - setOf( - Route.Id("CR-Greenbush"), - Route.Id("CR-Kingston"), - Route.Id("CR-Middleborough"), - Route.Id("CR-NewBedford"), + internal fun lineOffset( + route: Route, + settings: Map, + consumeShift: (Exp) -> Exp, + ): Exp { + val routeIds = Exp.array(contents = listOf(Exp(route.id.idText))) + val mapRoutes = Exp.array(contents = listOf(Exp(MapStopRoute.matching(route)?.name ?: ""))) + fun b(scale: Double) = + consumeShift(MapExp.lineShiftEast(scale, routeIds, mapRoutes, settings)) + return if (settings[Settings.ShiftingScaleWithZoom] == true) { + Exp.interpolate( + Interpolation.Linear, + Exp.zoom(), + Exp(6) to b(0.5), + Exp(MapDefaults.midZoomThreshold) to b(0.5), + Exp(MapDefaults.closeZoomThreshold) to b(1.0), ) - - return if (route.type == RouteType.COMMUTER_RAIL) { - when { - greenOverlappingCR.contains(route.id) -> { - // These overlap with GL, GL is offset below, so do nothing - 0.0 - } - redOverlappingCR.contains(route.id) -> { - // These overlap with RL. RL is offset below, shift West - maxLineWidth * 1.5 - } - else -> { - // Some overlap with OL and should shift East. - // Shift the rest east too so they scale porportionally - -maxLineWidth - } - } - } else if (route.id.idText.contains("Green")) { - // Account for overlapping North Station - Haymarket - // Offset to the East - maxLineWidth } else { - 0.0 + b(1.0) } } } diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/StopFeaturesBuilder.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/StopFeaturesBuilder.kt index 1dcde33763..dcf7b1bdaf 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/StopFeaturesBuilder.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/StopFeaturesBuilder.kt @@ -25,8 +25,9 @@ public object StopFeaturesBuilder { // Map routes is an array of MapStopRoute enum names internal val propMapRoutesKey = FeatureProperty>("mapRoutes") internal val propNameKey = FeatureProperty("name") - // Route IDs are in a map keyed by MapStopRoute enum names, each with a list of IDs - internal val propRouteIdsKey = FeatureProperty>>("routeIds") + internal val propRouteIdsKey = FeatureProperty>("routeIds") + internal val propRouteIdsByTypeKey = + FeatureProperty>>("routeIdsByType") internal val propServiceStatusKey = FeatureProperty>("serviceStatus") internal val propSortOrderKey = FeatureProperty("sortOrder") @@ -131,8 +132,9 @@ public object StopFeaturesBuilder { put(propNameKey, stop.name) put(propIsTerminalKey, mapStop.isTerminal) put(propMapRoutesKey, mapStop.routeTypes.map { it.name }) + put(propRouteIdsKey, mapStop.routes.flatMap { it.value.map { it.id.idText } }) put( - propRouteIdsKey, + propRouteIdsByTypeKey, mapStop.routes .map { (routeType, routes) -> Pair(routeType.name, routes.map { it.id.idText }) } .toMap(), diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/StopIcons.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/StopIcons.kt index 40087b55cd..a6b7679811 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/StopIcons.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/StopIcons.kt @@ -69,7 +69,10 @@ public object StopIcons { Exp("-"), Exp.at( Exp(0), - Exp.get(MapExp.topRouteExp, Exp.get(StopFeaturesBuilder.propRouteIdsKey)), + Exp.get( + MapExp.topRouteExp, + Exp.get(StopFeaturesBuilder.propRouteIdsByTypeKey), + ), ), ), Exp(""), diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/StopLayerGenerator.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/StopLayerGenerator.kt index db5f3ba3a0..d4d5a89efb 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/StopLayerGenerator.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/StopLayerGenerator.kt @@ -1,12 +1,15 @@ package com.mbta.tid.mbta_app.map +import com.mbta.tid.mbta_app.map.style.ArrayType import com.mbta.tid.mbta_app.map.style.Exp +import com.mbta.tid.mbta_app.map.style.Interpolation import com.mbta.tid.mbta_app.map.style.SymbolLayer import com.mbta.tid.mbta_app.map.style.TextAnchor import com.mbta.tid.mbta_app.map.style.TextJustify import com.mbta.tid.mbta_app.map.style.downcastToColor import com.mbta.tid.mbta_app.model.MapStopRoute import com.mbta.tid.mbta_app.model.StopDetailsFilter +import com.mbta.tid.mbta_app.repositories.Settings import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.ExperimentalSerializationApi @@ -42,17 +45,26 @@ public object StopLayerGenerator { public suspend fun createStopLayers( colorPalette: ColorPalette, state: State, + settings: Map, ): List { return withContext(Dispatchers.Default) { val sourceId = StopFeaturesBuilder.stopSourceId val stopLayer = - createStopLayer(id = stopLayerId, colorPalette = colorPalette, state = state) + createStopLayer( + id = stopLayerId, + colorPalette = colorPalette, + state = state, + settings = settings, + ) val stopTouchTargetLayer = SymbolLayer(id = stopTouchTargetLayerId, source = sourceId) stopTouchTargetLayer.iconImage = Exp.image(Exp(StopIcons.stopDummyIcon)) stopTouchTargetLayer.iconPadding = 22.0 includeSharedProps(stopTouchTargetLayer, forBus = false, state = state) + if (settings[Settings.ShiftingIncludeStops] == true) { + stopTouchTargetLayer.iconOffset = shiftFromLine(settings, scale = 1.0) + } val stopSelectedPinLayer = SymbolLayer(id = stopLayerSelectedPinId, source = sourceId) stopSelectedPinLayer.iconImage = @@ -65,8 +77,8 @@ public object StopLayerGenerator { MapExp.selectedExp(state) to Exp.get(StopFeaturesBuilder.propNameKey), Exp(""), ) - includedDefaultTextProps(stopSelectedPinLayer, colorPalette) - stopSelectedPinLayer.iconOffset = offsetPinValue() + includedDefaultTextProps(stopSelectedPinLayer, colorPalette, settings) + stopSelectedPinLayer.iconOffset = offsetPinValue(settings) stopTouchTargetLayer.filter = Exp.ge( Exp.zoom(), @@ -79,7 +91,7 @@ public object StopLayerGenerator { val transferLayer = SymbolLayer(id = getTransferLayerId(index), source = sourceId) transferLayer.iconImage = (StopIcons.getTransferLayerIcon(index)) - transferLayer.iconOffset = offsetTransferValue(index) + transferLayer.iconOffset = offsetTransferValue(index, settings) includeSharedProps(transferLayer, forBus = false, state = state) return@map transferLayer @@ -87,12 +99,29 @@ public object StopLayerGenerator { val alertLayers = (0.rangeUntil(maxTransferLayers)).map { index -> - createAlertLayer(id = getAlertLayerId(index), index, state = state) + createAlertLayer( + id = getAlertLayerId(index), + index, + state = state, + settings = settings, + ) } val busLayer = - createStopLayer(id = busLayerId, forBus = true, colorPalette, state = state) - val busAlertLayer = createAlertLayer(id = busAlertLayerId, forBus = true, state = state) + createStopLayer( + id = busLayerId, + forBus = true, + colorPalette, + state = state, + settings, + ) + val busAlertLayer = + createAlertLayer( + id = busAlertLayerId, + forBus = true, + state = state, + settings = settings, + ) listOf(stopTouchTargetLayer, busLayer, busAlertLayer, stopLayer) + transferLayers + @@ -101,15 +130,44 @@ public object StopLayerGenerator { } } + private fun shiftFromLine( + settings: Map, + scale: Double = 1.0, + ): Exp> { + val routeIds = Exp.get(StopFeaturesBuilder.propRouteIdsKey) + val mapRoutes = Exp.get(StopFeaturesBuilder.propMapRoutesKey) + fun b(relativeScale: Double = 1.0) = + Exp.array( + ArrayType.Number, + 2, + listOf( + MapExp.lineShiftEast(scale * relativeScale, routeIds, mapRoutes, settings), + Exp(0), + ), + ) + return if (settings[Settings.ShiftingScaleWithZoom] == true) { + Exp.interpolate( + Interpolation.Linear, + Exp.zoom(), + Exp(6) to b(0.5), + Exp(MapDefaults.midZoomThreshold) to b(0.5), + Exp(MapDefaults.closeZoomThreshold) to b(1.0), + ) + } else { + b(1.0) + } + } + internal fun createAlertLayer( id: String, index: Int = 0, forBus: Boolean = false, state: State, + settings: Map, ): SymbolLayer { val alertLayer = SymbolLayer(id = id, source = StopFeaturesBuilder.stopSourceId) alertLayer.iconImage = AlertIcons.getAlertLayerIcon(index, forBus = forBus) - alertLayer.iconOffset = offsetAlertValue(index) + alertLayer.iconOffset = offsetAlertValue(index, settings) alertLayer.iconAllowOverlap = true includeSharedProps(alertLayer, forBus, state) @@ -121,18 +179,27 @@ public object StopLayerGenerator { forBus: Boolean = false, colorPalette: ColorPalette, state: State, + settings: Map, ): SymbolLayer { val stopLayer = SymbolLayer(id = id, source = StopFeaturesBuilder.stopSourceId) stopLayer.iconImage = (StopIcons.getStopLayerIcon(forBus = forBus)) stopLayer.textField = (MapExp.stopLabelTextExp(forBus = forBus, state = state)) - includedDefaultTextProps(stopLayer, colorPalette) + includedDefaultTextProps(stopLayer, colorPalette, settings) stopLayer.textAllowOverlap = false includeSharedProps(stopLayer, forBus, state) + if (settings[Settings.ShiftingIncludeStops] == true) { + stopLayer.iconOffset = + shiftFromLine(settings, scale = 1.0) // TODO figure out why 1/24 doesn’t work + } return stopLayer } - internal fun includedDefaultTextProps(layer: SymbolLayer, colorPalette: ColorPalette) { + internal fun includedDefaultTextProps( + layer: SymbolLayer, + colorPalette: ColorPalette, + settings: Map, + ) { layer.textColor = Exp(colorPalette.text).downcastToColor() layer.textFont = listOf("Inter Regular") layer.textHaloColor = Exp(colorPalette.fill3).downcastToColor() @@ -143,7 +210,7 @@ public object StopLayerGenerator { layer.textJustify = TextJustify.AUTO layer.textAllowOverlap = true layer.textOptional = true - layer.textOffset = MapExp.labelOffsetExp + layer.textOffset = MapExp.labelOffsetExp(settings) } @OptIn(ExperimentalSerializationApi::class) @@ -186,27 +253,33 @@ public object StopLayerGenerator { Exp.ge(Exp.zoom(), if (forBus) Exp(busStopZoomThreshold) else Exp(stopZoomThreshold)), ) - internal fun offsetAlertValue(index: Int): Exp> { + internal fun offsetAlertValue(index: Int, settings: Map): Exp> { return Exp.step( Exp.zoom(), - MapExp.offsetAlertExp(closeZoom = false, index), - Exp(MapDefaults.closeZoomThreshold) to MapExp.offsetAlertExp(closeZoom = true, index), + MapExp.offsetAlertExp(closeZoom = false, index, settings), + Exp(MapDefaults.closeZoomThreshold) to + MapExp.offsetAlertExp(closeZoom = true, index, settings), ) } - internal fun offsetTransferValue(index: Int): Exp> { + internal fun offsetTransferValue( + index: Int, + settings: Map, + ): Exp> { return Exp.step( Exp.zoom(), - MapExp.offsetTransferExp(closeZoom = false, index), - Exp(MapDefaults.closeZoomThreshold) to MapExp.offsetTransferExp(closeZoom = true, index), + MapExp.offsetTransferExp(closeZoom = false, index, settings), + Exp(MapDefaults.closeZoomThreshold) to + MapExp.offsetTransferExp(closeZoom = true, index, settings), ) } - internal fun offsetPinValue(): Exp> { + internal fun offsetPinValue(settings: Map): Exp> { return Exp.step( Exp.zoom(), - MapExp.offsetPinExp(closeZoom = false), - Exp(MapDefaults.closeZoomThreshold) to MapExp.offsetPinExp(closeZoom = true), + MapExp.offsetPinExp(closeZoom = false, settings = settings), + Exp(MapDefaults.closeZoomThreshold) to + MapExp.offsetPinExp(closeZoom = true, settings = settings), ) } } diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/morePage/MoreItem.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/morePage/MoreItem.kt index 21f3999788..2221737e65 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/morePage/MoreItem.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/morePage/MoreItem.kt @@ -16,7 +16,7 @@ public sealed class MoreItem { public data class NavLink( val label: SharedString, val callback: () -> Unit, - val note: String? = null, + val note: SharedString? = null, ) : MoreItem() public data class Phone(val label: String, val phoneNumber: String) : MoreItem() diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/SettingsRepository.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/SettingsRepository.kt index e58c67bb88..bfe23b89b6 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/SettingsRepository.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/SettingsRepository.kt @@ -44,6 +44,10 @@ public enum class Settings( Notifications(booleanPreferencesKey("notifications")), SearchRouteResults(booleanPreferencesKey("searchRouteResults_featureFlag")), StationAccessibility(booleanPreferencesKey("elevator_accessibility")), + ShiftingDisabled(booleanPreferencesKey("shifting_disabled")), + ShiftingUseTranslate(booleanPreferencesKey("shifting_use_translate")), + ShiftingIncludeStops(booleanPreferencesKey("shifting_include_stops")), + ShiftingScaleWithZoom(booleanPreferencesKey("shifting_scale_with_zoom")), } public class MockSettingsRepository diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/utils/MapLayerManager.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/utils/MapLayerManager.kt index 3718a76c5c..6c1945a47d 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/utils/MapLayerManager.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/utils/MapLayerManager.kt @@ -6,6 +6,7 @@ import com.mbta.tid.mbta_app.map.StopLayerGenerator import com.mbta.tid.mbta_app.map.style.FeatureCollection import com.mbta.tid.mbta_app.model.response.GlobalResponse import com.mbta.tid.mbta_app.model.response.MapFriendlyRouteResponse +import com.mbta.tid.mbta_app.repositories.Settings public interface IMapLayerManager { public suspend fun addLayers( @@ -13,6 +14,7 @@ public interface IMapLayerManager { state: StopLayerGenerator.State, globalResponse: GlobalResponse, colorPalette: ColorPalette, + settings: Map, ) public suspend fun addLayers( @@ -20,6 +22,7 @@ public interface IMapLayerManager { state: StopLayerGenerator.State, globalResponse: GlobalResponse, colorPalette: ColorPalette, + settings: Map, ) public fun resetPuckPosition() diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/utils/SharedString.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/utils/SharedString.kt index 3fde36e96b..348e0904b8 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/utils/SharedString.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/utils/SharedString.kt @@ -16,6 +16,10 @@ public enum class SharedString { RouteSearch, SendAppFeedback, SettingsSection, + ShiftingDisabled, + ShiftingIncludeStops, + ShiftingScaleWithZoom, + ShiftingUseTranslate, SoftwareLicenses, StationAccessibilityInfo, SupportAccessibilityNote, diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/viewModel/MapViewModel.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/viewModel/MapViewModel.kt index bb07731e87..234bd681a6 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/viewModel/MapViewModel.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/viewModel/MapViewModel.kt @@ -29,6 +29,7 @@ import com.mbta.tid.mbta_app.repositories.IErrorBannerStateRepository import com.mbta.tid.mbta_app.repositories.IGlobalRepository import com.mbta.tid.mbta_app.repositories.IRailRouteShapeRepository import com.mbta.tid.mbta_app.repositories.ISentryRepository +import com.mbta.tid.mbta_app.repositories.ISettingsRepository import com.mbta.tid.mbta_app.repositories.IStopRepository import com.mbta.tid.mbta_app.repositories.ITripRepository import com.mbta.tid.mbta_app.routes.SheetRoutes @@ -95,6 +96,7 @@ public class MapViewModel( private val globalRepository: IGlobalRepository, private val railRouteShapeRepository: IRailRouteShapeRepository, private val sentryRepository: ISentryRepository, + private val settingsRepository: ISettingsRepository, private val stopRepository: IStopRepository, private val tripRepository: ITripRepository, private val clock: Clock, @@ -286,9 +288,12 @@ public class MapViewModel( val state = stopLayerGeneratorState val globalResponse = globalData ?: return@run val colorPalette = if (isDarkMode) ColorPalette.dark else ColorPalette.light - routeShapes?.let { addLayers(it, state, globalResponse, colorPalette) } + val settings = settingsRepository.getSettings() + routeShapes?.let { + addLayers(it, state, globalResponse, colorPalette, settings) + } ?: allRailRouteShapes?.let { - addLayers(it, state, globalResponse, colorPalette) + addLayers(it, state, globalResponse, colorPalette, settings) } ?: return@run resetPuckPosition() @@ -688,12 +693,14 @@ public class MapViewModel( return } + val settings = settingsRepository.getSettings() layerManager?.updateRouteSourceData(routeSourceData) layerManager?.addLayers( routeShapes, stopLayerGeneratorState, globalResponse, if (isDarkMode == true) ColorPalette.dark else ColorPalette.light, + settings, ) } diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/viewModel/MoreViewModel.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/viewModel/MoreViewModel.kt index e20e454f6a..7399f53255 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/viewModel/MoreViewModel.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/viewModel/MoreViewModel.kt @@ -118,6 +118,22 @@ public class MoreViewModel( } }, ), + MoreItem.Toggle( + label = SharedString.ShiftingDisabled, + settings = Settings.ShiftingDisabled, + ), + MoreItem.Toggle( + label = SharedString.ShiftingUseTranslate, + settings = Settings.ShiftingUseTranslate, + ), + MoreItem.Toggle( + label = SharedString.ShiftingIncludeStops, + settings = Settings.ShiftingIncludeStops, + ), + MoreItem.Toggle( + label = SharedString.ShiftingScaleWithZoom, + settings = Settings.ShiftingScaleWithZoom, + ), ), ), MoreSection( diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/viewModel/viewModelModule.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/viewModel/viewModelModule.kt index 0c615c6bdf..deaf11cff2 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/viewModel/viewModelModule.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/viewModel/viewModelModule.kt @@ -42,6 +42,7 @@ public fun viewModelModule(): Module = module { get(), get(), get(), + get(), get(named("coroutineDispatcherDefault")), get(named("coroutineDispatcherIO")), ) diff --git a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/map/RouteLayerGeneratorTest.kt b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/map/RouteLayerGeneratorTest.kt index 1cc7c1d841..45802ebbae 100644 --- a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/map/RouteLayerGeneratorTest.kt +++ b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/map/RouteLayerGeneratorTest.kt @@ -15,6 +15,7 @@ class RouteLayerGeneratorTest { MapTestDataHelper.routeResponse.routesWithSegmentedShapes, MapTestDataHelper.global, ColorPalette.light, + settings = emptyMap(), ) assertEquals( @@ -44,6 +45,7 @@ class RouteLayerGeneratorTest { MapTestDataHelper.routeResponse.routesWithSegmentedShapes, MapTestDataHelper.global, ColorPalette.light, + settings = emptyMap(), ) for (layer in routeLayers) { @@ -58,6 +60,7 @@ class RouteLayerGeneratorTest { MapTestDataHelper.routeResponse.routesWithSegmentedShapes, MapTestDataHelper.global, ColorPalette.light, + settings = emptyMap(), ) for (route in listOf(MapTestDataHelper.routeRed, MapTestDataHelper.routeOrange)) { @@ -80,6 +83,7 @@ class RouteLayerGeneratorTest { MapTestDataHelper.routeResponse.routesWithSegmentedShapes, MapTestDataHelper.global, colorPalette, + settings = emptyMap(), ) for (suspendedLayer in routeLayers.filter { it.id.endsWith("-suspended") }) { diff --git a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/map/StopFeaturesBuilderTest.kt b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/map/StopFeaturesBuilderTest.kt index 0a730484b6..4e5ab06410 100644 --- a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/map/StopFeaturesBuilderTest.kt +++ b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/map/StopFeaturesBuilderTest.kt @@ -220,7 +220,8 @@ class StopFeaturesBuilderTest { val alewifeRoutes = alewifeFeature?.properties?.get(StopFeaturesBuilder.propMapRoutesKey) assertEquals(listOf(MapStopRoute.RED.name, MapStopRoute.BUS.name), alewifeRoutes) - val alewifeRouteIds = alewifeFeature?.properties?.get(StopFeaturesBuilder.propRouteIdsKey) + val alewifeRouteIds = + alewifeFeature?.properties?.get(StopFeaturesBuilder.propRouteIdsByTypeKey) assertEquals( mapOf( MapStopRoute.RED.name to listOf(MapTestDataHelper.routeRed.id.idText), diff --git a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/map/StopLayerGeneratorTest.kt b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/map/StopLayerGeneratorTest.kt index ffebb4f296..da9e6df4b8 100644 --- a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/map/StopLayerGeneratorTest.kt +++ b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/map/StopLayerGeneratorTest.kt @@ -25,6 +25,7 @@ class StopLayerGeneratorTest { StopLayerGenerator.createStopLayers( ColorPalette.light, StopLayerGenerator.State.Default, + settings = emptyMap(), ) assertEquals(11, stopLayers.size) @@ -94,6 +95,7 @@ class StopLayerGeneratorTest { StopLayerGenerator.createStopLayers( ColorPalette.light, StopLayerGenerator.State.Default, + settings = emptyMap(), ) val busLayer = stopLayers[1] @@ -163,6 +165,7 @@ class StopLayerGeneratorTest { StopLayerGenerator.State.StopDetails( selectedStopId = checkNotNull(alewifeFeature.id) ), + settings = emptyMap(), ) val stopLayer = stopLayers[3] @@ -237,6 +240,7 @@ class StopLayerGeneratorTest { stopFilter = StopDetailsFilter(routeId = selectedBusRoute.id, directionId = 0), tripStops = null, ), + settings = emptyMap(), ) val stopLayer = stopLayers.first { it.id == StopLayerGenerator.stopLayerId } @@ -330,6 +334,7 @@ class StopLayerGeneratorTest { stopFilter = StopDetailsFilter(routeId = selectedBusRoute.id, directionId = 0), tripStops = listOf(typicalBusStop.id, atypicalBusStop.id), ), + settings = emptyMap(), ) val stopLayer = stopLayers.first { it.id == StopLayerGenerator.stopLayerId } diff --git a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/repositories/SettingsRepositoryTest.kt b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/repositories/SettingsRepositoryTest.kt index 3077e83699..bcc3000217 100644 --- a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/repositories/SettingsRepositoryTest.kt +++ b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/repositories/SettingsRepositoryTest.kt @@ -42,6 +42,10 @@ internal class SettingsRepositoryTest : KoinTest { Settings.Notifications to false, Settings.SearchRouteResults to false, Settings.StationAccessibility to false, + Settings.ShiftingDisabled to false, + Settings.ShiftingIncludeStops to false, + Settings.ShiftingScaleWithZoom to false, + Settings.ShiftingUseTranslate to false, ), repo.getSettings(), ) diff --git a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/viewModel/MapViewModelTests.kt b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/viewModel/MapViewModelTests.kt index a471fc1f6b..509ee1ee94 100644 --- a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/viewModel/MapViewModelTests.kt +++ b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/viewModel/MapViewModelTests.kt @@ -510,6 +510,7 @@ internal class MapViewModelTests : KoinTest { any(), any(), any(), + any(), ) } viewModel.layerManagerInitialized(layerManager2) @@ -520,6 +521,7 @@ internal class MapViewModelTests : KoinTest { any(), any(), any(), + any(), ) } } @@ -651,6 +653,7 @@ internal class MapViewModelTests : KoinTest { globalRepository = get(), railRouteShapeRepository = get(), sentryRepository = get(), + settingsRepository = get(), stopRepository = stopRepo, tripRepository = get(), clock = get(),