From 0f2593d57d318cc2d269511e31ca6ed8171bd983 Mon Sep 17 00:00:00 2001 From: Suleman Ali Date: Thu, 14 Aug 2025 12:12:45 +0500 Subject: [PATCH 01/13] feat: marker added fix(api): marker added chore: added markers function to View --- .../mapboxnavigation/MapboxNavigationView.kt | 4 +++ .../MapboxNavigationViewManager.kt | 10 +++++++ ios/MapboxNavigationView.swift | 30 ++++++++++++++----- ios/MapboxNavigationViewManager.m | 2 +- ios/MapboxNavigationViewManager.swift | 12 ++++++-- src/MapboxNavigation.tsx | 2 ++ src/MapboxNavigationViewNativeComponent.ts | 1 + src/types.ts | 1 + 8 files changed, 52 insertions(+), 10 deletions(-) diff --git a/android/src/main/java/com/mapboxnavigation/MapboxNavigationView.kt b/android/src/main/java/com/mapboxnavigation/MapboxNavigationView.kt index fd6e6b8..b65bb47 100644 --- a/android/src/main/java/com/mapboxnavigation/MapboxNavigationView.kt +++ b/android/src/main/java/com/mapboxnavigation/MapboxNavigationView.kt @@ -832,4 +832,8 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout else -> DirectionsCriteria.PROFILE_DRIVING_TRAFFIC } } + + public void updateMarkers(List> userList) { + // update pins here + } } diff --git a/android/src/main/java/com/mapboxnavigation/MapboxNavigationViewManager.kt b/android/src/main/java/com/mapboxnavigation/MapboxNavigationViewManager.kt index f1d3fb0..f1a3be1 100644 --- a/android/src/main/java/com/mapboxnavigation/MapboxNavigationViewManager.kt +++ b/android/src/main/java/com/mapboxnavigation/MapboxNavigationViewManager.kt @@ -116,6 +116,16 @@ class MapboxNavigationViewManager(private var reactContext: ReactApplicationCont } } + @ReactProp(name = "realTimeList") + public void setRealTimeList(MapboxNavigationView view, ReadableArray list) { + List> userList = new ArrayList<>(); + for (int i = 0; i < list.size(); i++) { + ReadableMap map = list.getMap(i); + userList.add(map.toHashMap()); + } + view.updateMarkers(userList); + } + companion object { const val NAME = "MapboxNavigationView" } diff --git a/ios/MapboxNavigationView.swift b/ios/MapboxNavigationView.swift index ef7d9a8..4e9c655 100644 --- a/ios/MapboxNavigationView.swift +++ b/ios/MapboxNavigationView.swift @@ -29,18 +29,26 @@ public protocol MapboxCarPlayNavigationDelegate { public class MapboxNavigationView: UIView, NavigationViewControllerDelegate { public weak var navViewController: NavigationViewController? public var indexedRouteResponse: IndexedRouteResponse? - + var embedded: Bool var embedding: Bool @objc public var startOrigin: NSArray = [] { didSet { setNeedsLayout() } } - + var waypoints: [Waypoint] = [] { didSet { setNeedsLayout() } } - + // ADD THIS + @objc var realTimeList: NSArray = [] { + didSet { + let userList = realTimeList as? [[String: Any]] ?? [] + DispatchQueue.main.async { + updateMarkers(userList) + } + } + } func setWaypoints(waypoints: [MapboxWaypoint]) { self.waypoints = waypoints.enumerated().map { (index, waypointData) in let name = waypointData.name as? String ?? "\(index)" @@ -49,11 +57,11 @@ public class MapboxNavigationView: UIView, NavigationViewControllerDelegate { return waypoint } } - + @objc var destination: NSArray = [] { didSet { setNeedsLayout() } } - + @objc var shouldSimulateRoute: Bool = false @objc var showsEndOfRouteFeedback: Bool = false @objc var showCancelButton: Bool = false @@ -96,7 +104,7 @@ public class MapboxNavigationView: UIView, NavigationViewControllerDelegate { super.removeFromSuperview() // cleanup and teardown any existing resources self.navViewController?.removeFromParent() - + // MARK: End CarPlay Navigation if let carPlayNavigation = UIApplication.shared.delegate as? MapboxCarPlayNavigationDelegate { carPlayNavigation.endNavigation() @@ -167,7 +175,7 @@ public class MapboxNavigationView: UIView, NavigationViewControllerDelegate { strongSelf.embedding = false strongSelf.embedded = true - + // MARK: Start CarPlay Navigation if let carPlayNavigation = UIApplication.shared.delegate as? MapboxCarPlayNavigationDelegate { carPlayNavigation.startNavigation(with: strongSelf) @@ -205,4 +213,12 @@ public class MapboxNavigationView: UIView, NavigationViewControllerDelegate { ]) return true; } + + @objc func updateMarkers(_ users: [[String: Any]]) { + // Example: + // 1. Remove old markers + // 2. Loop through `users` and add new markers + // Make sure to convert coordinates from Double + print("Updating markers with \(users.count) users") + } } diff --git a/ios/MapboxNavigationViewManager.m b/ios/MapboxNavigationViewManager.m index 1e369ca..ee8fa51 100644 --- a/ios/MapboxNavigationViewManager.m +++ b/ios/MapboxNavigationViewManager.m @@ -31,5 +31,5 @@ @interface RCT_EXTERN_MODULE(MapboxNavigationViewManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(distanceUnit, NSString) RCT_EXPORT_VIEW_PROPERTY(mute, BOOL) RCT_EXPORT_VIEW_PROPERTY(travelMode, NSString) - +RCT_EXPORT_VIEW_PROPERTY(realTimeList, NSArray) @end diff --git a/ios/MapboxNavigationViewManager.swift b/ios/MapboxNavigationViewManager.swift index 11b240c..560a3a6 100644 --- a/ios/MapboxNavigationViewManager.swift +++ b/ios/MapboxNavigationViewManager.swift @@ -10,11 +10,11 @@ class MapboxNavigationViewManager: RCTViewManager { override func view() -> UIView! { return MapboxNavigationView(); } - + override static func requiresMainQueueSetup() -> Bool { return true } - + @objc(setWaypoints:waypoints:) public func setWaypoints(view: Any, waypoints: [MapboxWaypoint]) { guard let currentView = view as? MapboxNavigationView else { @@ -22,4 +22,12 @@ class MapboxNavigationViewManager: RCTViewManager { } currentView.setWaypoints(waypoints: waypoints) } + @objc func setRealTimeList(_ reactTag: NSNumber, list: NSArray) { + self.bridge.uiManager.addUIBlock { (_, viewRegistry) in + if let view = viewRegistry?[reactTag] as? MapboxNavigationView { + let userList = list as? [[String: Any]] ?? [] + view.updateMarkers(userList) + } + } + } } diff --git a/src/MapboxNavigation.tsx b/src/MapboxNavigation.tsx index 5332a2d..559bcf6 100644 --- a/src/MapboxNavigation.tsx +++ b/src/MapboxNavigation.tsx @@ -107,6 +107,7 @@ class MapboxNavigation extends React.Component< onCancelNavigation, onError, travelMode, + realTimeList, // <-- NEW PROP ...rest } = this.props; @@ -128,6 +129,7 @@ class MapboxNavigation extends React.Component< onCancelNavigation?.(event.nativeEvent) } travelMode={travelMode} + realTimeList={realTimeList} {...rest} /> diff --git a/src/MapboxNavigationViewNativeComponent.ts b/src/MapboxNavigationViewNativeComponent.ts index b0df355..97c5f11 100644 --- a/src/MapboxNavigationViewNativeComponent.ts +++ b/src/MapboxNavigationViewNativeComponent.ts @@ -24,6 +24,7 @@ interface NativeProps extends ViewProps { showsEndOfRouteFeedback?: boolean; hideStatusView?: boolean; travelMode?: string; + realTimeList?: Array; } export default codegenNativeComponent( diff --git a/src/types.ts b/src/types.ts index dde4851..161b39d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -107,4 +107,5 @@ export interface MapboxNavigationProps { onError?: (error: MapboxEvent) => void; onCancelNavigation?: (event: MapboxEvent) => void; onArrive?: (point: WaypointEvent) => void; + realTimeList?: Array; // new optional prop } From 6d92d6be016240e619bda0eae62f4631d1262ab4 Mon Sep 17 00:00:00 2001 From: Suleman Ali Date: Sat, 16 Aug 2025 22:31:01 +0500 Subject: [PATCH 02/13] feat: marker added fix(api): marker added chore: added markers function to View --- ios/MapboxNavigationViewManager.swift | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/ios/MapboxNavigationViewManager.swift b/ios/MapboxNavigationViewManager.swift index 560a3a6..497a934 100644 --- a/ios/MapboxNavigationViewManager.swift +++ b/ios/MapboxNavigationViewManager.swift @@ -22,12 +22,9 @@ class MapboxNavigationViewManager: RCTViewManager { } currentView.setWaypoints(waypoints: waypoints) } - @objc func setRealTimeList(_ reactTag: NSNumber, list: NSArray) { - self.bridge.uiManager.addUIBlock { (_, viewRegistry) in - if let view = viewRegistry?[reactTag] as? MapboxNavigationView { - let userList = list as? [[String: Any]] ?? [] - view.updateMarkers(userList) - } + + + @objc override static func propConfig_realTimeList() -> [String] { + return ["NSArray"] // tells RN it's a prop, not just a method } - } } From e1f2a6ad8d3ab0032ba8f72b0f685a1439b5402f Mon Sep 17 00:00:00 2001 From: Suleman Ali Date: Sat, 16 Aug 2025 22:33:36 +0500 Subject: [PATCH 03/13] feat: marker added fix(api): marker added chore: added markers function to View --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e6079e9..1da334f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pawan-pk/react-native-mapbox-navigation", - "version": "0.5.2", + "version": "0.5.3", "description": "Mapbox React Native SDKs enable interactive maps and real-time, traffic-aware turn-by-turn navigation, dynamically adjusting routes to avoid congestion.", "source": "./src/index.tsx", "main": "./lib/commonjs/index.cjs", From ee8d8d4365cf4ec57b05251594a0fc23ed799838 Mon Sep 17 00:00:00 2001 From: Suleman Ali Date: Sun, 17 Aug 2025 00:14:38 +0500 Subject: [PATCH 04/13] feat: marker added fix(api): marker added chore: added markers function to View --- ios/MapboxNavigationView.swift | 2 +- ios/MapboxNavigationViewManager.swift | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/ios/MapboxNavigationView.swift b/ios/MapboxNavigationView.swift index 4e9c655..72866be 100644 --- a/ios/MapboxNavigationView.swift +++ b/ios/MapboxNavigationView.swift @@ -45,7 +45,7 @@ public class MapboxNavigationView: UIView, NavigationViewControllerDelegate { didSet { let userList = realTimeList as? [[String: Any]] ?? [] DispatchQueue.main.async { - updateMarkers(userList) + self.updateMarkers(userList) } } } diff --git a/ios/MapboxNavigationViewManager.swift b/ios/MapboxNavigationViewManager.swift index 497a934..2cb3002 100644 --- a/ios/MapboxNavigationViewManager.swift +++ b/ios/MapboxNavigationViewManager.swift @@ -22,9 +22,4 @@ class MapboxNavigationViewManager: RCTViewManager { } currentView.setWaypoints(waypoints: waypoints) } - - - @objc override static func propConfig_realTimeList() -> [String] { - return ["NSArray"] // tells RN it's a prop, not just a method - } } From fc0de332ce502911e7f5574ced83cd0e31de6290 Mon Sep 17 00:00:00 2001 From: Suleman Ali Date: Sun, 17 Aug 2025 00:22:28 +0500 Subject: [PATCH 05/13] feat: marker added fix(api): marker added chore: added markers function to View --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1da334f..e6079e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pawan-pk/react-native-mapbox-navigation", - "version": "0.5.3", + "version": "0.5.2", "description": "Mapbox React Native SDKs enable interactive maps and real-time, traffic-aware turn-by-turn navigation, dynamically adjusting routes to avoid congestion.", "source": "./src/index.tsx", "main": "./lib/commonjs/index.cjs", From 55f99c8e12f7f338a079b6bc5722f8de3ed2bedb Mon Sep 17 00:00:00 2001 From: Suleman Ali Date: Sun, 17 Aug 2025 02:15:06 +0500 Subject: [PATCH 06/13] feat: marker added fix(api): marker added chore: added markers function to View --- ios/MapboxNavigationView.swift | 34 ++++++++++++++------------- ios/MapboxNavigationViewManager.m | 7 +++++- ios/MapboxNavigationViewManager.swift | 8 +++++++ ios/MapboxParticipant.h | 0 ios/MapboxParticipant.m | 8 +++++++ ios/RCTConvert+MapboxNavigation.h | 5 ++++ ios/RCTConvert+MapboxNavigation.m | 20 ++++++++++++++++ 7 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 ios/MapboxParticipant.h create mode 100644 ios/MapboxParticipant.m diff --git a/ios/MapboxNavigationView.swift b/ios/MapboxNavigationView.swift index 72866be..a741cfb 100644 --- a/ios/MapboxNavigationView.swift +++ b/ios/MapboxNavigationView.swift @@ -40,15 +40,9 @@ public class MapboxNavigationView: UIView, NavigationViewControllerDelegate { var waypoints: [Waypoint] = [] { didSet { setNeedsLayout() } } - // ADD THIS - @objc var realTimeList: NSArray = [] { - didSet { - let userList = realTimeList as? [[String: Any]] ?? [] - DispatchQueue.main.async { - self.updateMarkers(userList) - } - } - } + var participants: [MapboxParticipant] = [] + + func setWaypoints(waypoints: [MapboxWaypoint]) { self.waypoints = waypoints.enumerated().map { (index, waypointData) in let name = waypointData.name as? String ?? "\(index)" @@ -214,11 +208,19 @@ public class MapboxNavigationView: UIView, NavigationViewControllerDelegate { return true; } - @objc func updateMarkers(_ users: [[String: Any]]) { - // Example: - // 1. Remove old markers - // 2. Loop through `users` and add new markers - // Make sure to convert coordinates from Double - print("Updating markers with \(users.count) users") - } + func setParticipants(participants: [MapboxParticipant]) { + self.participants = participants + guard let mapView = navViewController?.navigationMapView else { return } + for user in participants { + if let lat = user.latitude, + let lon = user.longitude { + let point = PointAnnotation(coordinate: CLLocationCoordinate2D(latitude: lat, longitude: lon)) + point.title = user.name as? String ?? "User" + mapView.addAnnotation(point) + } + } + + // You can add any logic here if you need to refresh the view when participants change + } + } diff --git a/ios/MapboxNavigationViewManager.m b/ios/MapboxNavigationViewManager.m index ee8fa51..07138d4 100644 --- a/ios/MapboxNavigationViewManager.m +++ b/ios/MapboxNavigationViewManager.m @@ -7,6 +7,7 @@ #import "React/RCTViewManager.h" #import "MapboxWaypoint.h" +#import "MapboxParticipant.h" #import "RCTConvert+MapboxNavigation.h" @interface RCT_EXTERN_MODULE(MapboxNavigationViewManager, RCTViewManager) @@ -31,5 +32,9 @@ @interface RCT_EXTERN_MODULE(MapboxNavigationViewManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(distanceUnit, NSString) RCT_EXPORT_VIEW_PROPERTY(mute, BOOL) RCT_EXPORT_VIEW_PROPERTY(travelMode, NSString) -RCT_EXPORT_VIEW_PROPERTY(realTimeList, NSArray) +RCT_CUSTOM_VIEW_PROPERTY(realTimeList, NSArray, NSObject) +{ + MapboxParticipantArray *participants = [RCTConvert MapboxParticipantArray:json]; + [self performSelector:@selector(setParticipants:participants:) withObject:view withObject:participants]; +} @end diff --git a/ios/MapboxNavigationViewManager.swift b/ios/MapboxNavigationViewManager.swift index 2cb3002..38e4c9d 100644 --- a/ios/MapboxNavigationViewManager.swift +++ b/ios/MapboxNavigationViewManager.swift @@ -22,4 +22,12 @@ class MapboxNavigationViewManager: RCTViewManager { } currentView.setWaypoints(waypoints: waypoints) } + + @objc(setParticipants:participants:) + public func setParticipants(view: Any, participants: [MapboxParticipant]) { + guard let currentView = view as? MapboxNavigationView else { + return + } + currentView.setParticipants(participants: participants) + } } diff --git a/ios/MapboxParticipant.h b/ios/MapboxParticipant.h new file mode 100644 index 0000000..e69de29 diff --git a/ios/MapboxParticipant.m b/ios/MapboxParticipant.m new file mode 100644 index 0000000..83c826d --- /dev/null +++ b/ios/MapboxParticipant.m @@ -0,0 +1,8 @@ +// +// MapboxParticipant.m +// + +#import "MapboxParticipant.h" + +@implementation MapboxParticipant +@end \ No newline at end of file diff --git a/ios/RCTConvert+MapboxNavigation.h b/ios/RCTConvert+MapboxNavigation.h index 5c41a48..a55d37f 100644 --- a/ios/RCTConvert+MapboxNavigation.h +++ b/ios/RCTConvert+MapboxNavigation.h @@ -9,6 +9,7 @@ #import #import "MapboxWaypoint.h" +#import "MapboxParticipant.h" @interface RCTConvert (MapboxNavigation) @@ -17,4 +18,8 @@ typedef NSArray MapboxWaypointArray; + (MapboxWaypointArray *)MapboxWaypointArray:(id)json; +// MARK: - Participant ++ (MapboxParticipant *)MapboxParticipant:(id)json; +typedef NSArray MapboxParticipantArray; ++ (MapboxParticipantArray *)MapboxParticipantArray:(id)json; @end diff --git a/ios/RCTConvert+MapboxNavigation.m b/ios/RCTConvert+MapboxNavigation.m index eac9906..e88dd15 100644 --- a/ios/RCTConvert+MapboxNavigation.m +++ b/ios/RCTConvert+MapboxNavigation.m @@ -23,5 +23,25 @@ + (MapboxWaypoint *)MapboxWaypoint:(id)json { RCT_ARRAY_CONVERTER(MapboxWaypoint) ++ (MapboxParticipant *)MapboxParticipant:(id)json { + MapboxParticipant *p = [MapboxParticipant new]; + json = [self NSDictionary:json]; + p.participantId = json[@"_id"]; + p.userMail = json[@"userMail"]; + p.coverImage = json[@"coverImage"]; + p.displayName = json[@"displayName"]; + p.imageUrl = json[@"imageUrl"]; + p.isBenzifiMember = json[@"isBenzifiMember"] != nil ? [json[@"isBenzifiMember"] boolValue] : NO; + p.nation = json[@"nation"]; + p.userName = json[@"userName"]; + p.coordinate = (CLLocationCoordinate2D){ + [self CLLocationDegrees:json[@"lat"]], + [self CLLocationDegrees:json[@"lng"]] + }; + return p; +} + +RCT_ARRAY_CONVERTER(MapboxParticipant) + @end From d36407b5153635217beac54a932008b2e79b6e63 Mon Sep 17 00:00:00 2001 From: Suleman Ali Date: Sun, 17 Aug 2025 02:16:36 +0500 Subject: [PATCH 07/13] feat: marker added fix(api): marker added chore: added markers function to View --- ios/MapboxParticipant.h | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ios/MapboxParticipant.h b/ios/MapboxParticipant.h index e69de29..392e8b7 100644 --- a/ios/MapboxParticipant.h +++ b/ios/MapboxParticipant.h @@ -0,0 +1,20 @@ +// +// MapboxParticipant.h +// + +#import +#import + +@interface MapboxParticipant : NSObject + +@property(nonatomic, strong) NSString *participantId; +@property(nonatomic, strong) NSString *userMail; +@property(nonatomic, strong) NSString *coverImage; +@property(nonatomic, strong) NSString *displayName; +@property(nonatomic, strong) NSString *imageUrl; +@property(nonatomic, assign) BOOL isBenzifiMember; +@property(nonatomic, strong) NSString *nation; +@property(nonatomic, strong) NSString *userName; +@property(nonatomic, assign) CLLocationCoordinate2D coordinate; + +@end \ No newline at end of file From e4989d8db7e040121e47d37535cea179b88c23d7 Mon Sep 17 00:00:00 2001 From: Suleman Ali Date: Sun, 17 Aug 2025 03:26:14 +0500 Subject: [PATCH 08/13] feat: marker added fix(api): marker added chore: added markers function to View --- ios/MapboxNavigationView.swift | 18 +++++++----------- src/MapboxNavigation.tsx | 4 ---- src/MapboxNavigationViewNativeComponent.ts | 13 ++++++++++++- src/types.ts | 15 ++++++++++++++- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/ios/MapboxNavigationView.swift b/ios/MapboxNavigationView.swift index a741cfb..9ba8df5 100644 --- a/ios/MapboxNavigationView.swift +++ b/ios/MapboxNavigationView.swift @@ -211,16 +211,12 @@ public class MapboxNavigationView: UIView, NavigationViewControllerDelegate { func setParticipants(participants: [MapboxParticipant]) { self.participants = participants guard let mapView = navViewController?.navigationMapView else { return } - for user in participants { - if let lat = user.latitude, - let lon = user.longitude { - let point = PointAnnotation(coordinate: CLLocationCoordinate2D(latitude: lat, longitude: lon)) - point.title = user.name as? String ?? "User" - mapView.addAnnotation(point) - } - } - - // You can add any logic here if you need to refresh the view when participants change + let pointAnnotationManager = mapView.mapView.annotations.makePointAnnotationManager() + pointAnnotationManager.annotations = participants.map {user in + var pointAnnotation = PointAnnotation(coordinate: user.coordinate) + pointAnnotation.image = .init(image: UIImage(named: "dest-pin")!, name: "dest-pin") + pointAnnotation.textField = user.displayName + return pointAnnotation + } } - } diff --git a/src/MapboxNavigation.tsx b/src/MapboxNavigation.tsx index 559bcf6..0aba348 100644 --- a/src/MapboxNavigation.tsx +++ b/src/MapboxNavigation.tsx @@ -12,10 +12,6 @@ import { import type { MapboxNavigationProps } from './types'; import MapboxNavigationView from './MapboxNavigationViewNativeComponent'; -// import MapboxNavigationNativeComponent, { -// Commands, -// } from './MapboxNavigationViewNativeComponent'; - const permissions: Array = Platform.OS === 'android' && Platform.Version >= 33 ? [ diff --git a/src/MapboxNavigationViewNativeComponent.ts b/src/MapboxNavigationViewNativeComponent.ts index 97c5f11..d368399 100644 --- a/src/MapboxNavigationViewNativeComponent.ts +++ b/src/MapboxNavigationViewNativeComponent.ts @@ -24,7 +24,18 @@ interface NativeProps extends ViewProps { showsEndOfRouteFeedback?: boolean; hideStatusView?: boolean; travelMode?: string; - realTimeList?: Array; + realTimeList?: { + _id: string; + userMail: string; + coverImage: string; + displayName: string; + imageUrl: string; + isBenzifiMember: boolean; + nation: string; + userName: string; + lat: number; + lng: number; + }[]; } export default codegenNativeComponent( diff --git a/src/types.ts b/src/types.ts index 161b39d..34ee7c4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -29,6 +29,19 @@ export type WaypointEvent = Coordinate & { index?: number; }; +export type Participant = { + _id: string; + userMail: string; + coverImage: string; + displayName: string; + imageUrl: string; + isBenzifiMember: boolean; + nation: string; + userName: string; + lat: number; + lng: number; +}; + export type Location = { latitude: number; longitude: number; @@ -69,6 +82,7 @@ export interface MapboxNavigationProps { destination: Coordinate & { title?: string }; language?: Language; distanceUnit?: 'metric' | 'imperial'; + realTimeList?: Participant[]; /** * Specifies the mode of travel for navigation. @@ -107,5 +121,4 @@ export interface MapboxNavigationProps { onError?: (error: MapboxEvent) => void; onCancelNavigation?: (event: MapboxEvent) => void; onArrive?: (point: WaypointEvent) => void; - realTimeList?: Array; // new optional prop } From 5aa53db38c42920ad07769ef737ba5c71522842e Mon Sep 17 00:00:00 2001 From: Suleman Ali Date: Tue, 19 Aug 2025 19:52:41 +0500 Subject: [PATCH 09/13] feat: marker added fix(api): marker added chore: added markers function to View --- .../MapboxNavigationViewManager.kt | 10 -- .../mapboxnavigation/ParticipantsManager.kt | 44 +++++ .../mapboxnavigation/ParticipantsPackage.kt | 18 ++ ios/MapboxNavigationView.swift | 28 +-- ios/MapboxNavigationViewManager.m | 6 +- ios/MapboxNavigationViewManager.swift | 8 - ios/MapboxParticipant.h | 20 --- ios/MapboxParticipant.m | 8 - ios/ParticipantsManager.m | 8 + ios/ParticipantsManager.swift | 54 ++++++ ios/RCTConvert+MapboxNavigation.h | 5 - ios/RCTConvert+MapboxNavigation.m | 23 +-- src/MapboxNavigation.tsx | 160 ++++++++---------- src/MapboxNavigationViewNativeComponent.ts | 12 -- src/types.ts | 14 -- 15 files changed, 208 insertions(+), 210 deletions(-) create mode 100644 android/src/main/java/com/mapboxnavigation/ParticipantsManager.kt create mode 100644 android/src/main/java/com/mapboxnavigation/ParticipantsPackage.kt delete mode 100644 ios/MapboxParticipant.h delete mode 100644 ios/MapboxParticipant.m create mode 100644 ios/ParticipantsManager.m create mode 100644 ios/ParticipantsManager.swift diff --git a/android/src/main/java/com/mapboxnavigation/MapboxNavigationViewManager.kt b/android/src/main/java/com/mapboxnavigation/MapboxNavigationViewManager.kt index f1a3be1..f1d3fb0 100644 --- a/android/src/main/java/com/mapboxnavigation/MapboxNavigationViewManager.kt +++ b/android/src/main/java/com/mapboxnavigation/MapboxNavigationViewManager.kt @@ -116,16 +116,6 @@ class MapboxNavigationViewManager(private var reactContext: ReactApplicationCont } } - @ReactProp(name = "realTimeList") - public void setRealTimeList(MapboxNavigationView view, ReadableArray list) { - List> userList = new ArrayList<>(); - for (int i = 0; i < list.size(); i++) { - ReadableMap map = list.getMap(i); - userList.add(map.toHashMap()); - } - view.updateMarkers(userList); - } - companion object { const val NAME = "MapboxNavigationView" } diff --git a/android/src/main/java/com/mapboxnavigation/ParticipantsManager.kt b/android/src/main/java/com/mapboxnavigation/ParticipantsManager.kt new file mode 100644 index 0000000..dd005c9 --- /dev/null +++ b/android/src/main/java/com/mapboxnavigation/ParticipantsManager.kt @@ -0,0 +1,44 @@ +package com.yourpackage.participants + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.WritableArray +import com.facebook.react.modules.core.DeviceEventManagerModule + +class ParticipantsManager(private val context: ReactApplicationContext) : + ReactContextBaseJavaModule(context) { + + companion object { + var shared: ParticipantsManager? = null + } + + private var participants: List> = emptyList() + + init { + shared = this + } + + override fun getName(): String = "ParticipantsManager" + + @ReactMethod + fun updateParticipants(list: List>) { + participants = list + + val array: WritableArray = Arguments.createArray() + list.forEach { map -> + val writableMap = Arguments.makeNativeMap(map) + array.pushMap(writableMap) + } + + sendEvent("onParticipantsUpdate", array) + } + + private fun sendEvent(eventName: String, data: Any) { + context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit(eventName, data) + } + + fun getParticipants(): List> = participants +} diff --git a/android/src/main/java/com/mapboxnavigation/ParticipantsPackage.kt b/android/src/main/java/com/mapboxnavigation/ParticipantsPackage.kt new file mode 100644 index 0000000..ebdbe52 --- /dev/null +++ b/android/src/main/java/com/mapboxnavigation/ParticipantsPackage.kt @@ -0,0 +1,18 @@ +package com.yourpackage.participants + +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ViewManager + +class ParticipantsPackage : ReactPackage { + override fun createViewManagers( + reactContext: ReactApplicationContext + ): MutableList> = mutableListOf() + + override fun createNativeModules( + reactContext: ReactApplicationContext + ): MutableList { + return mutableListOf(ParticipantsManager(reactContext)) + } +} diff --git a/ios/MapboxNavigationView.swift b/ios/MapboxNavigationView.swift index 9ba8df5..04ac2d8 100644 --- a/ios/MapboxNavigationView.swift +++ b/ios/MapboxNavigationView.swift @@ -2,6 +2,7 @@ import MapboxCoreNavigation import MapboxNavigation import MapboxDirections +import MapboxMaps extension UIView { var parentViewController: UIViewController? { @@ -26,7 +27,7 @@ public protocol MapboxCarPlayNavigationDelegate { func endNavigation() } -public class MapboxNavigationView: UIView, NavigationViewControllerDelegate { +public class MapboxNavigationView: UIView, NavigationViewControllerDelegate, ParticipantsManagerDelegate { public weak var navViewController: NavigationViewController? public var indexedRouteResponse: IndexedRouteResponse? @@ -40,8 +41,6 @@ public class MapboxNavigationView: UIView, NavigationViewControllerDelegate { var waypoints: [Waypoint] = [] { didSet { setNeedsLayout() } } - var participants: [MapboxParticipant] = [] - func setWaypoints(waypoints: [MapboxWaypoint]) { self.waypoints = waypoints.enumerated().map { (index, waypointData) in @@ -78,6 +77,12 @@ public class MapboxNavigationView: UIView, NavigationViewControllerDelegate { self.embedded = false self.embedding = false super.init(frame: frame) + ParticipantsManager.shared?.delegate = self + + // Initial load + if let participants = ParticipantsManager.shared?.getParticipants() { + updateParticipantsOnMap(participants) + } } required init?(coder aDecoder: NSCoder) { @@ -94,6 +99,11 @@ public class MapboxNavigationView: UIView, NavigationViewControllerDelegate { } } + func participantsDidUpdate(_ list: [[String: Any]]) { + print("Participants updated:", list) + updateParticipantsOnMap(list) + } + public override func removeFromSuperview() { super.removeFromSuperview() // cleanup and teardown any existing resources @@ -208,15 +218,7 @@ public class MapboxNavigationView: UIView, NavigationViewControllerDelegate { return true; } - func setParticipants(participants: [MapboxParticipant]) { - self.participants = participants - guard let mapView = navViewController?.navigationMapView else { return } - let pointAnnotationManager = mapView.mapView.annotations.makePointAnnotationManager() - pointAnnotationManager.annotations = participants.map {user in - var pointAnnotation = PointAnnotation(coordinate: user.coordinate) - pointAnnotation.image = .init(image: UIImage(named: "dest-pin")!, name: "dest-pin") - pointAnnotation.textField = user.displayName - return pointAnnotation - } + private func updateParticipantsOnMap(_ list: [[String: Any]]) { + // Update Mapbox markers here } } diff --git a/ios/MapboxNavigationViewManager.m b/ios/MapboxNavigationViewManager.m index 07138d4..41724b3 100644 --- a/ios/MapboxNavigationViewManager.m +++ b/ios/MapboxNavigationViewManager.m @@ -32,9 +32,5 @@ @interface RCT_EXTERN_MODULE(MapboxNavigationViewManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(distanceUnit, NSString) RCT_EXPORT_VIEW_PROPERTY(mute, BOOL) RCT_EXPORT_VIEW_PROPERTY(travelMode, NSString) -RCT_CUSTOM_VIEW_PROPERTY(realTimeList, NSArray, NSObject) -{ - MapboxParticipantArray *participants = [RCTConvert MapboxParticipantArray:json]; - [self performSelector:@selector(setParticipants:participants:) withObject:view withObject:participants]; -} + @end diff --git a/ios/MapboxNavigationViewManager.swift b/ios/MapboxNavigationViewManager.swift index 38e4c9d..2cb3002 100644 --- a/ios/MapboxNavigationViewManager.swift +++ b/ios/MapboxNavigationViewManager.swift @@ -22,12 +22,4 @@ class MapboxNavigationViewManager: RCTViewManager { } currentView.setWaypoints(waypoints: waypoints) } - - @objc(setParticipants:participants:) - public func setParticipants(view: Any, participants: [MapboxParticipant]) { - guard let currentView = view as? MapboxNavigationView else { - return - } - currentView.setParticipants(participants: participants) - } } diff --git a/ios/MapboxParticipant.h b/ios/MapboxParticipant.h deleted file mode 100644 index 392e8b7..0000000 --- a/ios/MapboxParticipant.h +++ /dev/null @@ -1,20 +0,0 @@ -// -// MapboxParticipant.h -// - -#import -#import - -@interface MapboxParticipant : NSObject - -@property(nonatomic, strong) NSString *participantId; -@property(nonatomic, strong) NSString *userMail; -@property(nonatomic, strong) NSString *coverImage; -@property(nonatomic, strong) NSString *displayName; -@property(nonatomic, strong) NSString *imageUrl; -@property(nonatomic, assign) BOOL isBenzifiMember; -@property(nonatomic, strong) NSString *nation; -@property(nonatomic, strong) NSString *userName; -@property(nonatomic, assign) CLLocationCoordinate2D coordinate; - -@end \ No newline at end of file diff --git a/ios/MapboxParticipant.m b/ios/MapboxParticipant.m deleted file mode 100644 index 83c826d..0000000 --- a/ios/MapboxParticipant.m +++ /dev/null @@ -1,8 +0,0 @@ -// -// MapboxParticipant.m -// - -#import "MapboxParticipant.h" - -@implementation MapboxParticipant -@end \ No newline at end of file diff --git a/ios/ParticipantsManager.m b/ios/ParticipantsManager.m new file mode 100644 index 0000000..b1913c1 --- /dev/null +++ b/ios/ParticipantsManager.m @@ -0,0 +1,8 @@ +#import +#import + +@interface RCT_EXTERN_MODULE(ParticipantsManager, RCTEventEmitter) + +RCT_EXTERN_METHOD(updateParticipants:(NSArray *)list) + +@end \ No newline at end of file diff --git a/ios/ParticipantsManager.swift b/ios/ParticipantsManager.swift new file mode 100644 index 0000000..0ec938d --- /dev/null +++ b/ios/ParticipantsManager.swift @@ -0,0 +1,54 @@ +import Foundation +import React + + + +protocol ParticipantsManagerDelegate: AnyObject { + func participantsDidUpdate(_ list: [[String: Any]]) +} + +@objc(ParticipantsManager) +class ParticipantsManager: RCTEventEmitter { + static var shared: ParticipantsManager? + + private var hasListeners = false + private var participants: [[String: Any]] = [] + weak var delegate: ParticipantsManagerDelegate? // 👈 native delegate + + override init() { + super.init() + ParticipantsManager.shared = self + } + + override static func requiresMainQueueSetup() -> Bool { + return true + } + + override func supportedEvents() -> [String]! { + return ["onParticipantsUpdate"] + } + + override func startObserving() { + hasListeners = true + } + + override func stopObserving() { + hasListeners = false + } + + // Called from JS + @objc func updateParticipants(_ list: [[String: Any]]) { + participants = list + // Notify JS listeners + if hasListeners { + sendEvent(withName: "onParticipantsUpdate", body: list) + } + // Notify native listener + delegate?.participantsDidUpdate(list) + } + + // Allow MapboxNavigationView to pull latest list + func getParticipants() -> [[String: Any]] { + return participants + } +} \ No newline at end of file diff --git a/ios/RCTConvert+MapboxNavigation.h b/ios/RCTConvert+MapboxNavigation.h index a55d37f..5c41a48 100644 --- a/ios/RCTConvert+MapboxNavigation.h +++ b/ios/RCTConvert+MapboxNavigation.h @@ -9,7 +9,6 @@ #import #import "MapboxWaypoint.h" -#import "MapboxParticipant.h" @interface RCTConvert (MapboxNavigation) @@ -18,8 +17,4 @@ typedef NSArray MapboxWaypointArray; + (MapboxWaypointArray *)MapboxWaypointArray:(id)json; -// MARK: - Participant -+ (MapboxParticipant *)MapboxParticipant:(id)json; -typedef NSArray MapboxParticipantArray; -+ (MapboxParticipantArray *)MapboxParticipantArray:(id)json; @end diff --git a/ios/RCTConvert+MapboxNavigation.m b/ios/RCTConvert+MapboxNavigation.m index e88dd15..cddfd4d 100644 --- a/ios/RCTConvert+MapboxNavigation.m +++ b/ios/RCTConvert+MapboxNavigation.m @@ -23,25 +23,4 @@ + (MapboxWaypoint *)MapboxWaypoint:(id)json { RCT_ARRAY_CONVERTER(MapboxWaypoint) -+ (MapboxParticipant *)MapboxParticipant:(id)json { - MapboxParticipant *p = [MapboxParticipant new]; - json = [self NSDictionary:json]; - p.participantId = json[@"_id"]; - p.userMail = json[@"userMail"]; - p.coverImage = json[@"coverImage"]; - p.displayName = json[@"displayName"]; - p.imageUrl = json[@"imageUrl"]; - p.isBenzifiMember = json[@"isBenzifiMember"] != nil ? [json[@"isBenzifiMember"] boolValue] : NO; - p.nation = json[@"nation"]; - p.userName = json[@"userName"]; - p.coordinate = (CLLocationCoordinate2D){ - [self CLLocationDegrees:json[@"lat"]], - [self CLLocationDegrees:json[@"lng"]] - }; - return p; -} - -RCT_ARRAY_CONVERTER(MapboxParticipant) - -@end - +@end \ No newline at end of file diff --git a/src/MapboxNavigation.tsx b/src/MapboxNavigation.tsx index 0aba348..71464ed 100644 --- a/src/MapboxNavigation.tsx +++ b/src/MapboxNavigation.tsx @@ -1,5 +1,4 @@ -import * as React from 'react'; - +import React, { useEffect, useState } from 'react'; import type { Permission, TextStyle, ViewStyle } from 'react-native'; import { PermissionsAndroid, @@ -8,7 +7,6 @@ import { Text, View, } from 'react-native'; - import type { MapboxNavigationProps } from './types'; import MapboxNavigationView from './MapboxNavigationViewNativeComponent'; @@ -20,45 +18,34 @@ const permissions: Array = ] : ['android.permission.ACCESS_FINE_LOCATION']; -type MapboxNavigationState = { - prepared: boolean; - error?: string; -}; - -class MapboxNavigation extends React.Component< - MapboxNavigationProps, - MapboxNavigationState -> { - constructor(props: MapboxNavigationProps) { - super(props); - this.createState(); - } +const MapboxNavigation: React.FC = (props) => { + const [prepared, setPrepared] = useState(false); + const [error, setError] = useState(); - createState() { - this.state = { prepared: false }; - } - - componentDidMount(): void { + useEffect(() => { if (Platform.OS === 'android') { - this.requestPermission(); + requestPermission(); } else { - this.setState({ prepared: true }); + setPrepared(true); } - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - async requestPermission() { + const requestPermission = async () => { try { - let result = await PermissionsAndroid.requestMultiple(permissions); + const result = await PermissionsAndroid.requestMultiple(permissions); type ResultKey = keyof typeof result; + if ( result[permissions[0] as ResultKey] === PermissionsAndroid.RESULTS.GRANTED ) { - this.setState({ prepared: true }); + setPrepared(true); } else { const errorMessage = 'Permission is not granted.'; - this.setState({ error: errorMessage }); + setError(errorMessage); } + if ( permissions.length > 1 && result[permissions[1] as ResultKey] !== @@ -66,81 +53,68 @@ class MapboxNavigation extends React.Component< ) { const errorMessage = 'Notification permission is not granted.'; console.warn(errorMessage); - - this.props.onError?.({ message: errorMessage }); + props.onError?.({ message: errorMessage }); } } catch (e) { - const error = e as Error; - this.setState({ error: error.message }); - console.warn('[Mapbox Navigation] ' + error.message); - this.props.onError?.({ message: error.message }); - } - } - - render() { - if (!this.state.prepared) { - const overiteViewStyle: ViewStyle = { - justifyContent: 'center', - alignItems: 'center', - }; - const overiteTextStyle: TextStyle = this.state.error - ? { color: 'red' } - : {}; - return ( - - Loading... - - ); + const err = e as Error; + setError(err.message); + console.warn('[Mapbox Navigation] ' + err.message); + props.onError?.({ message: err.message }); } - const { - startOrigin, - destination, - style, - distanceUnit = 'imperial', - onArrive, - onLocationChange, - onRouteProgressChange, - onCancelNavigation, - onError, - travelMode, - realTimeList, // <-- NEW PROP - ...rest - } = this.props; + }; + if (!prepared) { + const overiteViewStyle: ViewStyle = { + justifyContent: 'center', + alignItems: 'center', + }; + const overiteTextStyle: TextStyle = error ? { color: 'red' } : {}; return ( - - onLocationChange?.(event.nativeEvent)} - onRouteProgressChange={(event) => - onRouteProgressChange?.(event.nativeEvent) - } - onError={(event) => onError?.(event.nativeEvent)} - onArrive={(event) => onArrive?.(event.nativeEvent)} - onCancelNavigation={(event) => - onCancelNavigation?.(event.nativeEvent) - } - travelMode={travelMode} - realTimeList={realTimeList} - {...rest} - /> + + Loading... ); } -} + + const { + startOrigin, + destination, + style, + distanceUnit = 'imperial', + onArrive, + onLocationChange, + onRouteProgressChange, + onCancelNavigation, + onError, + travelMode, + ...rest + } = props; + + return ( + + onLocationChange?.(event.nativeEvent)} + onRouteProgressChange={(event) => + onRouteProgressChange?.(event.nativeEvent) + } + onError={(event) => onError?.(event.nativeEvent)} + onArrive={(event) => onArrive?.(event.nativeEvent)} + onCancelNavigation={(event) => onCancelNavigation?.(event.nativeEvent)} + travelMode={travelMode} + {...rest} + /> + + ); +}; const styles = StyleSheet.create({ - mapbox: { - flex: 1, - }, - message: { - textAlign: 'center', - fontSize: 16, - }, + mapbox: { flex: 1 }, + message: { textAlign: 'center', fontSize: 16 }, }); export default MapboxNavigation; diff --git a/src/MapboxNavigationViewNativeComponent.ts b/src/MapboxNavigationViewNativeComponent.ts index d368399..b0df355 100644 --- a/src/MapboxNavigationViewNativeComponent.ts +++ b/src/MapboxNavigationViewNativeComponent.ts @@ -24,18 +24,6 @@ interface NativeProps extends ViewProps { showsEndOfRouteFeedback?: boolean; hideStatusView?: boolean; travelMode?: string; - realTimeList?: { - _id: string; - userMail: string; - coverImage: string; - displayName: string; - imageUrl: string; - isBenzifiMember: boolean; - nation: string; - userName: string; - lat: number; - lng: number; - }[]; } export default codegenNativeComponent( diff --git a/src/types.ts b/src/types.ts index 34ee7c4..dde4851 100644 --- a/src/types.ts +++ b/src/types.ts @@ -29,19 +29,6 @@ export type WaypointEvent = Coordinate & { index?: number; }; -export type Participant = { - _id: string; - userMail: string; - coverImage: string; - displayName: string; - imageUrl: string; - isBenzifiMember: boolean; - nation: string; - userName: string; - lat: number; - lng: number; -}; - export type Location = { latitude: number; longitude: number; @@ -82,7 +69,6 @@ export interface MapboxNavigationProps { destination: Coordinate & { title?: string }; language?: Language; distanceUnit?: 'metric' | 'imperial'; - realTimeList?: Participant[]; /** * Specifies the mode of travel for navigation. From b9f8a413b1c6fea84511543bc10ceade04b15dab Mon Sep 17 00:00:00 2001 From: Suleman Ali Date: Tue, 19 Aug 2025 20:40:01 +0500 Subject: [PATCH 10/13] feat: marker added fix(api): marker added chore: added markers function to View --- ios/MapboxNavigationViewManager.m | 1 - ios/ParticipantsManager.swift | 16 +++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/ios/MapboxNavigationViewManager.m b/ios/MapboxNavigationViewManager.m index 41724b3..1e369ca 100644 --- a/ios/MapboxNavigationViewManager.m +++ b/ios/MapboxNavigationViewManager.m @@ -7,7 +7,6 @@ #import "React/RCTViewManager.h" #import "MapboxWaypoint.h" -#import "MapboxParticipant.h" #import "RCTConvert+MapboxNavigation.h" @interface RCT_EXTERN_MODULE(MapboxNavigationViewManager, RCTViewManager) diff --git a/ios/ParticipantsManager.swift b/ios/ParticipantsManager.swift index 0ec938d..b5e4981 100644 --- a/ios/ParticipantsManager.swift +++ b/ios/ParticipantsManager.swift @@ -18,8 +18,13 @@ class ParticipantsManager: RCTEventEmitter { override init() { super.init() ParticipantsManager.shared = self + print("✅ ParticipantsManager initialized") } + override static func moduleName() -> String! { + return "ParticipantsManager" + } + override static func requiresMainQueueSetup() -> Bool { return true } @@ -37,7 +42,8 @@ class ParticipantsManager: RCTEventEmitter { } // Called from JS - @objc func updateParticipants(_ list: [[String: Any]]) { + @objc(updateParticipants:) + func updateParticipants(_ list: [[String: Any]]) { participants = list // Notify JS listeners if hasListeners { @@ -47,6 +53,14 @@ class ParticipantsManager: RCTEventEmitter { delegate?.participantsDidUpdate(list) } +// @objc +// func updateParticipants(_ participants: NSArray) { +// print("📡 iOS ParticipantsManager got \(participants.count) participants") +// for case let dict as NSDictionary in participants { +// print("👉 Participant:", dict) +// } +// } + // Allow MapboxNavigationView to pull latest list func getParticipants() -> [[String: Any]] { return participants From ce731d8bfec8e0e4fbb5c0e70361fc12660462f2 Mon Sep 17 00:00:00 2001 From: Suleman Ali Date: Sat, 23 Aug 2025 08:22:17 +0500 Subject: [PATCH 11/13] feat: marker added fix(api): marker added chore: added markers function to View --- .../MapboxNavigationPackage.kt | 2 +- .../mapboxnavigation/MapboxNavigationView.kt | 125 ++++++++++++++++-- .../mapboxnavigation/ParticipantsManager.kt | 36 ++--- .../mapboxnavigation/ParticipantsPackage.kt | 18 --- .../src/main/res/drawable/ic_user_offline.png | Bin 0 -> 18868 bytes android/src/main/res/drawable/ic_user_pin.png | Bin 0 -> 18938 bytes example/android/build.gradle | 4 +- example/ios/.mapbox | 0 .../project.pbxproj | 46 +++---- .../Images.xcassets/Contents.json | 4 +- .../user_active_pin.imageset/Contents.json | 23 ++++ .../user_active_pin.imageset/dest-pin 1.png | Bin 0 -> 18938 bytes .../user_active_pin.imageset/dest-pin 2.png | Bin 0 -> 18938 bytes .../user_active_pin.imageset/dest-pin 3.png | Bin 0 -> 18938 bytes .../user_offline_pin.imageset/Contents.json | 23 ++++ .../user_offline_pin.imageset/dest-pin 1.png | Bin 0 -> 18868 bytes .../user_offline_pin.imageset/dest-pin 2.png | Bin 0 -> 18868 bytes .../user_offline_pin.imageset/dest-pin.png | Bin 0 -> 18868 bytes .../ios/MapboxNavigationExample/Info.plist | 2 - example/ios/Podfile.lock | 8 +- example/src/App.tsx | 74 ++++++----- example/src/Participant.ts | 12 ++ example/src/realTimeList.tsx | 72 ++++++++++ ios/MapboxNavigationView.swift | 32 ++++- ios/ParticipantsManager.swift | 23 +--- src/MapboxNavigation.tsx | 9 +- 26 files changed, 368 insertions(+), 145 deletions(-) delete mode 100644 android/src/main/java/com/mapboxnavigation/ParticipantsPackage.kt create mode 100644 android/src/main/res/drawable/ic_user_offline.png create mode 100644 android/src/main/res/drawable/ic_user_pin.png create mode 100644 example/ios/.mapbox create mode 100644 example/ios/MapboxNavigationExample/Images.xcassets/user_active_pin.imageset/Contents.json create mode 100644 example/ios/MapboxNavigationExample/Images.xcassets/user_active_pin.imageset/dest-pin 1.png create mode 100644 example/ios/MapboxNavigationExample/Images.xcassets/user_active_pin.imageset/dest-pin 2.png create mode 100644 example/ios/MapboxNavigationExample/Images.xcassets/user_active_pin.imageset/dest-pin 3.png create mode 100644 example/ios/MapboxNavigationExample/Images.xcassets/user_offline_pin.imageset/Contents.json create mode 100644 example/ios/MapboxNavigationExample/Images.xcassets/user_offline_pin.imageset/dest-pin 1.png create mode 100644 example/ios/MapboxNavigationExample/Images.xcassets/user_offline_pin.imageset/dest-pin 2.png create mode 100644 example/ios/MapboxNavigationExample/Images.xcassets/user_offline_pin.imageset/dest-pin.png create mode 100644 example/src/Participant.ts create mode 100644 example/src/realTimeList.tsx diff --git a/android/src/main/java/com/mapboxnavigation/MapboxNavigationPackage.kt b/android/src/main/java/com/mapboxnavigation/MapboxNavigationPackage.kt index 82fdf46..bc9af4a 100644 --- a/android/src/main/java/com/mapboxnavigation/MapboxNavigationPackage.kt +++ b/android/src/main/java/com/mapboxnavigation/MapboxNavigationPackage.kt @@ -14,6 +14,6 @@ class MapboxNavigationViewPackage : ReactPackage { } override fun createNativeModules(reactContext: ReactApplicationContext): List { - return emptyList() + return listOf(ParticipantsManager(reactContext)) } } diff --git a/android/src/main/java/com/mapboxnavigation/MapboxNavigationView.kt b/android/src/main/java/com/mapboxnavigation/MapboxNavigationView.kt index b65bb47..59e2d9b 100644 --- a/android/src/main/java/com/mapboxnavigation/MapboxNavigationView.kt +++ b/android/src/main/java/com/mapboxnavigation/MapboxNavigationView.kt @@ -1,13 +1,23 @@ package com.mapboxnavigation +import android.animation.TypeEvaluator +import android.animation.ValueAnimator import android.annotation.SuppressLint +import android.content.Context import android.content.res.Configuration import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.drawable.Drawable import android.util.Log import android.view.LayoutInflater import android.view.View +import android.view.animation.LinearInterpolator import android.widget.FrameLayout +import androidx.core.content.ContextCompat import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.UiThreadUtil.runOnUiThread import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.events.RCTEventEmitter import com.mapbox.api.directions.v5.DirectionsCriteria @@ -19,8 +29,15 @@ import com.mapbox.geojson.Point import com.mapbox.maps.CameraOptions import com.mapbox.maps.EdgeInsets import com.mapbox.maps.ImageHolder +import com.mapbox.maps.extension.style.atmosphere.generated.getAtmosphere +import com.mapbox.maps.extension.style.layers.properties.generated.IconPitchAlignment import com.mapbox.maps.plugin.LocationPuck2D import com.mapbox.maps.plugin.animation.camera +import com.mapbox.maps.plugin.annotation.annotations +import com.mapbox.maps.plugin.annotation.generated.PointAnnotation +import com.mapbox.maps.plugin.annotation.generated.PointAnnotationManager +import com.mapbox.maps.plugin.annotation.generated.PointAnnotationOptions +import com.mapbox.maps.plugin.annotation.generated.createPointAnnotationManager import com.mapbox.maps.plugin.locationcomponent.location import com.mapbox.navigation.base.TimeFormat import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions @@ -83,7 +100,8 @@ import com.mapboxnavigation.databinding.NavigationViewBinding import java.util.Locale @SuppressLint("ViewConstructor") -class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout(context.baseContext) { +class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout(context.baseContext), + ParticipantsManager.ParticipantsDelegate { private companion object { private const val BUTTON_ANIMATION_DURATION = 1500L } @@ -96,6 +114,9 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout private var distanceUnit: String = DirectionsCriteria.IMPERIAL private var locale = Locale.getDefault() private var travelMode: String = DirectionsCriteria.PROFILE_DRIVING + private val userAnnotations: MutableMap = mutableMapOf() + private val animators: MutableMap = mutableMapOf() + public var pointAnnotationManager: PointAnnotationManager? = null /** * Bindings to the example layout. @@ -487,6 +508,7 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout .build() ) } + ParticipantsManager.shared?.delegate = this } @SuppressLint("MissingPermission") @@ -624,7 +646,7 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout binding.mapView.location.apply { setLocationProvider(navigationLocationProvider) this.locationPuck = LocationPuck2D( - bearingImage = ImageHolder.Companion.from( + bearingImage = ImageHolder.from( com.mapbox.navigation.ui.maps.R.drawable.mapbox_navigation_puck_icon ) ) @@ -726,12 +748,12 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout mapboxNavigation?.setNavigationRoutes(routes) // show UI elements - binding.soundButton.visibility = View.VISIBLE - binding.routeOverview.visibility = View.VISIBLE - binding.tripProgressCard.visibility = View.VISIBLE + binding.soundButton.visibility = VISIBLE + binding.routeOverview.visibility = VISIBLE + binding.tripProgressCard.visibility = VISIBLE // move the camera to overview when new route is available -// navigationCamera.requestNavigationCameraToOverview() + // navigationCamera.requestNavigationCameraToOverview() mapboxNavigation?.startTripSession(withForegroundService = true) } @@ -764,10 +786,10 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout mapboxNavigation?.setNavigationRoutes(listOf()) // hide UI elements - binding.soundButton.visibility = View.INVISIBLE - binding.maneuverView.visibility = View.INVISIBLE - binding.routeOverview.visibility = View.INVISIBLE - binding.tripProgressCard.visibility = View.INVISIBLE + binding.soundButton.visibility = INVISIBLE + binding.maneuverView.visibility = INVISIBLE + binding.routeOverview.visibility = INVISIBLE + binding.tripProgressCard.visibility = INVISIBLE } private fun sendErrorToReact(error: String?) { @@ -820,7 +842,7 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout } fun setShowCancelButton(show: Boolean) { - binding.stop.visibility = if (show) View.VISIBLE else View.INVISIBLE + binding.stop.visibility = if (show) VISIBLE else INVISIBLE } fun setTravelMode(mode: String) { @@ -832,8 +854,83 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout else -> DirectionsCriteria.PROFILE_DRIVING_TRAFFIC } } - - public void updateMarkers(List> userList) { - // update pins here + override fun participantsDidUpdate(list: List>) { + Log.d("Suleman",list.toString()) + runOnUiThread { + val mapView = binding.mapView ?: return@runOnUiThread + if (pointAnnotationManager == null) { + pointAnnotationManager = mapView.annotations.createPointAnnotationManager().apply { + iconPitchAlignment = IconPitchAlignment.MAP + } + } + val seenIds = mutableSetOf() + val annotations = ArrayList(); + list.forEach { user -> + val id = user["id"] as? String ?: return@forEach + val displayName = user["displayName"] as? String ?: return@forEach + val lat = user["lat"] as? Double ?: return@forEach + val lng = user["lng"] as? Double ?: return@forEach + var imageUrl = user["imageUrl"] as? String ?: "" + + val newPoint = Point.fromLngLat(lng, lat) + seenIds.add(id) + + val existingAnnotation = userAnnotations[id] + + if (existingAnnotation != null) { + // Animate from old position to new + animators[id]?.cancel() + val animator = ValueAnimator.ofObject( + CarEvaluator(), + existingAnnotation.point, + newPoint + ).apply { + duration = 1000L // 1 second + interpolator = LinearInterpolator() + addUpdateListener { valueAnimator -> + val animatedPoint = valueAnimator.animatedValue as Point + existingAnnotation.point = animatedPoint + pointAnnotationManager?.update(listOf(existingAnnotation)) + } + start() + } + animators[id] = animator + + } else { + // New user → create annotation + val annotation = PointAnnotationOptions() + .withPoint(newPoint) + .withIconImage(bitmapFromFile(path = imageUrl)) + .withIconSize(0.2) + .withTextField(displayName) + .let { pointAnnotationManager!!.create(it) } + annotations.add(annotation) + userAnnotations[id] = annotation + } } + pointAnnotationManager?.update(annotations) + } + } + + fun bitmapFromFile(path: String): Bitmap { + return try { + BitmapFactory.decodeFile(path) + } catch (e: Exception) { + return BitmapFactory.decodeResource(context.resources, R.drawable.ic_user_offline) + } + } +} + +private class CarEvaluator : TypeEvaluator { + override fun evaluate( + fraction: Float, + startValue: Point, + endValue: Point + ): Point { + val lat = + startValue.latitude() + (endValue.latitude() - startValue.latitude()) * fraction + val lon = + startValue.longitude() + (endValue.longitude() - startValue.longitude()) * fraction + return Point.fromLngLat(lon, lat) + } } diff --git a/android/src/main/java/com/mapboxnavigation/ParticipantsManager.kt b/android/src/main/java/com/mapboxnavigation/ParticipantsManager.kt index dd005c9..77c705a 100644 --- a/android/src/main/java/com/mapboxnavigation/ParticipantsManager.kt +++ b/android/src/main/java/com/mapboxnavigation/ParticipantsManager.kt @@ -1,9 +1,10 @@ -package com.yourpackage.participants +package com.mapboxnavigation import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.WritableArray import com.facebook.react.modules.core.DeviceEventManagerModule @@ -16,6 +17,10 @@ class ParticipantsManager(private val context: ReactApplicationContext) : private var participants: List> = emptyList() + interface ParticipantsDelegate { + fun participantsDidUpdate(list: List>) + } + var delegate: ParticipantsDelegate? = null init { shared = this } @@ -23,21 +28,20 @@ class ParticipantsManager(private val context: ReactApplicationContext) : override fun getName(): String = "ParticipantsManager" @ReactMethod - fun updateParticipants(list: List>) { - participants = list - - val array: WritableArray = Arguments.createArray() - list.forEach { map -> - val writableMap = Arguments.makeNativeMap(map) - array.pushMap(writableMap) - } - - sendEvent("onParticipantsUpdate", array) - } - - private fun sendEvent(eventName: String, data: Any) { - context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) - .emit(eventName, data) + fun updateParticipants(list: ReadableArray) { + val participants = mutableListOf>() + + for (i in 0 until list.size()) { + val map = list.getMap(i) ?: continue + val convertedMap = map.toHashMap() + participants.add(convertedMap) + } + + // Now you can use 'participants' as List> + this.participants = participants + delegate?.participantsDidUpdate(participants) +// context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) +// .emit("onParticipantsUpdate", list) } fun getParticipants(): List> = participants diff --git a/android/src/main/java/com/mapboxnavigation/ParticipantsPackage.kt b/android/src/main/java/com/mapboxnavigation/ParticipantsPackage.kt deleted file mode 100644 index ebdbe52..0000000 --- a/android/src/main/java/com/mapboxnavigation/ParticipantsPackage.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.yourpackage.participants - -import com.facebook.react.ReactPackage -import com.facebook.react.bridge.NativeModule -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.uimanager.ViewManager - -class ParticipantsPackage : ReactPackage { - override fun createViewManagers( - reactContext: ReactApplicationContext - ): MutableList> = mutableListOf() - - override fun createNativeModules( - reactContext: ReactApplicationContext - ): MutableList { - return mutableListOf(ParticipantsManager(reactContext)) - } -} diff --git a/android/src/main/res/drawable/ic_user_offline.png b/android/src/main/res/drawable/ic_user_offline.png new file mode 100644 index 0000000000000000000000000000000000000000..709d2609b8bc760107fe48f9aea02e270ca43562 GIT binary patch literal 18868 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 LSRequiresIPhoneOS - MBXAccessToken - MapboxToken NSAppTransportSecurity NSAllowsArbitraryLoads diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 3ce2f09..ae5deeb 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -959,7 +959,7 @@ PODS: - React-Mapbuffer (0.74.3): - glog - React-debug - - react-native-mapbox-navigation (0.4.4): + - react-native-mapbox-navigation (0.5.2): - DoubleConversion - glog - hermes-engine @@ -1444,7 +1444,7 @@ SPEC CHECKSUMS: React-jsitracing: 6b3c8c98313642140530f93c46f5a6ca4530b446 React-logger: fa92ba4d3a5d39ac450f59be2a3cec7b099f0304 React-Mapbuffer: 9f68550e7c6839d01411ac8896aea5c868eff63a - react-native-mapbox-navigation: 67c175cb05d646ebe02b77b9a6ae974ab185dba3 + react-native-mapbox-navigation: 6d261acd1d0234b7ca1fc672d94ca23461c2e363 React-nativeconfig: fa5de9d8f4dbd5917358f8ad3ad1e08762f01dcb React-NativeModulesApple: 585d1b78e0597de364d259cb56007052d0bda5e5 React-perflogger: 7bb9ba49435ff66b666e7966ee10082508a203e8 @@ -1471,8 +1471,8 @@ SPEC CHECKSUMS: SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Solar-dev: 4612dc9878b9fed2667d23b327f1d4e54e16e8d0 Turf: aa2ede4298009639d10db36aba1a7ebaad072a5e - Yoga: 04f1db30bb810187397fa4c37dd1868a27af229c + Yoga: 88480008ccacea6301ff7bf58726e27a72931c8d PODFILE CHECKSUM: c745c267888f56367ee22ec85344a16a407b6acb -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/example/src/App.tsx b/example/src/App.tsx index 2d79d7a..8032d87 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,10 +1,33 @@ -import { Button, StyleSheet, Text, View } from 'react-native'; +import { Button, NativeModules, StyleSheet, Text, View } from 'react-native'; import MapboxNavigation from '@pawan-pk/react-native-mapbox-navigation'; -import { useState } from 'react'; - +import { useEffect, useState } from 'react'; +import useRandomUsers from './realTimeList'; +const { ParticipantsManager } = NativeModules; +interface Coordinates { + latitude: number; + longitude: number; +} export default function App() { const [navigating, setNavigating] = useState(false); + const participants = useRandomUsers(); // This changes over time + const startOrigin: Coordinates = { latitude: 28.4212, longitude: 70.2989 }; + const destination: Coordinates = { latitude: 31.5204, longitude: 74.3587 }; + const waypoints: Coordinates[] = []; + const [delay, setDelay] = useState(false); + useEffect(() => { + const timer = setTimeout(() => setDelay(true), 100); + return () => clearTimeout(timer); + }, []); + + // // 🚀 Sync participants with native ParticipantsManager + useEffect(() => { + if (!ParticipantsManager) { + console.error('❌ ParticipantsManager not found on iOS!'); + } else { + ParticipantsManager.updateParticipants(participants); + } + }, [participants]); if (!navigating) { return ( @@ -24,37 +47,18 @@ export default function App() { return ( { setNavigating(false); }} @@ -81,4 +85,8 @@ const styles = StyleSheet.create({ marginBottom: 20, textAlign: 'center', }, + map: { + backgroundColor: 'white', + flex: 1, + }, }); diff --git a/example/src/Participant.ts b/example/src/Participant.ts new file mode 100644 index 0000000..48ee934 --- /dev/null +++ b/example/src/Participant.ts @@ -0,0 +1,12 @@ +export interface Participant { + id: string; + userMail: string; + coverImage: string; + displayName: string; + imageUrl: string; + isBenzifiMember: boolean; + nation: string; + userName: string; + lat: number; // starting latitude + lng: number; // starting longitude +} diff --git a/example/src/realTimeList.tsx b/example/src/realTimeList.tsx new file mode 100644 index 0000000..763b9ae --- /dev/null +++ b/example/src/realTimeList.tsx @@ -0,0 +1,72 @@ +import { useEffect, useState } from 'react'; +import type { Participant } from './Participant'; + +export default function useRandomUsers() { + const startOrigin = { latitude: 28.4212, longitude: 70.2989 }; + const destination = { latitude: 31.5204, longitude: 74.3587 }; + const [participants, setParticipants] = useState([]); + // Generate initial 10 users within start-destination bounds + useEffect(() => { + const users: Participant[] = Array.from({ length: 10 }).map((_, idx) => ({ + id: `user-${idx + 1}`, + userMail: `user${idx + 1}@example.com`, + coverImage: '', + displayName: `User ${idx + 1}`, + imageUrl: `image-${Date.now() + idx}.jpg`, + isBenzifiMember: false, + nation: 'AE', + userName: `user${idx + 1}`, + lat: getRandomInRange(startOrigin.latitude, destination.latitude), + lng: getRandomInRange(startOrigin.longitude, destination.longitude), + })); + setParticipants(users); + }, [ + startOrigin.latitude, + startOrigin.longitude, + destination.latitude, + destination.longitude, + ]); + + // Update positions every 5 seconds + useEffect(() => { + const interval = setInterval(() => { + setParticipants((prev) => + prev.map((user) => ({ + ...user, + lat: clamp( + user.lat + getRandomDelta(), + startOrigin.latitude, + destination.latitude + ), + lng: clamp( + user.lng + getRandomDelta(), + startOrigin.longitude, + destination.longitude + ), + })) + ); + }, 5000); + + return () => clearInterval(interval); + }, [ + startOrigin.latitude, + startOrigin.longitude, + destination.latitude, + destination.longitude, + ]); + return participants; +} +// Helper: Random value in given lat/lng range +function getRandomInRange(min: number, max: number): number { + return Math.random() * (max - min) + min; +} + +// Helper: Small random delta for movement +function getRandomDelta(): number { + return (Math.random() - 0.5) * 0.01; // ~1km variation +} + +// Clamp value to min/max +function clamp(value: number, min: number, max: number): number { + return Math.max(min, Math.min(max, value)); +} diff --git a/ios/MapboxNavigationView.swift b/ios/MapboxNavigationView.swift index 04ac2d8..c29f168 100644 --- a/ios/MapboxNavigationView.swift +++ b/ios/MapboxNavigationView.swift @@ -72,17 +72,13 @@ public class MapboxNavigationView: UIView, NavigationViewControllerDelegate, Par @objc var onArrive: RCTDirectEventBlock? @objc var vehicleMaxHeight: NSNumber? @objc var vehicleMaxWidth: NSNumber? + var pointAnnotationManager: PointAnnotationManager? override init(frame: CGRect) { self.embedded = false self.embedding = false super.init(frame: frame) ParticipantsManager.shared?.delegate = self - - // Initial load - if let participants = ParticipantsManager.shared?.getParticipants() { - updateParticipantsOnMap(participants) - } } required init?(coder aDecoder: NSCoder) { @@ -218,7 +214,29 @@ public class MapboxNavigationView: UIView, NavigationViewControllerDelegate, Par return true; } - private func updateParticipantsOnMap(_ list: [[String: Any]]) { - // Update Mapbox markers here + private func updateParticipantsOnMap(_ list: [[String: Any]]) { + guard let mapView = navViewController?.navigationMapView else { return } + if pointAnnotationManager == nil { + pointAnnotationManager = mapView.mapView.annotations.makePointAnnotationManager() + } + var uannotations: [PointAnnotation] = [] + for user in list { + guard + let id = user["id"] as? String, + let displayName = user["displayName"] as? String, + let lat = user["lat"] as? Double, + let lng = user["lng"] as? Double + else { continue } + let imageUrl = user["imageUrl"] as? String ?? "" + let newCoordinate = CLLocationCoordinate2D(latitude: lat, longitude: lng) + var annotation = PointAnnotation(id: id, coordinate: newCoordinate) + annotation.textField = displayName + annotation.image = .init(image: UIImage(contentsOfFile: imageUrl) ?? UIImage(named: "user_offline_pin")!, name: id) + annotation.iconSize = 0.2 + annotation.userInfo = user + uannotations.append(annotation) } + self.pointAnnotationManager?.annotations.removeAll() + self.pointAnnotationManager?.annotations = uannotations + } } diff --git a/ios/ParticipantsManager.swift b/ios/ParticipantsManager.swift index b5e4981..f564257 100644 --- a/ios/ParticipantsManager.swift +++ b/ios/ParticipantsManager.swift @@ -13,7 +13,7 @@ class ParticipantsManager: RCTEventEmitter { private var hasListeners = false private var participants: [[String: Any]] = [] - weak var delegate: ParticipantsManagerDelegate? // 👈 native delegate + weak var delegate: ParticipantsManagerDelegate? // 👈 native delegate override init() { super.init() @@ -21,9 +21,9 @@ class ParticipantsManager: RCTEventEmitter { print("✅ ParticipantsManager initialized") } - override static func moduleName() -> String! { - return "ParticipantsManager" - } + override static func moduleName() -> String! { + return "ParticipantsManager" + } override static func requiresMainQueueSetup() -> Bool { return true @@ -42,8 +42,8 @@ class ParticipantsManager: RCTEventEmitter { } // Called from JS - @objc(updateParticipants:) - func updateParticipants(_ list: [[String: Any]]) { + @objc(updateParticipants:) + func updateParticipants(_ list: [[String: Any]]) { participants = list // Notify JS listeners if hasListeners { @@ -52,17 +52,8 @@ class ParticipantsManager: RCTEventEmitter { // Notify native listener delegate?.participantsDidUpdate(list) } - -// @objc -// func updateParticipants(_ participants: NSArray) { -// print("📡 iOS ParticipantsManager got \(participants.count) participants") -// for case let dict as NSDictionary in participants { -// print("👉 Participant:", dict) -// } -// } - // Allow MapboxNavigationView to pull latest list func getParticipants() -> [[String: Any]] { return participants } -} \ No newline at end of file +} diff --git a/src/MapboxNavigation.tsx b/src/MapboxNavigation.tsx index 71464ed..bf7110d 100644 --- a/src/MapboxNavigation.tsx +++ b/src/MapboxNavigation.tsx @@ -113,8 +113,13 @@ const MapboxNavigation: React.FC = (props) => { }; const styles = StyleSheet.create({ - mapbox: { flex: 1 }, - message: { textAlign: 'center', fontSize: 16 }, + mapbox: { + flex: 1, + }, + message: { + textAlign: 'center', + fontSize: 16, + }, }); export default MapboxNavigation; From 5362fd5c9feaf90469e2694611379942b0c74223 Mon Sep 17 00:00:00 2001 From: Suleman Ali Date: Sat, 13 Sep 2025 01:17:46 +0500 Subject: [PATCH 12/13] fix: format and lint --- .watchmanconfig | 2 +- .yarnrc.yml | 4 +- CODE_OF_CONDUCT.md | 21 +- .../java/com/mapboxnavigation/LRUCache.kt | 80 +++ .../mapboxnavigation/MapboxNavigationView.kt | 625 ++++++++++-------- .../AppIcon.appiconset/Contents.json | 62 +- .../Images.xcassets/Contents.json | 6 +- .../user_active_pin.imageset/Contents.json | 26 +- .../user_offline_pin.imageset/Contents.json | 26 +- example/src/App.tsx | 21 +- example/src/Points.ts | 32 + example/src/dummyImageURLs.ts | 18 + example/src/realTimeList.tsx | 56 +- ios/LRUCache.swift | 96 +++ ios/MapboxNavigationView.swift | 368 ++++++----- ios/UIImage+extensions.swift | 39 ++ lefthook.yml | 4 +- 17 files changed, 932 insertions(+), 554 deletions(-) create mode 100644 android/src/main/java/com/mapboxnavigation/LRUCache.kt create mode 100644 example/src/Points.ts create mode 100644 example/src/dummyImageURLs.ts create mode 100644 ios/LRUCache.swift create mode 100644 ios/UIImage+extensions.swift diff --git a/.watchmanconfig b/.watchmanconfig index 9e26dfe..0967ef4 100644 --- a/.watchmanconfig +++ b/.watchmanconfig @@ -1 +1 @@ -{} \ No newline at end of file +{} diff --git a/.yarnrc.yml b/.yarnrc.yml index 13215d6..5badb2e 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -3,8 +3,8 @@ nmHoistingLimits: workspaces plugins: - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs - spec: "@yarnpkg/plugin-interactive-tools" + spec: '@yarnpkg/plugin-interactive-tools' - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs - spec: "@yarnpkg/plugin-workspace-tools" + spec: '@yarnpkg/plugin-workspace-tools' yarnPath: .yarn/releases/yarn-3.6.1.cjs diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 45d257b..8b4fcfd 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,4 +1,3 @@ - # Contributor Covenant Code of Conduct ## Our Pledge @@ -18,23 +17,23 @@ diverse, inclusive, and healthy community. Examples of behavior that contributes to a positive environment for our community include: -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the overall +- Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or advances of +- The use of sexualized language or imagery, and sexual attention or advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email address, +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities diff --git a/android/src/main/java/com/mapboxnavigation/LRUCache.kt b/android/src/main/java/com/mapboxnavigation/LRUCache.kt new file mode 100644 index 0000000..217e707 --- /dev/null +++ b/android/src/main/java/com/mapboxnavigation/LRUCache.kt @@ -0,0 +1,80 @@ +class LRUCache(private val capacity: Int) { + + private inner class Node( + val key: K, + var value: V, + var prev: Node? = null, + var next: Node? = null + ) + + private val map = HashMap() + private var head: Node? = null // Most recently used + private var tail: Node? = null // Least recently used + + init { + require(capacity > 0) { "LRUCache capacity must be greater than 0" } + } + + @Synchronized + fun get(key: K): V? { + val node = map[key] ?: return null + moveToHead(node) + return node.value + } + + @Synchronized + fun put(key: K, value: V) { + val node = map[key] + if (node != null) { + node.value = value + moveToHead(node) + } else { + val newNode = Node(key, value) + map[key] = newNode + addToHead(newNode) + + if (map.size > capacity) { + removeTail()?.let { removed -> map.remove(removed.key) } + } + } + } + + // ==== Private helpers ==== + private fun moveToHead(node: Node) { + removeNode(node) + addToHead(node) + } + + private fun addToHead(node: Node) { + node.prev = null + node.next = head + head?.prev = node + head = node + if (tail == null) { + tail = node + } + } + + private fun removeNode(node: Node) { + val prev = node.prev + val next = node.next + + if (prev != null) { + prev.next = next + } else { + head = next + } + + if (next != null) { + next.prev = prev + } else { + tail = prev + } + } + + private fun removeTail(): Node? { + val node = tail ?: return null + removeNode(node) + return node + } +} diff --git a/android/src/main/java/com/mapboxnavigation/MapboxNavigationView.kt b/android/src/main/java/com/mapboxnavigation/MapboxNavigationView.kt index 59e2d9b..d423ea2 100644 --- a/android/src/main/java/com/mapboxnavigation/MapboxNavigationView.kt +++ b/android/src/main/java/com/mapboxnavigation/MapboxNavigationView.kt @@ -1,5 +1,6 @@ package com.mapboxnavigation +import LRUCache import android.animation.TypeEvaluator import android.animation.ValueAnimator import android.annotation.SuppressLint @@ -8,20 +9,23 @@ import android.content.res.Configuration import android.content.res.Resources import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.graphics.BitmapShader import android.graphics.Canvas -import android.graphics.drawable.Drawable +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import android.graphics.Shader +import android.net.Uri import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.animation.LinearInterpolator import android.widget.FrameLayout -import androidx.core.content.ContextCompat import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.UiThreadUtil.runOnUiThread import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.events.RCTEventEmitter import com.mapbox.api.directions.v5.DirectionsCriteria -import com.mapbox.api.directions.v5.models.DirectionsWaypoint import com.mapbox.api.directions.v5.models.RouteOptions import com.mapbox.bindgen.Expected import com.mapbox.common.location.Location @@ -29,7 +33,6 @@ import com.mapbox.geojson.Point import com.mapbox.maps.CameraOptions import com.mapbox.maps.EdgeInsets import com.mapbox.maps.ImageHolder -import com.mapbox.maps.extension.style.atmosphere.generated.getAtmosphere import com.mapbox.maps.extension.style.layers.properties.generated.IconPitchAlignment import com.mapbox.maps.plugin.LocationPuck2D import com.mapbox.maps.plugin.animation.camera @@ -97,11 +100,17 @@ import com.mapbox.navigation.voice.model.SpeechError import com.mapbox.navigation.voice.model.SpeechValue import com.mapbox.navigation.voice.model.SpeechVolume import com.mapboxnavigation.databinding.NavigationViewBinding +import java.io.File +import java.io.FileOutputStream +import java.net.HttpURLConnection +import java.net.URL import java.util.Locale +import kotlin.concurrent.thread +import android.graphics.* @SuppressLint("ViewConstructor") -class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout(context.baseContext), - ParticipantsManager.ParticipantsDelegate { +class MapboxNavigationView(private val context: ThemedReactContext) : + FrameLayout(context.baseContext), ParticipantsManager.ParticipantsDelegate { private companion object { private const val BUTTON_ANIMATION_DURATION = 1500L } @@ -117,26 +126,25 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout private val userAnnotations: MutableMap = mutableMapOf() private val animators: MutableMap = mutableMapOf() public var pointAnnotationManager: PointAnnotationManager? = null + val cacheManager = LRUCache(10000000) - /** - * Bindings to the example layout. - */ - private var binding: NavigationViewBinding = NavigationViewBinding.inflate(LayoutInflater.from(context), this, true) + /** Bindings to the example layout. */ + private var binding: NavigationViewBinding = + NavigationViewBinding.inflate(LayoutInflater.from(context), this, true) /** - * Produces the camera frames based on the location and routing data for the [navigationCamera] to execute. + * Produces the camera frames based on the location and routing data for the [navigationCamera] to + * execute. */ private var viewportDataSource = MapboxNavigationViewportDataSource(binding.mapView.mapboxMap) /** * Used to execute camera transitions based on the data generated by the [viewportDataSource]. - * This includes transitions from route overview to route following and continuously updating the camera as the location changes. + * This includes transitions from route overview to route following and continuously updating the + * camera as the location changes. */ - private var navigationCamera = NavigationCamera( - binding.mapView.mapboxMap, - binding.mapView.camera, - viewportDataSource - ) + private var navigationCamera = + NavigationCamera(binding.mapView.mapboxMap, binding.mapView.camera, viewportDataSource) /** * Mapbox Navigation entry point. There should only be one instance of this object for the app. @@ -150,36 +158,16 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout */ private val pixelDensity = Resources.getSystem().displayMetrics.density private val overviewPadding: EdgeInsets by lazy { - EdgeInsets( - 140.0 * pixelDensity, - 40.0 * pixelDensity, - 120.0 * pixelDensity, - 40.0 * pixelDensity - ) + EdgeInsets(140.0 * pixelDensity, 40.0 * pixelDensity, 120.0 * pixelDensity, 40.0 * pixelDensity) } private val landscapeOverviewPadding: EdgeInsets by lazy { - EdgeInsets( - 30.0 * pixelDensity, - 380.0 * pixelDensity, - 110.0 * pixelDensity, - 20.0 * pixelDensity - ) + EdgeInsets(30.0 * pixelDensity, 380.0 * pixelDensity, 110.0 * pixelDensity, 20.0 * pixelDensity) } private val followingPadding: EdgeInsets by lazy { - EdgeInsets( - 180.0 * pixelDensity, - 40.0 * pixelDensity, - 150.0 * pixelDensity, - 40.0 * pixelDensity - ) + EdgeInsets(180.0 * pixelDensity, 40.0 * pixelDensity, 150.0 * pixelDensity, 40.0 * pixelDensity) } private val landscapeFollowingPadding: EdgeInsets by lazy { - EdgeInsets( - 30.0 * pixelDensity, - 380.0 * pixelDensity, - 110.0 * pixelDensity, - 40.0 * pixelDensity - ) + EdgeInsets(30.0 * pixelDensity, 380.0 * pixelDensity, 110.0 * pixelDensity, 40.0 * pixelDensity) } /** @@ -189,12 +177,14 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout private lateinit var maneuverApi: MapboxManeuverApi /** - * Generates updates for the [MapboxTripProgressView] that include remaining time and distance to the destination. + * Generates updates for the [MapboxTripProgressView] that include remaining time and distance to + * the destination. */ private lateinit var tripProgressApi: MapboxTripProgressApi /** - * Stores and updates the state of whether the voice instructions should be played as they come or muted. + * Stores and updates the state of whether the voice instructions should be played as they come or + * muted. */ private var isVoiceInstructionsMuted = false set(value) { @@ -209,42 +199,36 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout } /** - * Extracts message that should be communicated to the driver about the upcoming maneuver. - * When possible, downloads a synthesized audio file that can be played back to the driver. + * Extracts message that should be communicated to the driver about the upcoming maneuver. When + * possible, downloads a synthesized audio file that can be played back to the driver. */ private lateinit var speechApi: MapboxSpeechApi /** - * Plays the synthesized audio files with upcoming maneuver instructions - * or uses an on-device Text-To-Speech engine to communicate the message to the driver. - * NOTE: do not use lazy initialization for this class since it takes some time to initialize - * the system services required for on-device speech synthesis. With lazy initialization - * there is a high risk that said services will not be available when the first instruction - * has to be played. [MapboxVoiceInstructionsPlayer] should be instantiated in - * `Activity#onCreate`. + * Plays the synthesized audio files with upcoming maneuver instructions or uses an on-device + * Text-To-Speech engine to communicate the message to the driver. NOTE: do not use lazy + * initialization for this class since it takes some time to initialize the system services + * required for on-device speech synthesis. With lazy initialization there is a high risk that + * said services will not be available when the first instruction has to be played. + * [MapboxVoiceInstructionsPlayer] should be instantiated in `Activity#onCreate`. */ private var voiceInstructionsPlayer: MapboxVoiceInstructionsPlayer? = null - /** - * Observes when a new voice instruction should be played. - */ + /** Observes when a new voice instruction should be played. */ private val voiceInstructionsObserver = VoiceInstructionsObserver { voiceInstructions -> speechApi.generate(voiceInstructions, speechCallback) } /** - * Based on whether the synthesized audio file is available, the callback plays the file - * or uses the fall back which is played back using the on-device Text-To-Speech engine. + * Based on whether the synthesized audio file is available, the callback plays the file or uses + * the fall back which is played back using the on-device Text-To-Speech engine. */ private val speechCallback = MapboxNavigationConsumer> { expected -> expected.fold( { error -> // play the instruction via fallback text-to-speech engine - voiceInstructionsPlayer?.play( - error.fallback, - voiceInstructionsPlayerCallback - ) + voiceInstructionsPlayer?.play(error.fallback, voiceInstructionsPlayerCallback) }, { value -> // play the sound file from the external generator @@ -257,7 +241,8 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout } /** - * When a synthesized audio file was downloaded, this callback cleans up the disk after it was played. + * When a synthesized audio file was downloaded, this callback cleans up the disk after it was + * played. */ private val voiceInstructionsPlayerCallback = MapboxNavigationConsumer { value -> @@ -266,25 +251,26 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout } /** - * [NavigationLocationProvider] is a utility class that helps to provide location updates generated by the Navigation SDK - * to the Maps SDK in order to update the user location indicator on the map. + * [NavigationLocationProvider] is a utility class that helps to provide location updates + * generated by the Navigation SDK to the Maps SDK in order to update the user location indicator + * on the map. */ private val navigationLocationProvider = NavigationLocationProvider() /** - * RouteLine: Additional route line options are available through the - * [MapboxRouteLineViewOptions] and [MapboxRouteLineApiOptions]. - * Notice here the [MapboxRouteLineViewOptions.routeLineBelowLayerId] option. The map is made up of layers. In this - * case the route line will be placed below the "road-label" layer which is a good default + * RouteLine: Additional route line options are available through the [MapboxRouteLineViewOptions] + * and [MapboxRouteLineApiOptions]. Notice here the + * [MapboxRouteLineViewOptions.routeLineBelowLayerId] option. The map is made up of layers. In + * this case the route line will be placed below the "road-label" layer which is a good default * for the most common Mapbox navigation related maps. You should consider if this should be * changed for your use case especially if you are using a custom map style. */ private val routeLineViewOptions: MapboxRouteLineViewOptions by lazy { MapboxRouteLineViewOptions.Builder(context) /** - * Route line related colors can be customized via the [RouteLineColorResources]. If using the - * default colors the [RouteLineColorResources] does not need to be set as seen here, the - * defaults will be used internally by the builder. + * Route line related colors can be customized via the [RouteLineColorResources]. If + * using the default colors the [RouteLineColorResources] does not need to be set as + * seen here, the defaults will be used internally by the builder. */ .routeLineColorResources(RouteLineColorResources.Builder().build()) .routeLineBelowLayerId("road-label-navigation") @@ -292,51 +278,40 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout } private val routeLineApiOptions: MapboxRouteLineApiOptions by lazy { - MapboxRouteLineApiOptions.Builder() - .build() + MapboxRouteLineApiOptions.Builder().build() } /** - * RouteLine: This class is responsible for rendering route line related mutations generated - * by the [routeLineApi] + * RouteLine: This class is responsible for rendering route line related mutations generated by + * the [routeLineApi] */ - private val routeLineView by lazy { - MapboxRouteLineView(routeLineViewOptions) - } - + private val routeLineView by lazy { MapboxRouteLineView(routeLineViewOptions) } /** * RouteLine: This class is responsible for generating route line related data which must be * rendered by the [routeLineView] in order to visualize the route line on the map. */ - private val routeLineApi: MapboxRouteLineApi by lazy { - MapboxRouteLineApi(routeLineApiOptions) - } + private val routeLineApi: MapboxRouteLineApi by lazy { MapboxRouteLineApi(routeLineApiOptions) } /** - * RouteArrow: This class is responsible for generating data related to maneuver arrows. The - * data generated must be rendered by the [routeArrowView] in order to apply mutations to - * the map. + * RouteArrow: This class is responsible for generating data related to maneuver arrows. The data + * generated must be rendered by the [routeArrowView] in order to apply mutations to the map. */ - private val routeArrowApi: MapboxRouteArrowApi by lazy { - MapboxRouteArrowApi() - } + private val routeArrowApi: MapboxRouteArrowApi by lazy { MapboxRouteArrowApi() } /** - * RouteArrow: Customization of the maneuver arrow(s) can be done using the - * [RouteArrowOptions]. Here the above layer ID is used to determine where in the map layer - * stack the arrows appear. Above the layer of the route traffic line is being used here. Your - * use case may necessitate adjusting this to a different layer position. + * RouteArrow: Customization of the maneuver arrow(s) can be done using the [RouteArrowOptions]. + * Here the above layer ID is used to determine where in the map layer stack the arrows appear. + * Above the layer of the route traffic line is being used here. Your use case may necessitate + * adjusting this to a different layer position. */ private val routeArrowOptions by lazy { - RouteArrowOptions.Builder(context) - .withAboveLayerId(TOP_LEVEL_ROUTE_LINE_LAYER_ID) - .build() + RouteArrowOptions.Builder(context).withAboveLayerId(TOP_LEVEL_ROUTE_LINE_LAYER_ID).build() } /** - * RouteArrow: This class is responsible for rendering the arrow related mutations generated - * by the [routeArrowApi] + * RouteArrow: This class is responsible for rendering the arrow related mutations generated by + * the [routeArrowApi] */ private val routeArrowView: MapboxRouteArrowView by lazy { MapboxRouteArrowView(routeArrowOptions) @@ -345,53 +320,52 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout /** * Gets notified with location updates. * - * Exposes raw updates coming directly from the location services - * and the updates enhanced by the Navigation SDK (cleaned up and matched to the road). + * Exposes raw updates coming directly from the location services and the updates enhanced by the + * Navigation SDK (cleaned up and matched to the road). */ - private val locationObserver = object : LocationObserver { - var firstLocationUpdateReceived = false + private val locationObserver = + object : LocationObserver { + var firstLocationUpdateReceived = false - override fun onNewRawLocation(rawLocation: Location) { - // not handled - } + override fun onNewRawLocation(rawLocation: Location) { + // not handled + } - override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) { - val enhancedLocation = locationMatcherResult.enhancedLocation - // update location puck's position on the map - navigationLocationProvider.changePosition( - location = enhancedLocation, - keyPoints = locationMatcherResult.keyPoints, - ) + override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) { + val enhancedLocation = locationMatcherResult.enhancedLocation + // update location puck's position on the map + navigationLocationProvider.changePosition( + location = enhancedLocation, + keyPoints = locationMatcherResult.keyPoints, + ) - // update camera position to account for new location - viewportDataSource.onLocationChanged(enhancedLocation) - viewportDataSource.evaluate() + // update camera position to account for new location + viewportDataSource.onLocationChanged(enhancedLocation) + viewportDataSource.evaluate() + + // if this is the first location update the activity has received, + // it's best to immediately move the camera to the current user location + if (!firstLocationUpdateReceived) { + firstLocationUpdateReceived = true + navigationCamera.requestNavigationCameraToOverview( + stateTransitionOptions = + NavigationCameraTransitionOptions.Builder() + .maxDuration(0) // instant transition + .build() + ) + } - // if this is the first location update the activity has received, - // it's best to immediately move the camera to the current user location - if (!firstLocationUpdateReceived) { - firstLocationUpdateReceived = true - navigationCamera.requestNavigationCameraToOverview( - stateTransitionOptions = NavigationCameraTransitionOptions.Builder() - .maxDuration(0) // instant transition - .build() - ) + val event = Arguments.createMap() + event.putDouble("longitude", enhancedLocation.longitude) + event.putDouble("latitude", enhancedLocation.latitude) + event.putDouble("heading", enhancedLocation.bearing ?: 0.0) + event.putDouble("accuracy", enhancedLocation.horizontalAccuracy ?: 0.0) + context.getJSModule(RCTEventEmitter::class.java) + .receiveEvent(id, "onLocationChange", event) } - - val event = Arguments.createMap() - event.putDouble("longitude", enhancedLocation.longitude) - event.putDouble("latitude", enhancedLocation.latitude) - event.putDouble("heading", enhancedLocation.bearing ?: 0.0) - event.putDouble("accuracy", enhancedLocation.horizontalAccuracy ?: 0.0) - context - .getJSModule(RCTEventEmitter::class.java) - .receiveEvent(id, "onLocationChange", event) } - } - /** - * Gets notified with progress along the currently active route. - */ + /** Gets notified with progress along the currently active route. */ private val routeProgressObserver = RouteProgressObserver { routeProgress -> // update the camera position to account for the progressed fragment of the route if (routeProgress.fractionTraveled.toDouble() != 0.0) { @@ -409,28 +383,27 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout // update top banner with maneuver instructions val maneuvers = maneuverApi.getManeuvers(routeProgress) maneuvers.fold( - { error -> - Log.w("Maneuvers error:", error.throwable) - }, + { error -> Log.w("Maneuvers error:", error.throwable) }, { - val maneuverViewOptions = ManeuverViewOptions.Builder() - .primaryManeuverOptions( - ManeuverPrimaryOptions.Builder() - .textAppearance(R.style.PrimaryManeuverTextAppearance) - .build() - ) - .secondaryManeuverOptions( - ManeuverSecondaryOptions.Builder() - .textAppearance(R.style.ManeuverTextAppearance) - .build() - ) - .subManeuverOptions( - ManeuverSubOptions.Builder() - .textAppearance(R.style.ManeuverTextAppearance) - .build() - ) - .stepDistanceTextAppearance(R.style.StepDistanceRemainingAppearance) - .build() + val maneuverViewOptions = + ManeuverViewOptions.Builder() + .primaryManeuverOptions( + ManeuverPrimaryOptions.Builder() + .textAppearance(R.style.PrimaryManeuverTextAppearance) + .build() + ) + .secondaryManeuverOptions( + ManeuverSecondaryOptions.Builder() + .textAppearance(R.style.ManeuverTextAppearance) + .build() + ) + .subManeuverOptions( + ManeuverSubOptions.Builder() + .textAppearance(R.style.ManeuverTextAppearance) + .build() + ) + .stepDistanceTextAppearance(R.style.StepDistanceRemainingAppearance) + .build() binding.maneuverView.visibility = View.VISIBLE binding.maneuverView.updateManeuverViewOptions(maneuverViewOptions) @@ -439,17 +412,14 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout ) // update bottom trip progress summary - binding.tripProgressView.render( - tripProgressApi.getTripProgress(routeProgress) - ) + binding.tripProgressView.render(tripProgressApi.getTripProgress(routeProgress)) val event = Arguments.createMap() event.putDouble("distanceTraveled", routeProgress.distanceTraveled.toDouble()) event.putDouble("durationRemaining", routeProgress.durationRemaining) event.putDouble("fractionTraveled", routeProgress.fractionTraveled.toDouble()) event.putDouble("distanceRemaining", routeProgress.distanceRemaining.toDouble()) - context - .getJSModule(RCTEventEmitter::class.java) + context.getJSModule(RCTEventEmitter::class.java) .receiveEvent(id, "onRouteProgressChange", event) } @@ -458,18 +428,15 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout * * A change can mean: * - routes get changed with [MapboxNavigation.setNavigationRoutes] - * - routes annotations get refreshed (for example, congestion annotation that indicate the live traffic along the route) + * - routes annotations get refreshed (for example, congestion annotation that indicate the live + * traffic along the route) * - driver got off route and a reroute was executed */ private val routesObserver = RoutesObserver { routeUpdateResult -> if (routeUpdateResult.navigationRoutes.isNotEmpty()) { // generate route geometries asynchronously and render them - routeLineApi.setNavigationRoutes( - routeUpdateResult.navigationRoutes - ) { value -> - binding.mapView.mapboxMap.style?.apply { - routeLineView.renderRouteDrawData(this, value) - } + routeLineApi.setNavigationRoutes(routeUpdateResult.navigationRoutes) { value -> + binding.mapView.mapboxMap.style?.apply { routeLineView.renderRouteDrawData(this, value) } } // update the camera position to account for the new route @@ -480,10 +447,7 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout val style = binding.mapView.mapboxMap.style if (style != null) { routeLineApi.clearRouteLine { value -> - routeLineView.renderClearRouteLineValue( - style, - value - ) + routeLineView.renderClearRouteLineValue(style, value) } routeArrowView.render(style, routeArrowApi.clearArrows()) } @@ -500,14 +464,12 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout private fun onCreate() { // initialize Mapbox Navigation - mapboxNavigation = if (MapboxNavigationProvider.isCreated()) { - MapboxNavigationProvider.retrieve() - } else { - MapboxNavigationProvider.create( - NavigationOptions.Builder(context) - .build() - ) - } + mapboxNavigation = + if (MapboxNavigationProvider.isCreated()) { + MapboxNavigationProvider.retrieve() + } else { + MapboxNavigationProvider.create(NavigationOptions.Builder(context).build()) + } ParticipantsManager.shared?.delegate = this } @@ -519,10 +481,7 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout } // Recenter Camera - val initialCameraOptions = CameraOptions.Builder() - .zoom(14.0) - .center(origin) - .build() + val initialCameraOptions = CameraOptions.Builder().zoom(14.0).center(origin).build() binding.mapView.mapboxMap.setCamera(initialCameraOptions) // Start Navigation @@ -536,8 +495,8 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout navigationCamera.registerNavigationCameraStateChangeObserver { navigationCameraState -> // shows/hide the recenter button depending on the camera state when (navigationCameraState) { - NavigationCameraState.TRANSITION_TO_FOLLOWING, - NavigationCameraState.FOLLOWING -> binding.recenter.visibility = View.INVISIBLE + NavigationCameraState.TRANSITION_TO_FOLLOWING, NavigationCameraState.FOLLOWING -> + binding.recenter.visibility = View.INVISIBLE NavigationCameraState.TRANSITION_TO_OVERVIEW, NavigationCameraState.OVERVIEW, @@ -558,41 +517,32 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout // make sure to use the same DistanceFormatterOptions across different features val unitType = if (distanceUnit == "imperial") UnitType.IMPERIAL else UnitType.METRIC - val distanceFormatterOptions = DistanceFormatterOptions.Builder(context) - .unitType(unitType) - .build() + val distanceFormatterOptions = + DistanceFormatterOptions.Builder(context).unitType(unitType).build() // initialize maneuver api that feeds the data to the top banner maneuver view - maneuverApi = MapboxManeuverApi( - MapboxDistanceFormatter(distanceFormatterOptions) - ) + maneuverApi = MapboxManeuverApi(MapboxDistanceFormatter(distanceFormatterOptions)) // initialize bottom progress view - tripProgressApi = MapboxTripProgressApi( - TripProgressUpdateFormatter.Builder(context) - .distanceRemainingFormatter( - DistanceRemainingFormatter(distanceFormatterOptions) - ) - .timeRemainingFormatter( - TimeRemainingFormatter(context) - ) - .percentRouteTraveledFormatter( - PercentDistanceTraveledFormatter() - ) - .estimatedTimeToArrivalFormatter( - EstimatedTimeToArrivalFormatter(context, TimeFormat.NONE_SPECIFIED) - ) - .build() - ) + tripProgressApi = + MapboxTripProgressApi( + TripProgressUpdateFormatter.Builder(context) + .distanceRemainingFormatter( + DistanceRemainingFormatter(distanceFormatterOptions) + ) + .timeRemainingFormatter(TimeRemainingFormatter(context)) + .percentRouteTraveledFormatter(PercentDistanceTraveledFormatter()) + .estimatedTimeToArrivalFormatter( + EstimatedTimeToArrivalFormatter( + context, + TimeFormat.NONE_SPECIFIED + ) + ) + .build() + ) // initialize voice instructions api and the voice instruction player - speechApi = MapboxSpeechApi( - context, - locale.language - ) - voiceInstructionsPlayer = MapboxVoiceInstructionsPlayer( - context, - locale.language - ) + speechApi = MapboxSpeechApi(context, locale.language) + voiceInstructionsPlayer = MapboxVoiceInstructionsPlayer(context, locale.language) // load map style binding.mapView.mapboxMap.loadStyle(NavigationStyles.NAVIGATION_DAY_STYLE) { @@ -604,9 +554,7 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout binding.stop.setOnClickListener { val event = Arguments.createMap() event.putString("message", "Navigation Cancel") - context - .getJSModule(RCTEventEmitter::class.java) - .receiveEvent(id, "onCancelNavigation", event) + context.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "onCancelNavigation", event) } binding.recenter.setOnClickListener { @@ -645,11 +593,19 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout // initialize location puck binding.mapView.location.apply { setLocationProvider(navigationLocationProvider) - this.locationPuck = LocationPuck2D( - bearingImage = ImageHolder.from( - com.mapbox.navigation.ui.maps.R.drawable.mapbox_navigation_puck_icon + this.locationPuck = + LocationPuck2D( + bearingImage = + ImageHolder.from( + com.mapbox + .navigation + .ui + .maps + .R + .drawable + .mapbox_navigation_puck_icon + ) ) - ) puckBearingEnabled = true enabled = true } @@ -657,20 +613,21 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout startRoute() } - private val arrivalObserver = object : ArrivalObserver { + private val arrivalObserver = + object : ArrivalObserver { - override fun onWaypointArrival(routeProgress: RouteProgress) { - onArrival(routeProgress) - } + override fun onWaypointArrival(routeProgress: RouteProgress) { + onArrival(routeProgress) + } - override fun onNextRouteLegStart(routeLegProgress: RouteLegProgress) { - // do something when the user starts a new leg - } + override fun onNextRouteLegStart(routeLegProgress: RouteLegProgress) { + // do something when the user starts a new leg + } - override fun onFinalDestinationArrival(routeProgress: RouteProgress) { - onArrival(routeProgress) + override fun onFinalDestinationArrival(routeProgress: RouteProgress) { + onArrival(routeProgress) + } } - } private fun onArrival(routeProgress: RouteProgress) { val leg = routeProgress.currentLegProgress @@ -679,9 +636,7 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout event.putInt("index", leg.legIndex) event.putDouble("latitude", leg.legDestination?.location?.latitude() ?: 0.0) event.putDouble("longitude", leg.legDestination?.location?.longitude() ?: 0.0) - context - .getJSModule(RCTEventEmitter::class.java) - .receiveEvent(id, "onArrive", event) + context.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "onArrive", event) } } @@ -723,7 +678,10 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout .profile(travelMode) .build(), object : NavigationRouterCallback { - override fun onCanceled(routeOptions: RouteOptions, @RouterOrigin routerOrigin: String) { + override fun onCanceled( + routeOptions: RouteOptions, + @RouterOrigin routerOrigin: String + ) { // no implementation } @@ -795,9 +753,7 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout private fun sendErrorToReact(error: String?) { val event = Arguments.createMap() event.putString("error", error) - context - .getJSModule(RCTEventEmitter::class.java) - .receiveEvent(id, "onError", event) + context.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "onError", event) } fun onDropViewInstance() { @@ -846,66 +802,83 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout } fun setTravelMode(mode: String) { - travelMode = when (mode.lowercase()) { + travelMode = + when (mode.lowercase()) { "walking" -> DirectionsCriteria.PROFILE_WALKING "cycling" -> DirectionsCriteria.PROFILE_CYCLING "driving" -> DirectionsCriteria.PROFILE_DRIVING "driving-traffic" -> DirectionsCriteria.PROFILE_DRIVING_TRAFFIC else -> DirectionsCriteria.PROFILE_DRIVING_TRAFFIC - } + } } + override fun participantsDidUpdate(list: List>) { - Log.d("Suleman",list.toString()) runOnUiThread { val mapView = binding.mapView ?: return@runOnUiThread if (pointAnnotationManager == null) { - pointAnnotationManager = mapView.annotations.createPointAnnotationManager().apply { - iconPitchAlignment = IconPitchAlignment.MAP - } + pointAnnotationManager = + mapView.annotations.createPointAnnotationManager().apply { + iconPitchAlignment = IconPitchAlignment.MAP + } } val seenIds = mutableSetOf() - val annotations = ArrayList(); + val annotations = ArrayList() list.forEach { user -> val id = user["id"] as? String ?: return@forEach val displayName = user["displayName"] as? String ?: return@forEach val lat = user["lat"] as? Double ?: return@forEach val lng = user["lng"] as? Double ?: return@forEach - var imageUrl = user["imageUrl"] as? String ?: "" - + val imageUrl = user["imageUrl"] as? String ?: "" + var bitmap: Bitmap? = null val newPoint = Point.fromLngLat(lng, lat) + + val imageUrlLocal = cacheManager.get(id) + if (imageUrlLocal == null) { + downloadImageToTemp(context, imageUrl, id) { response -> + val result = response.getOrNull() + if (result == null) { + Log.e("ImageLoader", "❌ Failed to download:") + } else if (result != null) { + // ✅ Cache it + cacheManager.put(id, result.second); + bitmap = result.first; + } + } + } else { + val ouputBitmap = BitmapFactory.decodeFile(imageUrlLocal.path) + bitmap = ouputBitmap + } + seenIds.add(id) val existingAnnotation = userAnnotations[id] - - if (existingAnnotation != null) { + if (bitmap != null) { + if (existingAnnotation != null) { // Animate from old position to new animators[id]?.cancel() - val animator = ValueAnimator.ofObject( - CarEvaluator(), - existingAnnotation.point, - newPoint - ).apply { - duration = 1000L // 1 second - interpolator = LinearInterpolator() - addUpdateListener { valueAnimator -> - val animatedPoint = valueAnimator.animatedValue as Point - existingAnnotation.point = animatedPoint - pointAnnotationManager?.update(listOf(existingAnnotation)) + val animator = + ValueAnimator.ofObject(CarEvaluator(), existingAnnotation.point, newPoint).apply { + duration = 1000L // 1 second + interpolator = LinearInterpolator() + addUpdateListener { valueAnimator -> + val animatedPoint = valueAnimator.animatedValue as Point + existingAnnotation.point = animatedPoint + pointAnnotationManager?.update(listOf(existingAnnotation)) + } + start() } - start() - } animators[id] = animator - - } else { - // New user → create annotation - val annotation = PointAnnotationOptions() - .withPoint(newPoint) - .withIconImage(bitmapFromFile(path = imageUrl)) - .withIconSize(0.2) - .withTextField(displayName) - .let { pointAnnotationManager!!.create(it) } - annotations.add(annotation) - userAnnotations[id] = annotation + } else { + // New user → create annotation + val annotation = + PointAnnotationOptions() + .withPoint(newPoint) + .withIconImage(bitmap!!) + + .let { pointAnnotationManager!!.create(it) } + annotations.add(annotation) + userAnnotations[id] = annotation + } } } pointAnnotationManager?.update(annotations) @@ -916,21 +889,101 @@ class MapboxNavigationView(private val context: ThemedReactContext): FrameLayout return try { BitmapFactory.decodeFile(path) } catch (e: Exception) { - return BitmapFactory.decodeResource(context.resources, R.drawable.ic_user_offline) + return BitmapFactory.decodeResource(context.resources, R.drawable.ic_user_offline) + } + } + + + fun downloadImageToTemp( + context: Context, + urlString: String, + userId: String, + completion: (Result>) -> Unit + ) { + val tempDir = context.cacheDir + val file = File(tempDir, "$userId.jpg") + + // If already exists, return immediately + if (file.exists()) { + val bitmap = BitmapFactory.decodeFile(file.absolutePath) + completion(Result.success(Pair(bitmap, Uri.fromFile(file)))) + return + } + + thread { + try { + val url = URL(urlString) + val connection = url.openConnection() as HttpURLConnection + connection.doInput = true + connection.connect() + + val inputStream = connection.inputStream + val originalBitmap = BitmapFactory.decodeStream(inputStream) + + if (originalBitmap == null) { + completion(Result.failure(Exception("Invalid image data"))) + return@thread + } + + // Apply rounded corners + border (like Swift version) + val rounded = + originalBitmap.withRounded(borderWidth = 5f, borderColor = Color.WHITE) + + // Save bitmap to temp file + FileOutputStream(file).use { out -> + rounded.compress(Bitmap.CompressFormat.PNG, 100, out) + } + + completion(Result.success(Pair(rounded, Uri.fromFile(file)))) + + } catch (e: Exception) { + completion(Result.failure(e)) + } + } + } + + fun Bitmap.withRounded( + borderWidth: Float = 5f, + borderColor: Int = Color.WHITE, + targetSize: Int = 180 + ): Bitmap { + val output = Bitmap.createBitmap(targetSize, targetSize, Bitmap.Config.ARGB_8888) + val canvas = Canvas(output) + val paint = Paint(Paint.ANTI_ALIAS_FLAG) + + // Step 1: Crop the bitmap to a square (centered) + val squareSize = minOf(width, height) + val xOffset = (width - squareSize) / 2 + val yOffset = (height - squareSize) / 2 + val squareBitmap = Bitmap.createBitmap(this, xOffset, yOffset, squareSize, squareSize) + + // Step 2: Scale the square bitmap to target size + val scaledBitmap = Bitmap.createScaledBitmap(squareBitmap, targetSize, targetSize, true) + + // Step 3: Draw circular image using BitmapShader + val shader = BitmapShader(scaledBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) + paint.shader = shader + val radius = targetSize / 2f + canvas.drawCircle(radius, radius, radius - borderWidth, paint) + + // Step 4: Draw border + if (borderWidth > 0) { + val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.STROKE + color = borderColor + strokeWidth = borderWidth + } + canvas.drawCircle(radius, radius, radius - borderWidth , borderPaint) } + + return output } } private class CarEvaluator : TypeEvaluator { - override fun evaluate( - fraction: Float, - startValue: Point, - endValue: Point - ): Point { - val lat = - startValue.latitude() + (endValue.latitude() - startValue.latitude()) * fraction - val lon = - startValue.longitude() + (endValue.longitude() - startValue.longitude()) * fraction + override fun evaluate(fraction: Float, startValue: Point, endValue: Point): Point { + val lat = startValue.latitude() + (endValue.latitude() - startValue.latitude()) * fraction + val lon = startValue.longitude() + (endValue.longitude() - startValue.longitude()) * fraction return Point.fromLngLat(lon, lat) } } diff --git a/example/ios/MapboxNavigationExample/Images.xcassets/AppIcon.appiconset/Contents.json b/example/ios/MapboxNavigationExample/Images.xcassets/AppIcon.appiconset/Contents.json index 8121323..ddd7fca 100644 --- a/example/ios/MapboxNavigationExample/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/example/ios/MapboxNavigationExample/Images.xcassets/AppIcon.appiconset/Contents.json @@ -1,53 +1,53 @@ { - "images" : [ + "images": [ { - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" + "idiom": "iphone", + "scale": "2x", + "size": "20x20" }, { - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" + "idiom": "iphone", + "scale": "3x", + "size": "20x20" }, { - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" + "idiom": "iphone", + "scale": "2x", + "size": "29x29" }, { - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" + "idiom": "iphone", + "scale": "3x", + "size": "29x29" }, { - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" + "idiom": "iphone", + "scale": "2x", + "size": "40x40" }, { - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" + "idiom": "iphone", + "scale": "3x", + "size": "40x40" }, { - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" + "idiom": "iphone", + "scale": "2x", + "size": "60x60" }, { - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" + "idiom": "iphone", + "scale": "3x", + "size": "60x60" }, { - "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" + "idiom": "ios-marketing", + "scale": "1x", + "size": "1024x1024" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/example/ios/MapboxNavigationExample/Images.xcassets/Contents.json b/example/ios/MapboxNavigationExample/Images.xcassets/Contents.json index 73c0059..74d6a72 100644 --- a/example/ios/MapboxNavigationExample/Images.xcassets/Contents.json +++ b/example/ios/MapboxNavigationExample/Images.xcassets/Contents.json @@ -1,6 +1,6 @@ { - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/example/ios/MapboxNavigationExample/Images.xcassets/user_active_pin.imageset/Contents.json b/example/ios/MapboxNavigationExample/Images.xcassets/user_active_pin.imageset/Contents.json index f2eb70f..ddb2d3a 100644 --- a/example/ios/MapboxNavigationExample/Images.xcassets/user_active_pin.imageset/Contents.json +++ b/example/ios/MapboxNavigationExample/Images.xcassets/user_active_pin.imageset/Contents.json @@ -1,23 +1,23 @@ { - "images" : [ + "images": [ { - "filename" : "dest-pin 3.png", - "idiom" : "universal", - "scale" : "1x" + "filename": "dest-pin 3.png", + "idiom": "universal", + "scale": "1x" }, { - "filename" : "dest-pin 1.png", - "idiom" : "universal", - "scale" : "2x" + "filename": "dest-pin 1.png", + "idiom": "universal", + "scale": "2x" }, { - "filename" : "dest-pin 2.png", - "idiom" : "universal", - "scale" : "3x" + "filename": "dest-pin 2.png", + "idiom": "universal", + "scale": "3x" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/example/ios/MapboxNavigationExample/Images.xcassets/user_offline_pin.imageset/Contents.json b/example/ios/MapboxNavigationExample/Images.xcassets/user_offline_pin.imageset/Contents.json index e6a7e4e..80385c3 100644 --- a/example/ios/MapboxNavigationExample/Images.xcassets/user_offline_pin.imageset/Contents.json +++ b/example/ios/MapboxNavigationExample/Images.xcassets/user_offline_pin.imageset/Contents.json @@ -1,23 +1,23 @@ { - "images" : [ + "images": [ { - "filename" : "dest-pin.png", - "idiom" : "universal", - "scale" : "1x" + "filename": "dest-pin.png", + "idiom": "universal", + "scale": "1x" }, { - "filename" : "dest-pin 1.png", - "idiom" : "universal", - "scale" : "2x" + "filename": "dest-pin 1.png", + "idiom": "universal", + "scale": "2x" }, { - "filename" : "dest-pin 2.png", - "idiom" : "universal", - "scale" : "3x" + "filename": "dest-pin 2.png", + "idiom": "universal", + "scale": "3x" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/example/src/App.tsx b/example/src/App.tsx index 8032d87..d76f440 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,7 +1,7 @@ import { Button, NativeModules, StyleSheet, Text, View } from 'react-native'; import MapboxNavigation from '@pawan-pk/react-native-mapbox-navigation'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import useRandomUsers from './realTimeList'; const { ParticipantsManager } = NativeModules; interface Coordinates { @@ -15,17 +15,25 @@ export default function App() { const destination: Coordinates = { latitude: 31.5204, longitude: 74.3587 }; const waypoints: Coordinates[] = []; const [delay, setDelay] = useState(false); + // 🆕 keep track of last synced participants + const lastParticipantsRef = useRef(null); + useEffect(() => { const timer = setTimeout(() => setDelay(true), 100); return () => clearTimeout(timer); }, []); - - // // 🚀 Sync participants with native ParticipantsManager useEffect(() => { if (!ParticipantsManager) { - console.error('❌ ParticipantsManager not found on iOS!'); - } else { + console.error('❌ ParticipantsManager not found'); + return; + } + const serialized = JSON.stringify(participants); + if (serialized !== lastParticipantsRef.current) { + lastParticipantsRef.current = serialized; ParticipantsManager.updateParticipants(participants); + console.log('✅ Updated participants:', participants.length); + } else { + console.log('⚡ Skipped duplicate participants update'); } }, [participants]); @@ -68,6 +76,9 @@ export default function App() { onError={(error) => { console.log('onError', error); }} + onLocationChange={(location) => { + console.log('onLocationChange', location); + }} /> ); } diff --git a/example/src/Points.ts b/example/src/Points.ts new file mode 100644 index 0000000..86be340 --- /dev/null +++ b/example/src/Points.ts @@ -0,0 +1,32 @@ +export const Points: [number, number][] = [ + [70.298939, 28.421246], + [70.298792, 28.421181], + [70.295615, 28.422413], + [70.265977, 28.539847], + [70.265643, 28.539444], + [70.266367, 28.540479], + [70.259943, 28.577665], + [70.237826, 28.637858], + [70.236305, 28.638427], + [71.197527, 29.363616], + [71.334668, 29.624111], + [71.401108, 29.891232], + [71.37731, 30.066991], + [71.829678, 30.254591], + [72.124639, 30.64367], + [73.364253, 31.185353], + [73.676193, 31.370425], + [74.224858, 31.571651], + [74.239728, 31.559351], + [74.260817, 31.541392], + [74.264865, 31.538964], + [74.287482, 31.531906], + [74.28379, 31.52611], + [74.316776, 31.500769], + [74.326386, 31.502964], + [74.34759, 31.523117], + [74.347042, 31.521591], + [74.351174, 31.520956], + [74.355008, 31.523286], + [74.358707, 31.520403], +]; diff --git a/example/src/dummyImageURLs.ts b/example/src/dummyImageURLs.ts new file mode 100644 index 0000000..32387ca --- /dev/null +++ b/example/src/dummyImageURLs.ts @@ -0,0 +1,18 @@ +// Generate initial 10 users within start-destination bounds +export const dummyImageURLs: string[] = [ + 'https://picsum.photos/id/237/400/300', // Dog + 'https://picsum.photos/id/1003/400/300', // Mountains + 'https://picsum.photos/id/1025/400/300', // Parrot + 'https://picsum.photos/id/1069/400/300', // Bridge + 'https://picsum.photos/id/1074/400/300', // Forest + 'https://picsum.photos/id/1084/400/300', // Sea horizon + 'https://picsum.photos/id/1080/400/300', // Beach + 'https://picsum.photos/id/1081/400/300', // Rock formations + 'https://picsum.photos/id/1082/400/300', // River + 'https://picsum.photos/id/1083/400/300', // Desert + 'https://picsum.photos/id/1085/400/300', // Foggy forest + 'https://picsum.photos/id/1089/400/300', // Sunset + 'https://picsum.photos/id/109/400/300', // Old building + 'https://picsum.photos/id/111/400/300', // Trees + 'https://picsum.photos/id/112/400/300', // Skyline +]; diff --git a/example/src/realTimeList.tsx b/example/src/realTimeList.tsx index 763b9ae..5a10191 100644 --- a/example/src/realTimeList.tsx +++ b/example/src/realTimeList.tsx @@ -1,23 +1,24 @@ import { useEffect, useState } from 'react'; import type { Participant } from './Participant'; - +import { dummyImageURLs } from './dummyImageURLs'; +import { Points } from './Points'; export default function useRandomUsers() { const startOrigin = { latitude: 28.4212, longitude: 70.2989 }; const destination = { latitude: 31.5204, longitude: 74.3587 }; const [participants, setParticipants] = useState([]); - // Generate initial 10 users within start-destination bounds + useEffect(() => { const users: Participant[] = Array.from({ length: 10 }).map((_, idx) => ({ id: `user-${idx + 1}`, userMail: `user${idx + 1}@example.com`, coverImage: '', displayName: `User ${idx + 1}`, - imageUrl: `image-${Date.now() + idx}.jpg`, + imageUrl: dummyImageURLs[idx] ?? 'https://picsum.photos/id/112/400/300', isBenzifiMember: false, nation: 'AE', userName: `user${idx + 1}`, - lat: getRandomInRange(startOrigin.latitude, destination.latitude), - lng: getRandomInRange(startOrigin.longitude, destination.longitude), + lat: startOrigin.latitude, + lng: startOrigin.longitude, })); setParticipants(users); }, [ @@ -27,26 +28,27 @@ export default function useRandomUsers() { destination.longitude, ]); - // Update positions every 5 seconds + // Track user index along the path useEffect(() => { const interval = setInterval(() => { setParticipants((prev) => - prev.map((user) => ({ - ...user, - lat: clamp( - user.lat + getRandomDelta(), - startOrigin.latitude, - destination.latitude - ), - lng: clamp( - user.lng + getRandomDelta(), - startOrigin.longitude, - destination.longitude - ), - })) + prev.map((user, idx) => { + // Each user has an implicit "path index" + // Example: user-1 => pathIndex = tick + 0, user-2 => tick + 1 + const currentTick = Date.now() / 5000; // every 5 sec + const step = Math.floor(currentTick) + idx; + + const pathIndex = step % Points.length; // loop around + const [lng, lat] = Points[pathIndex]!; + + return { + ...user, + lat, + lng, + }; + }) ); }, 5000); - return () => clearInterval(interval); }, [ startOrigin.latitude, @@ -56,17 +58,3 @@ export default function useRandomUsers() { ]); return participants; } -// Helper: Random value in given lat/lng range -function getRandomInRange(min: number, max: number): number { - return Math.random() * (max - min) + min; -} - -// Helper: Small random delta for movement -function getRandomDelta(): number { - return (Math.random() - 0.5) * 0.01; // ~1km variation -} - -// Clamp value to min/max -function clamp(value: number, min: number, max: number): number { - return Math.max(min, Math.min(max, value)); -} diff --git a/ios/LRUCache.swift b/ios/LRUCache.swift new file mode 100644 index 0000000..aef2fb9 --- /dev/null +++ b/ios/LRUCache.swift @@ -0,0 +1,96 @@ +import Foundation + +final class LRUCache { + + private class Node { + let key: Key + var value: Value + var prev: Node? + var next: Node? + + init(key: Key, value: Value) { + self.key = key + self.value = value + } + } + + private let capacity: Int + private var dict: [Key: Node] = [:] + private var head: Node? // Most recently used + private var tail: Node? // Least recently used + private let lock = NSLock() + + init(capacity: Int) { + precondition(capacity > 0, "LRUCache capacity must be greater than 0") + self.capacity = capacity + } + + func get(_ key: Key) -> Value? { + lock.lock() + defer { lock.unlock() } + + guard let node = dict[key] else { return nil } + moveToHead(node) + return node.value + } + + func put(_ key: Key, value: Value) { + lock.lock() + defer { lock.unlock() } + + if let node = dict[key] { + node.value = value + moveToHead(node) + } else { + let newNode = Node(key: key, value: value) + dict[key] = newNode + addToHead(newNode) + + if dict.count > capacity { + if let tail = removeTail() { + dict.removeValue(forKey: tail.key) + } + } + } + } + + // MARK: - Private helpers + + private func moveToHead(_ node: Node) { + removeNode(node) + addToHead(node) + } + + private func addToHead(_ node: Node) { + node.prev = nil + node.next = head + head?.prev = node + head = node + if tail == nil { + tail = node + } + } + + private func removeNode(_ node: Node) { + let prev = node.prev + let next = node.next + + if let prev = prev { + prev.next = next + } else { + head = next + } + + if let next = next { + next.prev = prev + } else { + tail = prev + } + } + + private func removeTail() -> Node? { + guard let tail = tail else { return nil } + removeNode(tail) + return tail + } +} \ No newline at end of file diff --git a/ios/MapboxNavigationView.swift b/ios/MapboxNavigationView.swift index c29f168..f682878 100644 --- a/ios/MapboxNavigationView.swift +++ b/ios/MapboxNavigationView.swift @@ -28,191 +28,191 @@ public protocol MapboxCarPlayNavigationDelegate { } public class MapboxNavigationView: UIView, NavigationViewControllerDelegate, ParticipantsManagerDelegate { - public weak var navViewController: NavigationViewController? - public var indexedRouteResponse: IndexedRouteResponse? + public weak var navViewController: NavigationViewController? + public var indexedRouteResponse: IndexedRouteResponse? - var embedded: Bool - var embedding: Bool + var embedded: Bool + var embedding: Bool - @objc public var startOrigin: NSArray = [] { - didSet { setNeedsLayout() } - } + @objc public var startOrigin: NSArray = [] { + didSet { setNeedsLayout() } + } - var waypoints: [Waypoint] = [] { - didSet { setNeedsLayout() } - } + var waypoints: [Waypoint] = [] { + didSet { setNeedsLayout() } + } - func setWaypoints(waypoints: [MapboxWaypoint]) { - self.waypoints = waypoints.enumerated().map { (index, waypointData) in - let name = waypointData.name as? String ?? "\(index)" - let waypoint = Waypoint(coordinate: waypointData.coordinate, name: name) - waypoint.separatesLegs = waypointData.separatesLegs - return waypoint - } + func setWaypoints(waypoints: [MapboxWaypoint]) { + self.waypoints = waypoints.enumerated().map { (index, waypointData) in + let name = waypointData.name as? String ?? "\(index)" + let waypoint = Waypoint(coordinate: waypointData.coordinate, name: name) + waypoint.separatesLegs = waypointData.separatesLegs + return waypoint } + } - @objc var destination: NSArray = [] { - didSet { setNeedsLayout() } - } + @objc var destination: NSArray = [] { + didSet { setNeedsLayout() } + } - @objc var shouldSimulateRoute: Bool = false - @objc var showsEndOfRouteFeedback: Bool = false - @objc var showCancelButton: Bool = false - @objc var hideStatusView: Bool = false - @objc var mute: Bool = false - @objc var distanceUnit: NSString = "imperial" - @objc var language: NSString = "us" - @objc var destinationTitle: NSString = "Destination" - @objc var travelMode: NSString = "driving-traffic" - - @objc var onLocationChange: RCTDirectEventBlock? - @objc var onRouteProgressChange: RCTDirectEventBlock? - @objc var onError: RCTDirectEventBlock? - @objc var onCancelNavigation: RCTDirectEventBlock? - @objc var onArrive: RCTDirectEventBlock? - @objc var vehicleMaxHeight: NSNumber? - @objc var vehicleMaxWidth: NSNumber? - var pointAnnotationManager: PointAnnotationManager? - - override init(frame: CGRect) { - self.embedded = false - self.embedding = false - super.init(frame: frame) - ParticipantsManager.shared?.delegate = self - } + @objc var shouldSimulateRoute: Bool = false + @objc var showsEndOfRouteFeedback: Bool = false + @objc var showCancelButton: Bool = false + @objc var hideStatusView: Bool = false + @objc var mute: Bool = false + @objc var distanceUnit: NSString = "imperial" + @objc var language: NSString = "us" + @objc var destinationTitle: NSString = "Destination" + @objc var travelMode: NSString = "driving-traffic" + + @objc var onLocationChange: RCTDirectEventBlock? + @objc var onRouteProgressChange: RCTDirectEventBlock? + @objc var onError: RCTDirectEventBlock? + @objc var onCancelNavigation: RCTDirectEventBlock? + @objc var onArrive: RCTDirectEventBlock? + @objc var vehicleMaxHeight: NSNumber? + @objc var vehicleMaxWidth: NSNumber? + var pointAnnotationManager: PointAnnotationManager? + var cacheManager: LRUCache = LRUCache(capacity: 10000) + + override init(frame: CGRect) { + self.embedded = false + self.embedding = false + super.init(frame: frame) + ParticipantsManager.shared?.delegate = self + } - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } - public override func layoutSubviews() { - super.layoutSubviews() + public override func layoutSubviews() { + super.layoutSubviews() - if (navViewController == nil && !embedding && !embedded) { - embed() - } else { - navViewController?.view.frame = bounds - } + if (navViewController == nil && !embedding && !embedded) { + embed() + } else { + navViewController?.view.frame = bounds } + } - func participantsDidUpdate(_ list: [[String: Any]]) { - print("Participants updated:", list) - updateParticipantsOnMap(list) - } + func participantsDidUpdate(_ list: [[String: Any]]) { + updateParticipantsOnMap(list) + } - public override func removeFromSuperview() { - super.removeFromSuperview() - // cleanup and teardown any existing resources - self.navViewController?.removeFromParent() + public override func removeFromSuperview() { + super.removeFromSuperview() + // cleanup and teardown any existing resources + self.navViewController?.removeFromParent() - // MARK: End CarPlay Navigation - if let carPlayNavigation = UIApplication.shared.delegate as? MapboxCarPlayNavigationDelegate { - carPlayNavigation.endNavigation() - } - NotificationCenter.default.removeObserver(self, name: .navigationSettingsDidChange, object: nil) + // MARK: End CarPlay Navigation + if let carPlayNavigation = UIApplication.shared.delegate as? MapboxCarPlayNavigationDelegate { + carPlayNavigation.endNavigation() } + NotificationCenter.default.removeObserver(self, name: .navigationSettingsDidChange, object: nil) + } - private func embed() { - guard startOrigin.count == 2 && destination.count == 2 else { return } + private func embed() { + guard startOrigin.count == 2 && destination.count == 2 else { return } - embedding = true + embedding = true - let originWaypoint = Waypoint(coordinate: CLLocationCoordinate2D(latitude: startOrigin[1] as! CLLocationDegrees, longitude: startOrigin[0] as! CLLocationDegrees)) - var waypointsArray = [originWaypoint] + let originWaypoint = Waypoint(coordinate: CLLocationCoordinate2D(latitude: startOrigin[1] as! CLLocationDegrees, longitude: startOrigin[0] as! CLLocationDegrees)) + var waypointsArray = [originWaypoint] - // Add Waypoints - waypointsArray.append(contentsOf: waypoints) + // Add Waypoints + waypointsArray.append(contentsOf: waypoints) - let destinationWaypoint = Waypoint(coordinate: CLLocationCoordinate2D(latitude: destination[1] as! CLLocationDegrees, longitude: destination[0] as! CLLocationDegrees), name: destinationTitle as String) - waypointsArray.append(destinationWaypoint) + let destinationWaypoint = Waypoint(coordinate: CLLocationCoordinate2D(latitude: destination[1] as! CLLocationDegrees, longitude: destination[0] as! CLLocationDegrees), name: destinationTitle as String) + waypointsArray.append(destinationWaypoint) - let profile: MBDirectionsProfileIdentifier + let profile: MBDirectionsProfileIdentifier - switch travelMode { - case "cycling": - profile = .cycling - case "walking": - profile = .walking - case "driving-traffic": - profile = .automobileAvoidingTraffic - default: - profile = .automobile - } + switch travelMode { + case "cycling": + profile = .cycling + case "walking": + profile = .walking + case "driving-traffic": + profile = .automobileAvoidingTraffic + default: + profile = .automobile + } - let options = NavigationRouteOptions(waypoints: waypointsArray, profileIdentifier: profile) + let options = NavigationRouteOptions(waypoints: waypointsArray, profileIdentifier: profile) - let locale = self.language.replacingOccurrences(of: "-", with: "_") - options.locale = Locale(identifier: locale) - options.distanceMeasurementSystem = distanceUnit == "imperial" ? .imperial : .metric + let locale = self.language.replacingOccurrences(of: "-", with: "_") + options.locale = Locale(identifier: locale) + options.distanceMeasurementSystem = distanceUnit == "imperial" ? .imperial : .metric - Directions.shared.calculateRoutes(options: options) { [weak self] result in - guard let strongSelf = self, let parentVC = strongSelf.parentViewController else { - return - } + Directions.shared.calculateRoutes(options: options) { [weak self] result in + guard let strongSelf = self, let parentVC = strongSelf.parentViewController else { + return + } - switch result { - case .failure(let error): - strongSelf.onError!(["message": error.localizedDescription]) - case .success(let response): - strongSelf.indexedRouteResponse = response - let navigationOptions = NavigationOptions(simulationMode: strongSelf.shouldSimulateRoute ? .always : .never) - let vc = NavigationViewController(for: response, navigationOptions: navigationOptions) + switch result { + case .failure(let error): + strongSelf.onError!(["message": error.localizedDescription]) + case .success(let response): + strongSelf.indexedRouteResponse = response + let navigationOptions = NavigationOptions(simulationMode: strongSelf.shouldSimulateRoute ? .always : .never) + let vc = NavigationViewController(for: response, navigationOptions: navigationOptions) - vc.showsEndOfRouteFeedback = strongSelf.showsEndOfRouteFeedback - StatusView.appearance().isHidden = strongSelf.hideStatusView + vc.showsEndOfRouteFeedback = strongSelf.showsEndOfRouteFeedback + StatusView.appearance().isHidden = strongSelf.hideStatusView - NavigationSettings.shared.voiceMuted = strongSelf.mute - NavigationSettings.shared.distanceUnit = strongSelf.distanceUnit == "imperial" ? .mile : .kilometer + NavigationSettings.shared.voiceMuted = strongSelf.mute + NavigationSettings.shared.distanceUnit = strongSelf.distanceUnit == "imperial" ? .mile : .kilometer - vc.delegate = strongSelf + vc.delegate = strongSelf - parentVC.addChild(vc) - strongSelf.addSubview(vc.view) - vc.view.frame = strongSelf.bounds - vc.didMove(toParent: parentVC) - strongSelf.navViewController = vc - } + parentVC.addChild(vc) + strongSelf.addSubview(vc.view) + vc.view.frame = strongSelf.bounds + vc.didMove(toParent: parentVC) + strongSelf.navViewController = vc + } - strongSelf.embedding = false - strongSelf.embedded = true + strongSelf.embedding = false + strongSelf.embedded = true - // MARK: Start CarPlay Navigation - if let carPlayNavigation = UIApplication.shared.delegate as? MapboxCarPlayNavigationDelegate { - carPlayNavigation.startNavigation(with: strongSelf) - } - } + // MARK: Start CarPlay Navigation + if let carPlayNavigation = UIApplication.shared.delegate as? MapboxCarPlayNavigationDelegate { + carPlayNavigation.startNavigation(with: strongSelf) + } } + } - public func navigationViewController(_ navigationViewController: NavigationViewController, didUpdate progress: RouteProgress, with location: CLLocation, rawLocation: CLLocation) { - onLocationChange?([ - "longitude": location.coordinate.longitude, - "latitude": location.coordinate.latitude, - "heading": 0, - "accuracy": location.horizontalAccuracy.magnitude - ]) - onRouteProgressChange?([ - "distanceTraveled": progress.distanceTraveled, - "durationRemaining": progress.durationRemaining, - "fractionTraveled": progress.fractionTraveled, - "distanceRemaining": progress.distanceRemaining - ]) - } + public func navigationViewController(_ navigationViewController: NavigationViewController, didUpdate progress: RouteProgress, with location: CLLocation, rawLocation: CLLocation) { + onLocationChange?([ + "longitude": location.coordinate.longitude, + "latitude": location.coordinate.latitude, + "heading": 0, + "accuracy": location.horizontalAccuracy.magnitude + ]) + onRouteProgressChange?([ + "distanceTraveled": progress.distanceTraveled, + "durationRemaining": progress.durationRemaining, + "fractionTraveled": progress.fractionTraveled, + "distanceRemaining": progress.distanceRemaining + ]) + } - public func navigationViewControllerDidDismiss(_ navigationViewController: NavigationViewController, byCanceling canceled: Bool) { - if (!canceled) { - return; - } - onCancelNavigation?(["message": "Navigation Cancel"]); + public func navigationViewControllerDidDismiss(_ navigationViewController: NavigationViewController, byCanceling canceled: Bool) { + if (!canceled) { + return; } + onCancelNavigation?(["message": "Navigation Cancel"]); + } - public func navigationViewController(_ navigationViewController: NavigationViewController, didArriveAt waypoint: Waypoint) -> Bool { - onArrive?([ - "name": waypoint.name ?? waypoint.description, - "longitude": waypoint.coordinate.latitude, - "latitude": waypoint.coordinate.longitude, - ]) - return true; - } + public func navigationViewController(_ navigationViewController: NavigationViewController, didArriveAt waypoint: Waypoint) -> Bool { + onArrive?([ + "name": waypoint.name ?? waypoint.description, + "longitude": waypoint.coordinate.latitude, + "latitude": waypoint.coordinate.longitude, + ]) + return true; + } private func updateParticipantsOnMap(_ list: [[String: Any]]) { guard let mapView = navViewController?.navigationMapView else { return } @@ -223,20 +223,82 @@ public class MapboxNavigationView: UIView, NavigationViewControllerDelegate, Par for user in list { guard let id = user["id"] as? String, - let displayName = user["displayName"] as? String, let lat = user["lat"] as? Double, let lng = user["lng"] as? Double else { continue } let imageUrl = user["imageUrl"] as? String ?? "" let newCoordinate = CLLocationCoordinate2D(latitude: lat, longitude: lng) var annotation = PointAnnotation(id: id, coordinate: newCoordinate) - annotation.textField = displayName - annotation.image = .init(image: UIImage(contentsOfFile: imageUrl) ?? UIImage(named: "user_offline_pin")!, name: id) - annotation.iconSize = 0.2 + if let imageUrl = cacheManager.get(id), let userImge = UIImage(contentsOfFile: imageUrl.path) { + annotation.image = .init(image: userImge, name: id) + } else if let image = imageFromURI(imageUrl,userId: id) { + annotation.image = .init(image: image.withRounded(radius: 10, borderWidth: 1, borderColor: UIColor.white), name: id) + } annotation.userInfo = user uannotations.append(annotation) } - self.pointAnnotationManager?.annotations.removeAll() self.pointAnnotationManager?.annotations = uannotations } + + func imageFromURI(_ uri: String, userId: String) -> UIImage? { + if uri.hasPrefix("asset:/") { + let name = uri.replacingOccurrences(of: "asset:/", with: "") + return UIImage(named: name) + } else if uri.hasPrefix("file://") { + let path = uri.replacingOccurrences(of: "file://", with: "") + return UIImage(contentsOfFile: path) + } else { + downloadImageToTemp(urlString: uri, userId: userId, completion: { [weak self,userId] result in + guard let self = self else { return } + switch result { + case .success(let success): + self.cacheManager.put(userId, value: success.1) + case .failure(let failure): + print(failure.localizedDescription) + } + }) + } + return nil + } + + func downloadImageToTemp(urlString: String, userId: String , completion: @escaping (Result<(UIImage,URL), Error>) -> Void) { + let tempDir = FileManager.default.temporaryDirectory + let fileURL = tempDir.appendingPathComponent("\(userId).jpg") + if FileManager.default.fileExists(atPath: fileURL.path) { + completion(.success((UIImage(contentsOfFile: fileURL.path)!, fileURL))) + return + } + + guard let url = URL(string: urlString) else { + completion(.failure(NSError(domain: "InvalidURL", code: -1))) + return + } + + let task = URLSession.shared.dataTask(with: url) {[userId] data, response, error in + if let error = error { + completion(.failure(error)) + return + } + + guard let data = data, + let image = UIImage(data: data)?.withRounded(radius: 10, borderWidth: 1, borderColor: UIColor.white), + let newImageData = image.pngData() + else { + completion(.failure(NSError(domain: "InvalidImageData", code: -2))) + return + } + + // Create a temp file URL + let tempDir = FileManager.default.temporaryDirectory + let fileURL = tempDir.appendingPathComponent("\(userId).jpg") + + do { + try newImageData.write(to: fileURL) + completion(.success((image,fileURL))) + } catch { + completion(.failure(error)) + } + } + task.resume() + } } diff --git a/ios/UIImage+extensions.swift b/ios/UIImage+extensions.swift new file mode 100644 index 0000000..8efe4c3 --- /dev/null +++ b/ios/UIImage+extensions.swift @@ -0,0 +1,39 @@ +import UIKit +extension UIImage { + func withRounded(radius: CGFloat, borderWidth: CGFloat = 0, borderColor: UIColor = UIColor.white) -> UIImage { + let targetSize = CGSize(width: 20, height: 20) + let rect = CGRect(origin: .zero, size: targetSize) + + UIGraphicsBeginImageContextWithOptions(targetSize, false, 0.0) + let context = UIGraphicsGetCurrentContext() + + let path = UIBezierPath(roundedRect: rect, cornerRadius: radius) + path.addClip() + + // Compute aspect-fit size + let aspectWidth = targetSize.width / size.width + let aspectHeight = targetSize.height / size.height + let scaleFactor = min(aspectWidth, aspectHeight) + + let scaledWidth = size.width * scaleFactor + let scaledHeight = size.height * scaleFactor + + // Center the image in the target rect + let x = (targetSize.width - scaledWidth) / 2 + let y = (targetSize.height - scaledHeight) / 2 + let drawRect = CGRect(x: x, y: y, width: scaledWidth, height: scaledHeight) + + self.draw(in: drawRect) + + if borderWidth > 0 { + borderColor.setStroke() + path.lineWidth = borderWidth + path.stroke() + } + + let processedImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return processedImage ?? self + } +} diff --git a/lefthook.yml b/lefthook.yml index b22aaa7..82354c0 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -2,10 +2,10 @@ pre-commit: parallel: true commands: lint: - glob: "*.{js,ts,jsx,tsx}" + glob: '*.{js,ts,jsx,tsx}' run: npx eslint {staged_files} types: - glob: "*.{js,ts, jsx, tsx}" + glob: '*.{js,ts, jsx, tsx}' run: npx tsc --noEmit commit-msg: parallel: true From 32635200e8c1bf96b98e80d3d692a8a6630109bf Mon Sep 17 00:00:00 2001 From: Usman-benzifi Date: Thu, 25 Sep 2025 16:18:36 +0500 Subject: [PATCH 13/13] aspect fit to fill --- ios/UIImage+extensions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/UIImage+extensions.swift b/ios/UIImage+extensions.swift index 8efe4c3..50999ff 100644 --- a/ios/UIImage+extensions.swift +++ b/ios/UIImage+extensions.swift @@ -13,7 +13,7 @@ extension UIImage { // Compute aspect-fit size let aspectWidth = targetSize.width / size.width let aspectHeight = targetSize.height / size.height - let scaleFactor = min(aspectWidth, aspectHeight) + let scaleFactor = max(aspectWidth, aspectHeight) let scaledWidth = size.width * scaleFactor let scaledHeight = size.height * scaleFactor