diff --git a/app/Common/Components/MapAnnotation/BusAnnotation.swift b/app/Common/Components/MapAnnotation/BusAnnotation.swift new file mode 100644 index 00000000..99e0c507 --- /dev/null +++ b/app/Common/Components/MapAnnotation/BusAnnotation.swift @@ -0,0 +1,200 @@ + +import MapKit +import SwiftUI + +struct StopAnnotation: View { + let routes: [String] + var stopIcon: String + let isMetro: Bool + + init(routes: [String]) { + isMetro = routes.allSatisfy { METRO_LINES.contains($0) } + self.routes = routes + + guard !isMetro else { + stopIcon = "tram.circle.fill" + return + } + + let transportTypes = Set(routes.map { + getVehicleType($0) + }) + + if transportTypes.contains(.bus) { + stopIcon = "bus" + } else { + stopIcon = transportTypes.first!.rawValue + } + } + + var body: some View { + if isMetro { + MetroAnnotationStack(metroLines: routes) + } else { + Image( + systemName: stopIcon + ) + .imageScale(.small) + .padding(3) + .font(.system(size: 16)) + .foregroundStyle(.white) + .background( + LinearGradient( + gradient: Gradient(colors: getBgColors(routes) + + ), + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .clipShape(.rect(cornerRadius: 4)) + .overlay( + RoundedRectangle(cornerRadius: 6) + .stroke(.white, lineWidth: 2) + ) + } + } +} + +#Preview("Metro") { + Map { + Annotation( + "Random place on map", coordinate: CLLocationCoordinate2D( + latitude: 50.113680, longitude: 14.449520) + ) { + StopAnnotation( + routes: ["A"] + ) + } + } +} + +#Preview("Metro Stack") { + Map { + Annotation( + "Random place on map", coordinate: CLLocationCoordinate2D( + latitude: 50.113680, longitude: 14.449520) + ) { + StopAnnotation( + routes: ["A", "C"] + ) + } + } +} + +#Preview("Tram") { + Map { + Annotation( + "Random place on map", coordinate: CLLocationCoordinate2D( + latitude: 50.113680, longitude: 14.449520) + ) { + StopAnnotation( + routes: ["78"] + ) + } + } +} + +#Preview("Night Tram") { + Map { + Annotation( + "Random place on map", coordinate: CLLocationCoordinate2D( + latitude: 50.113680, longitude: 14.449520) + ) { + StopAnnotation( + routes: ["90"] + ) + } + } +} + +#Preview("Bus") { + Map { + Annotation( + "Random place on map", coordinate: CLLocationCoordinate2D( + latitude: 50.113680, longitude: 14.449520) + ) { + StopAnnotation( + routes: ["700"] + ) + } + } +} + +#Preview("Bus & Tram") { + Map { + Annotation( + "Random place on map", coordinate: CLLocationCoordinate2D( + latitude: 50.113680, longitude: 14.449520) + ) { + StopAnnotation( + routes: ["60", "700"] + ) + } + } +} + +#Preview("Night Bus") { + Map { + Annotation( + "Random place on map", coordinate: CLLocationCoordinate2D( + latitude: 50.113680, longitude: 14.449520) + ) { + StopAnnotation( + routes: ["900"] + ) + } + } +} + +#Preview("Detour") { + Map { + Annotation( + "Random place on map", coordinate: CLLocationCoordinate2D( + latitude: 50.113680, longitude: 14.449520) + ) { + StopAnnotation( + routes: ["X700"] + ) + } + } +} + +#Preview("Ferry") { + Map { + Annotation( + "Random place on map", coordinate: CLLocationCoordinate2D( + latitude: 50.113680, longitude: 14.449520) + ) { + StopAnnotation( + routes: ["P4"] + ) + } + } +} + +#Preview("Cable car") { + Map { + Annotation( + "Random place on map", coordinate: CLLocationCoordinate2D( + latitude: 50.113680, longitude: 14.449520) + ) { + StopAnnotation( + routes: ["LD"] + ) + } + } +} + +#Preview("Train") { + Map { + Annotation( + "Random place on map", coordinate: CLLocationCoordinate2D( + latitude: 50.113680, longitude: 14.449520) + ) { + StopAnnotation( + routes: ["S42"] + ) + } + } +} diff --git a/app/Common/Components/MapAnnotation/BusAnnotation/BusAnnotation.swift b/app/Common/Components/MapAnnotation/BusAnnotation/BusAnnotation.swift deleted file mode 100644 index 770c2be9..00000000 --- a/app/Common/Components/MapAnnotation/BusAnnotation/BusAnnotation.swift +++ /dev/null @@ -1,33 +0,0 @@ - -import MapKit -import SwiftUI - -struct BusStationAnnotation: View { - var body: some View { - Image( - systemName: "bus" - ) - .imageScale(.small) - .padding(2) - .font(.system(size: 16)) - .foregroundStyle(.white) - .background(.blue) - .clipShape(.rect(cornerRadius: 4)) - .overlay( - RoundedRectangle(cornerRadius: 6) - .stroke(.white, lineWidth: 2) - ) - } -} - -#Preview("Bus station annotation") { - Map { - Annotation( - "Random place on map", coordinate: CLLocationCoordinate2D( - latitude: 50.113680, longitude: 14.449520) - ) { - BusStationAnnotation( - ) - } - } -} diff --git a/app/Common/Components/MapAnnotation/AnnotationStack/MetroAnnotationStack.swift b/app/Common/Components/MapAnnotation/MetroAnnotationStack.swift similarity index 51% rename from app/Common/Components/MapAnnotation/AnnotationStack/MetroAnnotationStack.swift rename to app/Common/Components/MapAnnotation/MetroAnnotationStack.swift index f8353253..fefecd7b 100644 --- a/app/Common/Components/MapAnnotation/AnnotationStack/MetroAnnotationStack.swift +++ b/app/Common/Components/MapAnnotation/MetroAnnotationStack.swift @@ -31,30 +31,3 @@ struct MetroAnnotationStack: View { } } } - -#Preview("Two stations annotation") { - Map { - Annotation( - "Random place on map", coordinate: CLLocationCoordinate2D( - latitude: 50.113680, longitude: 14.449520) - ) { - MetroAnnotationStack( - metroLines: ["A", "B"] - ) - } - } -} - -// this is not very valid for Prague, but might be useful in the future -#Preview("Multiple stations annotation") { - Map { - Annotation( - "Random place on map", coordinate: CLLocationCoordinate2D( - latitude: 50.113680, longitude: 14.449520) - ) { - MetroAnnotationStack( - metroLines: ["A", "B", "C", "A", "B", "C"] - ) - } - } -} diff --git a/app/Common/Components/MapAnnotation/MetroAnnotation/MetroStationAnnotation.swift b/app/Common/Components/MapAnnotation/MetroStationAnnotation.swift similarity index 100% rename from app/Common/Components/MapAnnotation/MetroAnnotation/MetroStationAnnotation.swift rename to app/Common/Components/MapAnnotation/MetroStationAnnotation.swift diff --git a/app/Common/Utils/Vehicle/get-vehicle-bg-colors.swift b/app/Common/Utils/Vehicle/get-vehicle-bg-colors.swift new file mode 100644 index 00000000..3f2f8d9d --- /dev/null +++ b/app/Common/Utils/Vehicle/get-vehicle-bg-colors.swift @@ -0,0 +1,33 @@ + +import SwiftUI + +func getBgColors(_ routes: [String]) -> [Color] { + let vehicleTypes: [VehicleType] = routes.map { getVehicleType($0) } + + if vehicleTypes.count == 1, vehicleTypes[0] == .metro { + return [getMetroLineColor(routes[0])] + } + + if routes.allSatisfy({ isNightService($0) }) { + return [.gray] + } else if routes.allSatisfy({ $0.starts(with: "X") }) { + return [.orange] + } + + var res: [Color] = [] + if vehicleTypes.contains(.bus) { + res.append(.blue) + } + + if vehicleTypes.contains(.tram) { + res.append(.indigo) + } else if vehicleTypes.contains(.lightrail) { + res.append(.gray) + } else if vehicleTypes.contains(.cablecar) { + res.append(.brown) + } else if vehicleTypes.contains(.ferry) { + res.append(.mint) + } + + return res +} diff --git a/app/Common/Utils/Vehicle/get-vehicle-type.swift b/app/Common/Utils/Vehicle/get-vehicle-type.swift new file mode 100644 index 00000000..fd32dedd --- /dev/null +++ b/app/Common/Utils/Vehicle/get-vehicle-type.swift @@ -0,0 +1,35 @@ + +// https://pid.cz/jizdni-rady-podle-linek/metro/ +func getVehicleType(_ nameAnyCased: String) -> VehicleType { + let name = nameAnyCased.uppercased() + + if name.hasPrefix("LD") { + return .cablecar + } + + if name.hasPrefix("R") || + name.hasPrefix("S") || + name.hasPrefix("T") || + name.hasPrefix("U") + { + return .lightrail + } + + if name.hasPrefix("P") { + return .ferry + } + + let number = Int(name) + guard let number else { + return .bus + } + + // trolley bus + if number == 58 || number == 59 { + return .bus + } else if number < 100 { + return .tram + } + + return .bus +} diff --git a/app/Common/Utils/Vehicle/is-night-service.swift b/app/Common/Utils/Vehicle/is-night-service.swift new file mode 100644 index 00000000..d32ac83b --- /dev/null +++ b/app/Common/Utils/Vehicle/is-night-service.swift @@ -0,0 +1,10 @@ + + +func isNightService(_ name: String) -> Bool { + let number = Int(name) + guard let number else { + return false + } + + return number >= 900 || (number >= 90 && number < 100) +} diff --git a/app/Common/Utils/Vehicle/vehicle-type-enum.swift b/app/Common/Utils/Vehicle/vehicle-type-enum.swift new file mode 100644 index 00000000..87ee38e1 --- /dev/null +++ b/app/Common/Utils/Vehicle/vehicle-type-enum.swift @@ -0,0 +1,9 @@ + +enum VehicleType: String { + case bus + case tram + case cablecar + case ferry + case metro + case lightrail +} diff --git a/app/Common/Utils/metroUtils.swift b/app/Common/Utils/metroUtils.swift index 46983667..2593cdac 100644 --- a/app/Common/Utils/metroUtils.swift +++ b/app/Common/Utils/metroUtils.swift @@ -9,6 +9,8 @@ import Foundation import MapKit import SwiftUI +let METRO_LINES: [String] = ["A", "B", "C"] + enum MetroLine: String { case A case B @@ -73,4 +75,20 @@ func getClosestStationFromGeoJSON(location: CLLocation) -> MetroStationsGeoJSONF return stations.features[closestStationIndex] } -func getSortedStationsByDistance() {} +func shortenStopName(_ name: String) -> String { + if name == "Depo Hostivař" { + return "D. Hostivař" + } else if name == "Nemocnice Motol" { + return "N. Motol" + } else if name == "Pražského povstání" { + return "P. povstání" + } + + let shorten: String = name + .replacingOccurrences(of: "Náměstí", with: "Nám.") + .replacingOccurrences(of: "náměstí", with: "Nád.") + .replacingOccurrences(of: "Nádraží", with: "Nád.") + .replacingOccurrences(of: "nádraží", with: "nád.") + + return shorten +} diff --git a/app/metro-now-widgets/Core/Views/small/SmallWidgetView.swift b/app/metro-now-widgets/Core/Views/small/SmallWidgetView.swift index d5397e59..d3d88c96 100644 --- a/app/metro-now-widgets/Core/Views/small/SmallWidgetView.swift +++ b/app/metro-now-widgets/Core/Views/small/SmallWidgetView.swift @@ -18,7 +18,7 @@ struct SmallWidgetView: View { ForEach(0 ..< 2) { index in if entry.departures.count > index { DepartureView( - direction: entry.departures[index].direction, + direction: shortenStopName(entry.departures[index].direction), departureDate: entry.departures[index].departureDate, metroLine: entry.departures[index].metroLine ) diff --git a/app/metro-now.xcodeproj/project.pbxproj b/app/metro-now.xcodeproj/project.pbxproj index fc6565c5..5471a18f 100644 --- a/app/metro-now.xcodeproj/project.pbxproj +++ b/app/metro-now.xcodeproj/project.pbxproj @@ -171,9 +171,9 @@ 2D5BA5EB2C382FEA0055F12A /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( - AnnotationStack/MetroAnnotationStack.swift, - BusAnnotation/BusAnnotation.swift, - MetroAnnotation/MetroStationAnnotation.swift, + BusAnnotation.swift, + MetroAnnotationStack.swift, + MetroStationAnnotation.swift, ); target = 2DC639D72BF3CCBA00A72C7F /* metro-now */; }; @@ -194,6 +194,36 @@ ); target = 2DC639D72BF3CCBA00A72C7F /* metro-now */; }; + 2DD2EBC42C5302D400E3ACF4 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + "get-vehicle-bg-colors.swift", + "get-vehicle-type.swift", + "is-night-service.swift", + "vehicle-type-enum.swift", + ); + target = 2DC639D72BF3CCBA00A72C7F /* metro-now */; + }; + 2DD2EBC52C5302D400E3ACF4 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + "get-vehicle-bg-colors.swift", + "get-vehicle-type.swift", + "is-night-service.swift", + "vehicle-type-enum.swift", + ); + target = 2D4486822BFAA10A005C59CE /* metro-now-watch Watch App */; + }; + 2DD2EBC62C5302D400E3ACF4 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + "get-vehicle-bg-colors.swift", + "get-vehicle-type.swift", + "is-night-service.swift", + "vehicle-type-enum.swift", + ); + target = 2DF48A432C01F185002F754E /* metro-now-widgetsExtension */; + }; 2DFD2F2B2C4DC2EC009C81CC /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( @@ -221,6 +251,7 @@ 2D5BA5EA2C382FC30055F12A /* MapAnnotation */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (2D5BA5EB2C382FEA0055F12A /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = MapAnnotation; sourceTree = ""; }; 2D6C8CDD2C514882003E09A8 /* Countdown */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (2D6C8CE02C51488E003E09A8 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Countdown; sourceTree = ""; }; 2D6C8CE12C5157FC003E09A8 /* StationDetailView */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (2D6C8CE22C515802003E09A8 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = StationDetailView; sourceTree = ""; }; + 2DD2EBBD2C5302B700E3ACF4 /* Vehicle */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (2DD2EBC42C5302D400E3ACF4 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 2DD2EBC52C5302D400E3ACF4 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 2DD2EBC62C5302D400E3ACF4 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Vehicle; sourceTree = ""; }; 2DFD2F272C4DC296009C81CC /* API */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (2DFD2F2B2C4DC2EC009C81CC /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 2DFD2F432C4DCC37009C81CC /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = API; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ @@ -282,6 +313,7 @@ 2D1B2C3D2BFAD6F6007ED5EB /* Utils */ = { isa = PBXGroup; children = ( + 2DD2EBBD2C5302B700E3ACF4 /* Vehicle */, 2D1B2C412BFAD72C007ED5EB /* jsonUtils.swift */, 2D1B2C442BFAD7DB007ED5EB /* mapUtils.swift */, 2D1B2C4A2BFAD807007ED5EB /* fileUtils.swift */, diff --git a/app/metro-now/Core/Map/MapView.swift b/app/metro-now/Core/Map/MapView.swift index 228fd7c6..c85801ff 100644 --- a/app/metro-now/Core/Map/MapView.swift +++ b/app/metro-now/Core/Map/MapView.swift @@ -16,6 +16,8 @@ struct MapView: View { @State private var stops: [Stop]? @State private var visibleStops: [Stop] = [] + @State private var openedAnnotation: String? = nil + var body: some View { NavigationStack { Map { @@ -30,24 +32,47 @@ struct MapView: View { showDirection: true ) ) { - MetroAnnotationStack(metroLines: station.metroLines) + StopAnnotation( + routes: station.metroLines + ) } } } ForEach($visibleStops.wrappedValue, id: \.id) { stop in - Annotation( // stop.name - "", - coordinate: CLLocationCoordinate2D( - latitude: stop.latitude, - longitude: stop.longitude - ) - ) { - BusStationAnnotation() - .frame( - width: 4, - height: 4 + Annotation(stop.name, + coordinate: CLLocationCoordinate2D( + latitude: stop.latitude, + longitude: stop.longitude + )) { + + StopAnnotation( + routes: stop.routes.map(\.name) ) + .onTapGesture { + openedAnnotation = stop.id + } + + .sheet( + isPresented: Binding( + get: { openedAnnotation == stop.id }, + set: { newValue in + if !newValue { + openedAnnotation = nil + } + } + ), + onDismiss: { + openedAnnotation = nil + } + ) { + Text(stop.name) + .presentationDetents([ .medium, .large + ]) + .presentationDragIndicator(.visible) + .presentationBackgroundInteraction(.enabled) + .presentationCornerRadius(32) + } } } } @@ -55,7 +80,6 @@ struct MapView: View { .onMapCameraChange { bounds in - print(bounds) let region = bounds.region guard stops?.count ?? 0 > 0 else { visibleStops = [] @@ -68,6 +92,7 @@ struct MapView: View { element.latitude < region.center.latitude + region.span.latitudeDelta, element.longitude > region.center.longitude - region.span.longitudeDelta, element.longitude < region.center.longitude + region.span.longitudeDelta, + !METRO_LINES.contains(element.routes.first?.name ?? ""), i < 100 { i += 1