// // ProgressMapView.swift // SportsTime // // Interactive map showing stadium visit progress with custom annotations. // import SwiftUI import MapKit // MARK: - Progress Map View struct ProgressMapView: View { let stadiums: [Stadium] let visitStatus: [String: StadiumVisitStatus] @Binding var selectedStadium: Stadium? @State private var mapViewModel = MapInteractionViewModel() @State private var cameraPosition: MapCameraPosition = .region(MapInteractionViewModel.defaultRegion) var body: some View { Map(position: $cameraPosition, interactionModes: [.zoom, .pan]) { ForEach(stadiums) { stadium in Annotation( stadium.name, coordinate: CLLocationCoordinate2D( latitude: stadium.latitude, longitude: stadium.longitude ) ) { StadiumMapPin( stadium: stadium, isVisited: isVisited(stadium), isSelected: selectedStadium?.id == stadium.id, onTap: { Theme.Animation.withMotion(.spring(response: 0.3)) { if selectedStadium?.id == stadium.id { selectedStadium = nil } else { selectedStadium = stadium } } } ) } } } .onMapCameraChange { _ in mapViewModel.userDidInteract() } .overlay(alignment: .bottomTrailing) { if mapViewModel.shouldShowResetButton { Button { Theme.Animation.withMotion(.easeInOut(duration: 0.5)) { cameraPosition = .region(MapInteractionViewModel.defaultRegion) mapViewModel.resetToDefault() selectedStadium = nil } } label: { Image(systemName: "arrow.counterclockwise") .font(.title3) .padding(12) .background(.regularMaterial, in: Circle()) } .accessibilityLabel("Reset map view") .padding() } } .mapStyle(.standard(elevation: .realistic)) .clipShape(RoundedRectangle(cornerRadius: 16)) } private func isVisited(_ stadium: Stadium) -> Bool { if case .visited = visitStatus[stadium.id] { return true } return false } } // MARK: - Stadium Map Pin struct StadiumMapPin: View { let stadium: Stadium let isVisited: Bool let isSelected: Bool let onTap: () -> Void @Environment(\.colorScheme) private var colorScheme var body: some View { Button(action: onTap) { VStack(spacing: 2) { ZStack { // Pin background Circle() .fill(pinColor) .frame(width: pinSize, height: pinSize) .shadow(color: .black.opacity(0.2), radius: 2, y: 1) // Icon Image(systemName: isVisited ? "checkmark" : "sportscourt") .font(.system(size: iconSize, weight: .bold)) .foregroundStyle(.white) } // Pin pointer Triangle() .fill(pinColor) .frame(width: 10, height: 6) .offset(y: -2) .accessibilityHidden(true) // Stadium name (when selected) if isSelected { Text(stadium.name) .font(.caption2) .fontWeight(.semibold) .foregroundStyle(colorScheme == .dark ? .white : .primary) .padding(.horizontal, 8) .padding(.vertical, 4) .background { Capsule() .fill(colorScheme == .dark ? Color(.systemGray5) : .white) .shadow(color: .black.opacity(0.15), radius: 4, y: 2) } .fixedSize() .transition(.scale.combined(with: .opacity)) } } } .buttonStyle(.plain) .accessibilityLabel("\(stadium.name), \(isVisited ? "visited" : "not visited")") .accessibilityValue(isSelected ? "Selected" : "Not selected") .accessibilityAddTraits(isSelected ? .isSelected : []) .animation(Theme.Animation.prefersReducedMotion ? nil : .spring(response: 0.3), value: isSelected) } private var pinColor: Color { if isVisited { return .green } else { return .orange } } private var pinSize: CGFloat { isSelected ? 36 : 28 } private var iconSize: CGFloat { isSelected ? 16 : 12 } } // MARK: - Triangle Shape struct Triangle: Shape { func path(in rect: CGRect) -> Path { var path = Path() path.move(to: CGPoint(x: rect.midX, y: rect.maxY)) path.addLine(to: CGPoint(x: rect.minX, y: rect.minY)) path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY)) path.closeSubpath() return path } } // MARK: - Map Region Extension extension ProgressMapView { /// Calculate region to fit all stadiums static func region(for stadiums: [Stadium]) -> MKCoordinateRegion { guard !stadiums.isEmpty else { // Default to US center return MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: 39.8283, longitude: -98.5795), span: MKCoordinateSpan(latitudeDelta: 50, longitudeDelta: 50) ) } let latitudes = stadiums.map { $0.latitude } let longitudes = stadiums.map { $0.longitude } let minLat = latitudes.min()! let maxLat = latitudes.max()! let minLon = longitudes.min()! let maxLon = longitudes.max()! let center = CLLocationCoordinate2D( latitude: (minLat + maxLat) / 2, longitude: (minLon + maxLon) / 2 ) let span = MKCoordinateSpan( latitudeDelta: (maxLat - minLat) * 1.3 + 2, // Add padding longitudeDelta: (maxLon - minLon) * 1.3 + 2 ) return MKCoordinateRegion(center: center, span: span) } } // MARK: - Preview #Preview { ProgressMapView( stadiums: [], visitStatus: [:], selectedStadium: .constant(nil) ) .frame(height: 300) .padding() }