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

Feature/20774 location modes #345

Merged
merged 8 commits into from
Mar 27, 2024
Merged
10 changes: 4 additions & 6 deletions Sources/TripKit/managers/TKRegionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public class TKRegionManager: NSObject {
return try JSONDecoder().decode(TKAPI.RegionsResponse.self, from: data)
}.value
if let response {
await updateRegions(from: response)
updateRegions(from: response)
}

} catch {
Expand Down Expand Up @@ -77,7 +77,7 @@ public class TKRegionManager: NSObject {
extension TKRegionManager {

@MainActor
func updateRegions(from response: TKAPI.RegionsResponse) async {
func updateRegions(from response: TKAPI.RegionsResponse) {
// Silently ignore obviously bad data
guard response.modes != nil, response.regions != nil else {
// This asset isn't valid, due to race conditions
Expand All @@ -88,10 +88,8 @@ extension TKRegionManager {
self.response = response
NotificationCenter.default.post(name: .TKRegionManagerUpdatedRegions, object: self)

Task.detached(priority: .utility) {
if let encoded = try? JSONEncoder().encode(response) {
TKRegionManager.saveToCache(encoded)
}
if let encoded = try? JSONEncoder().encode(response) {
TKRegionManager.saveToCache(encoded)
}
}

Expand Down
10 changes: 10 additions & 0 deletions Sources/TripKit/managers/TKStyleManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ extension TKStyleManager {
return image!
}

public static func image(systemName: String) -> TKImage? {
#if canImport(UIKit)
return TKImage(systemName: systemName)?.withRenderingMode(.alwaysTemplate)
#elseif os(macOS)
return nil
#else
return nil
#endif
}

public static func optionalImage(named: String) -> TKImage? {
#if canImport(UIKit)
return TKImage(named: named, in: .tripKit, compatibleWith: nil)
Expand Down
16 changes: 16 additions & 0 deletions Sources/TripKit/model/TKNamedCoordinate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ open class TKNamedCoordinate : NSObject, NSSecureCoding, Codable, TKClusterable

@objc public var isSuburb: Bool = false

public var klass: String? = nil

public var modeIdentifiers: [String]? = nil

public var priority: Float?

@objc(namedCoordinateForAnnotation:)
Expand Down Expand Up @@ -144,6 +148,8 @@ open class TKNamedCoordinate : NSObject, NSSecureCoding, Codable, TKClusterable
case isDraggable
case isSuburb
case clusterIdentifier
case modeIdentifiers
case klass = "class"
}

public required init(from decoder: Decoder) throws {
Expand All @@ -170,6 +176,8 @@ open class TKNamedCoordinate : NSObject, NSSecureCoding, Codable, TKClusterable
locationID = try container.decodeIfPresent(String.self, forKey: .locationID)
timeZoneID = try container.decodeIfPresent(String.self, forKey: .timeZoneID)
clusterIdentifier = try container.decodeIfPresent(String.self, forKey: .clusterIdentifier)
modeIdentifiers = try container.decodeIfPresent([String].self, forKey: .modeIdentifiers)
klass = try container.decodeIfPresent(String.self, forKey: .klass)
isDraggable = (try container.decodeIfPresent(Bool.self, forKey: .isDraggable)) ?? false
isSuburb = (try container.decodeIfPresent(Bool.self, forKey: .isSuburb)) ?? false

Expand All @@ -189,6 +197,8 @@ open class TKNamedCoordinate : NSObject, NSSecureCoding, Codable, TKClusterable
try container.encode(locationID, forKey: .locationID)
try container.encode(timeZoneID, forKey: .timeZoneID)
try container.encode(clusterIdentifier, forKey: .clusterIdentifier)
try container.encode(modeIdentifiers, forKey: .modeIdentifiers)
try container.encode(klass, forKey: .klass)
try container.encode(isDraggable, forKey: .isDraggable)
try container.encode(isSuburb, forKey: .isSuburb)

Expand All @@ -214,6 +224,8 @@ open class TKNamedCoordinate : NSObject, NSSecureCoding, Codable, TKClusterable
self.locationID = decoded.locationID
self.timeZoneID = decoded.timeZoneID
self.clusterIdentifier = decoded.clusterIdentifier
self.modeIdentifiers = decoded.modeIdentifiers
self.klass = decoded.klass
self.data = decoded.data
self.isSuburb = decoded.isSuburb
self.isDraggable = decoded.isDraggable
Expand All @@ -235,6 +247,8 @@ open class TKNamedCoordinate : NSObject, NSSecureCoding, Codable, TKClusterable
locationID = aDecoder.decodeObject(of: NSString.self, forKey: "locationID") as String?
timeZoneID = aDecoder.decodeObject(of: NSString.self, forKey: "timeZone") as String?
clusterIdentifier = aDecoder.decodeObject(of: NSString.self, forKey: "clusterIdentifier") as String?
modeIdentifiers = aDecoder.decodeObject(of: [NSString.self, NSArray.self], forKey: "modeIdentifiers") as? [String]
klass = aDecoder.decodeObject(of: NSString.self, forKey: "klass") as String?
_placemark = aDecoder.decodeObject(of: CLPlacemark.self, forKey: "placemark")
isDraggable = aDecoder.decodeBool(forKey: "isDraggable")
isSuburb = aDecoder.decodeBool(forKey: "isSuburb")
Expand All @@ -256,6 +270,8 @@ open class TKNamedCoordinate : NSObject, NSSecureCoding, Codable, TKClusterable
aCoder.encode(locationID, forKey: "locationID")
aCoder.encode(timeZoneID, forKey: "timeZone")
aCoder.encode(clusterIdentifier, forKey: "clusterIdentifier")
aCoder.encode(modeIdentifiers, forKey: "modeIdentifiers")
aCoder.encode(klass, forKey: "klass")
aCoder.encode(_placemark, forKey: "placemark")
aCoder.encode(isDraggable, forKey: "isDraggable")
aCoder.encode(isSuburb, forKey: "isSuburb")
Expand Down
65 changes: 49 additions & 16 deletions Sources/TripKit/search/TKTripGoGeocoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,15 @@ public typealias TKSkedGoGeocoder = TKTripGoGeocoder
///
/// This geocoder is a wrapper around the [`geocode.json` endpoint](https://developer.tripgo.com/specs/#tag/Geocode/paths/~1geocode.json/get) of the TripGo API.
public class TKTripGoGeocoder: NSObject {
private var lastRect: MKMapRect = .null
private struct CacheToken: Equatable {
static func == (lhs: TKTripGoGeocoder.CacheToken, rhs: TKTripGoGeocoder.CacheToken) -> Bool {
MKMapRectEqualToRect(lhs.lastRect, rhs.lastRect) && lhs.modes == rhs.modes
}

var lastRect: MKMapRect = .null
var modes: Set<String> = []
}
private var resultCacheToken: CacheToken?
private var resultCache = NSCache<NSString, NSArray>()

private var onCompletion: (String, (Result<[TKAutocompletionResult], Error>) -> Void)? = nil
Expand Down Expand Up @@ -51,7 +59,13 @@ extension TKTripGoGeocoder: TKGeocoding {
}

let region = coordinateRegion.flatMap(TKRegionManager.shared.region)
TKServer.shared.hit(TKAPI.GeocodeResponse.self, path: "geocode.json", parameters: paras, region: region) { _, _, result in

var parasWithModes = paras
if let available = region?.modeIdentifiers {
parasWithModes["modes"] = Array(TKSettings.enabledModeIdentifiers(available))
}

TKServer.shared.hit(TKAPI.GeocodeResponse.self, path: "geocode.json", parameters: parasWithModes, region: region) { _, _, result in
completion(
result.map { response in
let coordinates = response.choices.map(\.named)
Expand All @@ -71,10 +85,25 @@ extension TKTripGoGeocoder: TKAutocompleting {
return
}

if lastRect.isNull || !lastRect.contains(mapRect) {
var paras: [String: Any] = [
"q": input,
"a": true
]

let coordinateRegion = MKCoordinateRegion(mapRect)
paras["near"] = coordinateRegion.center.isValid ? "\(coordinateRegion.center.latitude),\(coordinateRegion.center.longitude)" : nil

let region = TKRegionManager.shared.region(containing: coordinateRegion)
let modes = TKSettings.enabledModeIdentifiers(region.modeIdentifiers)
paras["modes"] = Array(modes)

let cacheToken = CacheToken(lastRect: mapRect, modes: modes)

if resultCacheToken != cacheToken {
// invalidate the cache
resultCache.removeAllObjects()
lastRect = mapRect
resultCacheToken = cacheToken

} else {
if let cached = resultCache.object(forKey: input as NSString) as? [TKAutocompletionResult] {
completion(.success(cached))
Expand All @@ -85,16 +114,6 @@ extension TKTripGoGeocoder: TKAutocompleting {
// Putting it into a local variable so that we can cancel this.
self.onCompletion = (input, completion)

var paras: [String: Any] = [
"q": input,
"a": true
]

let coordinateRegion = MKCoordinateRegion(mapRect)
paras["near"] = coordinateRegion.center.isValid ? "\(coordinateRegion.center.latitude),\(coordinateRegion.center.longitude)" : nil

let region = TKRegionManager.shared.region(containing: coordinateRegion)

TKServer.shared.hit(TKAPI.GeocodeResponse.self, path: "geocode.json", parameters: paras, region: region) { [weak self] _, _, result in
guard let self = self else { return }

Expand All @@ -116,14 +135,23 @@ extension TKTripGoGeocoder: TKAutocompleting {
accessoryAccessibilityLabel: Loc.ShowTimetable,
score: tuple.score
)

} else {
let preferredImage: TKImage?
switch named.klass {
case "SchoolLocation":
preferredImage = TKStyleManager.image(systemName: "graduationcap.fill")
default:
preferredImage = nil
}

return TKAutocompletionResult(
object: named,
title: name,
titleHighlightRanges: tuple.titleHighlight,
subtitle: named.address,
subtitleHighlightRanges: tuple.subtitleHighlight,
image: TKAutocompletionResult.image(for: .pin),
image: preferredImage ?? TKAutocompletionResult.image(for: .pin),
score: tuple.score
)
}
Expand Down Expand Up @@ -175,8 +203,13 @@ extension TKTripGoGeocoder {
return .init(score: ranged, titleHighlight: highlight ?? [])

} else if let query = query, let name = named.name ?? named.title {
var boost: Int = 0
// Rank those higher which get special modal functionality
if named.modeIdentifiers != nil {
boost += 20
}
let titleScore = TKAutocompletionResult.nameScore(searchTerm: query, candidate: name)
let ranged = TKAutocompletionResult.rangedScore(for: titleScore.score, min: 0, max: 50)
let ranged = TKAutocompletionResult.rangedScore(for: titleScore.score, min: 0 + boost, max: 50 + boost)
return .init(score: ranged, titleHighlight: titleScore.ranges)

} else {
Expand Down
4 changes: 2 additions & 2 deletions Sources/TripKit/server/TKRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -242,13 +242,13 @@ extension TKRouter {

let includesAllModes = try request.performAndWait { $0.additional.contains { $0.name == "allModes" } }

let enabledModes = try modes ?? request.performAndWait(\.modes)
if includesAllModes {
modeIdentifiers = try modes ?? request.performAndWait(\.modes)
modeIdentifiers = enabledModes
fetchTrips(for: request, bestOnly: false, additional: nil, callbackQueue: queue, completion: completion)
return 1
}

let enabledModes = try modes ?? request.performAndWait(\.modes)
guard !enabledModes.isEmpty else {
throw RoutingError.invalidRequest("No modes enabled")
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/TripKit/server/TKServer+Regions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ extension TKRegionManager {

switch response.result {
case .success(let model):
await updateRegions(from: model)
updateRegions(from: model)
if hasRegions {
return
} else {
Expand Down
37 changes: 27 additions & 10 deletions Sources/TripKitUI/cards/TKUIHomeCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import Foundation
import MapKit
import UIKit
import SwiftUI

import RxSwift
import RxCocoa
Expand Down Expand Up @@ -77,7 +78,12 @@ open class TKUIHomeCard: TKUITableCard {
tableView.keyboardDismissMode = .onDrag

tableView.register(TKUIHomeCardSectionHeader.self, forHeaderFooterViewReuseIdentifier: "TKUIHomeCardSectionHeader")
tableView.register(TKUIAutocompletionResultCell.self, forCellReuseIdentifier: TKUIAutocompletionResultCell.reuseIdentifier)

if #available(iOS 16, *) {
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "plain")
} else {
tableView.register(TKUIAutocompletionResultCell.self, forCellReuseIdentifier: TKUIAutocompletionResultCell.reuseIdentifier)
}

tableView.dataSource = nil
tableView.tableFooterView = UIView() // no trailing separators
Expand All @@ -95,16 +101,27 @@ open class TKUIHomeCard: TKUITableCard {

switch item {
case .search(let searchItem):
guard
let cell = tv.dequeueReusableCell(withIdentifier: TKUIAutocompletionResultCell.reuseIdentifier, for: ip) as? TKUIAutocompletionResultCell
if #available(iOS 16, *) {
let cell = tv.dequeueReusableCell(withIdentifier: "plain", for: ip)
cell.contentConfiguration = UIHostingConfiguration {
TKUIAutocompletionResultView(item: searchItem) { [weak self] in
self?.cellAccessoryTapped.onNext(.search($0))
}
}
return cell

} else {
guard
let cell = tv.dequeueReusableCell(withIdentifier: TKUIAutocompletionResultCell.reuseIdentifier, for: ip) as? TKUIAutocompletionResultCell
else { assertionFailure("Unable to load an instance of TKUIAutocompletionResultCell"); return fallback }

cell.configure(
with: searchItem,
onAccessoryTapped: { self.cellAccessoryTapped.onNext(.search($0)) }
)
cell.accessibilityTraits = .button
return cell

cell.configure(
with: searchItem,
onAccessoryTapped: { [weak self] in self?.cellAccessoryTapped.onNext(.search($0)) }
)
cell.accessibilityTraits = .button
return cell
}

case .component(let componentItem):
guard let cell = self.viewModel.componentViewModels.compactMap({ $0.cell(for: componentItem, at: ip, in: tv) }).first else {
Expand Down
5 changes: 4 additions & 1 deletion Sources/TripKitUI/cards/TKUIMapManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ open class TKUIMapManager: TGMapManager {
/// - default: TKUIAnnotationViewBuilder
public static var annotationBuilderFactory: ((MKAnnotation, MKMapView) -> TKUIAnnotationViewBuilder) = TKUIAnnotationViewBuilder.init

/// The POI categories from Apple Maps to never show on the map, e.g., as they are added separately.
public static var pointsOfInterestsToExclude: [MKPointOfInterestCategory] = [.publicTransport]

static var tileOverlays: [String: MKTileOverlay] = [:]

/// Callback that fires when attributions need to be displayed. In particular when using `tiles`.
Expand Down Expand Up @@ -199,7 +202,7 @@ open class TKUIMapManager: TGMapManager {
// Keep heading
heading = mapView.camera.heading

mapView.pointOfInterestFilter = MKPointOfInterestFilter(excluding: [.publicTransport])
mapView.pointOfInterestFilter = MKPointOfInterestFilter(excluding: TKUIMapManager.pointsOfInterestsToExclude)

// Add content
mapView.addOverlays(overlays, level: overlayLevel)
Expand Down
34 changes: 25 additions & 9 deletions Sources/TripKitUI/cards/TKUIRoutingQueryInputCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import Foundation
import MapKit
import SwiftUI

import TGCardViewController
import RxSwift
Expand Down Expand Up @@ -67,16 +68,27 @@ public class TKUIRoutingQueryInputCard: TKUITableCard {
return UIAccessibility.isReduceMotionEnabled ? .reload : .animated
},
configureCell: { [weak accessoryTapped] _, tv, ip, item in
guard let cell = tv.dequeueReusableCell(withIdentifier: TKUIAutocompletionResultCell.reuseIdentifier, for: ip) as? TKUIAutocompletionResultCell else {
preconditionFailure("Couldn't dequeue TKUIAutocompletionResultCell")
}
if let accessoryTapped {
cell.configure(with: item, onAccessoryTapped: { accessoryTapped.onNext($0) })
if #available(iOS 16, *) {
let cell = tv.dequeueReusableCell(withIdentifier: "plain", for: ip)
cell.contentConfiguration = UIHostingConfiguration {
TKUIAutocompletionResultView(item: item) { [weak accessoryTapped] in
accessoryTapped?.onNext($0)
}
}
return cell

} else {
cell.configure(with: item)
guard let cell = tv.dequeueReusableCell(withIdentifier: TKUIAutocompletionResultCell.reuseIdentifier, for: ip) as? TKUIAutocompletionResultCell else {
preconditionFailure("Couldn't dequeue TKUIAutocompletionResultCell")
}
if let accessoryTapped {
cell.configure(with: item, onAccessoryTapped: { accessoryTapped.onNext($0) })
} else {
cell.configure(with: item)
}
cell.accessibilityTraits = .button
return cell
}
cell.accessibilityTraits = .button
return cell
},
titleForHeaderInSection: { ds, index in
return ds.sectionModels[index].title
Expand All @@ -87,7 +99,11 @@ public class TKUIRoutingQueryInputCard: TKUITableCard {
tableView.delegate = nil
tableView.dataSource = nil

tableView.register(TKUIAutocompletionResultCell.self, forCellReuseIdentifier: TKUIAutocompletionResultCell.reuseIdentifier)
if #available(iOS 16, *) {
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "plain")
} else {
tableView.register(TKUIAutocompletionResultCell.self, forCellReuseIdentifier: TKUIAutocompletionResultCell.reuseIdentifier)
}

let route = Signal.merge(
titleView.rx.route,
Expand Down
Loading
Loading