diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ce3da7..950632d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +Version 3.4.0 - February 2021 +---------------- + * Update IndoorAtlas SDKs to 3.4.2 + * Add wayfinding A->B route request support + * Add time based location updates support + * Add low power and cart-mode support + Version 3.3.2 - November 2020 ---------------- * Update IndoorAtlas SDKs to 3.3.6 diff --git a/package.json b/package.json index 4cc1c2c..47f98bc 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "cordova-plugin-indooratlas", - "version": "3.3.2", + "version": "3.4.0", "description": "Cordova plugin using IndoorAtlas SDK.", "cordova": { "id": "cordova-plugin-indooratlas", "platforms": [ "android", - "ios" + "ios" ] }, "repository": { diff --git a/plugin.xml b/plugin.xml index 65b20db..67a25ba 100644 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="3.4.0"> IndoorAtlas IndoorAtlas Cordova Plugin. diff --git a/src/android/IALocationPlugin.java b/src/android/IALocationPlugin.java index ba56f29..e7ab9d4 100644 --- a/src/android/IALocationPlugin.java +++ b/src/android/IALocationPlugin.java @@ -17,11 +17,15 @@ import com.indooratlas.android.sdk.IALocationManager; import com.indooratlas.android.sdk.IALocationRequest; import com.indooratlas.android.sdk.IARegion; +import com.indooratlas.android.sdk.IARoute; import com.indooratlas.android.sdk.IAOrientationRequest; import com.indooratlas.android.sdk.IAOrientationListener; +import com.indooratlas.android.sdk.IAWayfindingListener; import com.indooratlas.android.sdk.IAWayfindingRequest; import com.indooratlas.android.sdk.IAGeofence; import com.indooratlas.android.sdk.IAGeofenceRequest; +import com.indooratlas.android.sdk.resources.IALatLngFloor; +import com.indooratlas.android.sdk.resources.IALatLngFloorCompatible; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; @@ -30,6 +34,8 @@ import org.json.JSONException; import org.json.JSONObject; +import static com.ialocation.plugin.IndoorLocationListener.getRouteJSONFromIARoute; + /** * Cordova Plugin which implements IndoorAtlas positioning service. * IndoorAtlas.initialize method should always be called before starting positioning session. @@ -168,9 +174,13 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo String watchId = args.getString(0); clearRegionWatch(watchId); callbackContext.success(); - } else if ("setDistanceFilter".equals(action)) { + } else if ("setOutputThresholds".equals(action)) { float distance = (float) args.getDouble(0); - setDistanceFilter(distance, callbackContext); + float interval = (float) args.getDouble(1); + setOutputThresholds(distance, interval, callbackContext); + } else if ("setPositioningMode".equals(action)) { + String mode = args.getString(0); + setPositioningMode(mode, callbackContext); } else if ("getTraceId".equals(action)) { getTraceId(callbackContext); } else if ("getFloorCertainty".equals(action)) { @@ -196,6 +206,18 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo double lon = args.getDouble(1); int floor = args.getInt(2); requestWayfindingUpdates(lat, lon, floor, callbackContext); + } else if ("requestWayfindingRoute".equals(action)) { + JSONObject _from = args.getJSONObject(0); + JSONObject _to = args.getJSONObject(1); + IALatLngFloor from = new IALatLngFloor( + _from.getDouble("latitude"), + _from.getDouble("longitude"), + _from.getInt("floor")); + IALatLngFloor to = new IALatLngFloor( + _to.getDouble("latitude"), + _to.getDouble("longitude"), + _to.getInt("floor")); + requestWayfindingRoute(from, to, callbackContext); } else if ("removeWayfindingUpdates".equals(action)) { removeWayfindingUpdates(); } else if ("watchGeofences".equals(action)) { @@ -387,6 +409,28 @@ public void run() { }); } + private void requestWayfindingRoute( + IALatLngFloorCompatible from, + IALatLngFloorCompatible to, + CallbackContext callbackContext + ) { + IAWayfindingListener listener = new IAWayfindingListener() { + @Override + public void onWayfindingUpdate(IARoute route) { + PluginResult pluginResult; + pluginResult = new PluginResult(PluginResult.Status.OK, getRouteJSONFromIARoute(route)); + pluginResult.setKeepCallback(false); + callbackContext.sendPluginResult(pluginResult); + } + }; + cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + mLocationManager.requestWayfindingRoute(from, to, listener); + } + }); + } + private IAGeofence geofenceFromJsonObject(JSONObject geoJson) { try { List iaEdges = new ArrayList<>(); @@ -596,24 +640,43 @@ protected void startPositioning(CallbackContext callbackContext) { callbackContext.error(PositionError.getErrorObject(PositionError.INITIALIZATION_ERROR)); } } + + private void setPositioningMode(String mode, CallbackContext callbackContext) { + int priority; + switch (mode) { + case "HIGH_ACCURACY": priority = IALocationRequest.PRIORITY_HIGH_ACCURACY; break; + case "LOW_POWER": priority = IALocationRequest.PRIORITY_LOW_POWER; break; + case "CART": priority = IALocationRequest.PRIORITY_CART_MODE; break; + default: + callbackContext.error(PositionError.getErrorObject(PositionError.INVALID_POSITIONING_MODE)); + return; + } + mLocationRequest.setPriority(priority); + } - private void setDistanceFilter(float distance, CallbackContext callbackContext) { + private void setOutputThresholds(float distance, float interval, CallbackContext callbackContext) { + if (distance < 0 || interval < 0) { + callbackContext.error(PositionError.getErrorObject(PositionError.INVALID_OUTPUT_THRESHOLD_VALUE)); + return; + } final boolean wasRunning = mLocationServiceRunning; if (wasRunning) stopPositioning(); if (distance >= 0) { mLocationRequest.setSmallestDisplacement(distance); - JSONObject successObject = new JSONObject(); - try { - successObject.put("message","Distance filter set"); - } catch (JSONException ex) { - Log.e(TAG, ex.toString()); - throw new IllegalStateException(ex.getMessage()); - } - callbackContext.success(successObject); - if (wasRunning) startPositioning(); - } else { - callbackContext.error(PositionError.getErrorObject(PositionError.INVALID_VALUE)); } + if (interval >= 0) { + // convert to milliseconds + mLocationRequest.setFastestInterval((long)(interval * 1000)); + } + JSONObject successObject = new JSONObject(); + try { + successObject.put("message","Output thresholds set"); + } catch (JSONException ex) { + Log.e(TAG, ex.toString()); + throw new IllegalStateException(ex.getMessage()); + } + callbackContext.success(successObject); + if (wasRunning) startPositioning(); } private void getTraceId(CallbackContext callbackContext) { diff --git a/src/android/IndoorLocationListener.java b/src/android/IndoorLocationListener.java index 76af4db..1aa8713 100644 --- a/src/android/IndoorLocationListener.java +++ b/src/android/IndoorLocationListener.java @@ -334,7 +334,7 @@ private JSONObject getLocationJSONFromIALocation(IALocation iaLocation) { } } - private JSONObject getRouteJSONFromIARoute(IARoute route) { + static JSONObject getRouteJSONFromIARoute(IARoute route) { JSONObject obj = new JSONObject(); try { JSONArray jsonArray = new JSONArray(); @@ -342,6 +342,7 @@ private JSONObject getRouteJSONFromIARoute(IARoute route) { jsonArray.put(jsonObjectFromRoutingLeg(leg)); } obj.put("legs", jsonArray); + obj.put("error", route.getError().name()); } catch(JSONException e) { Log.e("IAWAYFINDER", "json error with route"); } @@ -351,7 +352,7 @@ private JSONObject getRouteJSONFromIARoute(IARoute route) { /** * Create JSON object from the given RoutingLeg object */ - private JSONObject jsonObjectFromRoutingLeg(IARoute.Leg routingLeg) { + private static JSONObject jsonObjectFromRoutingLeg(IARoute.Leg routingLeg) { JSONObject obj = new JSONObject(); try { obj.put("begin", jsonObjectFromRoutingPoint(routingLeg.getBegin())); @@ -368,7 +369,7 @@ private JSONObject jsonObjectFromRoutingLeg(IARoute.Leg routingLeg) { /** * Create JSON object from RoutingPoint object */ - private JSONObject jsonObjectFromRoutingPoint(IARoute.Point routingPoint) { + private static JSONObject jsonObjectFromRoutingPoint(IARoute.Point routingPoint) { JSONObject obj = new JSONObject(); try { obj.put("latitude", routingPoint.getLatitude()); diff --git a/src/android/PositionError.java b/src/android/PositionError.java index ee8738b..aeeef23 100644 --- a/src/android/PositionError.java +++ b/src/android/PositionError.java @@ -19,8 +19,9 @@ public class PositionError { public static final int FLOOR_PLAN_UNAVAILABLE = 6; public static final int UNSPECIFIED_ERROR = 7; public static final int FLOOR_PLAN_UNDEFINED = 8; - public static final int INVALID_VALUE = 9; - + public static final int INVALID_OUTPUT_THRESHOLD_VALUE = 9; + public static final int INVALID_POSITIONING_MODE = 10; + /** * Returns an error JSON object with given errorCode and message * @param errorCode @@ -77,13 +78,15 @@ public static JSONObject getErrorObject(int errorCode) { errorObject.put("code", errorCode); errorObject.put("message", "Floor plan undefined. See ~/www/jsAPIKeys.js file"); break; - case INVALID_VALUE: - errorObject.put("code", errorCode); - errorObject.put("message", "Distance value should be positive"); - case UNSPECIFIED_ERROR: + case INVALID_OUTPUT_THRESHOLD_VALUE: errorObject.put("code", errorCode); - errorObject.put("message", "Unspecified error"); + errorObject.put("message", "Distance and time filter values should be positive"); break; + case INVALID_POSITIONING_MODE: + errorObject.put("code", errorCode); + errorObject.put("message", "Invalid positioning mode"); + break; + case UNSPECIFIED_ERROR: default: errorObject.put("code", errorCode); errorObject.put("message", "Unspecified error"); diff --git a/src/android/indooratlas.gradle b/src/android/indooratlas.gradle index 400d603..a0b27b7 100644 --- a/src/android/indooratlas.gradle +++ b/src/android/indooratlas.gradle @@ -7,5 +7,5 @@ repositories { dependencies { implementation 'com.android.support:appcompat-v7:27.0.2' - implementation 'com.indooratlas.android:indooratlas-android-sdk:3.3.6' + implementation 'com.indooratlas.android:indooratlas-android-sdk:3.4.2' } diff --git a/src/ios/IndoorAtlas/IndoorAtlas.framework/Headers/IAFloor.h b/src/ios/IndoorAtlas/IndoorAtlas.framework/Headers/IAFloor.h index a2f9865..947f590 100644 --- a/src/ios/IndoorAtlas/IndoorAtlas.framework/Headers/IAFloor.h +++ b/src/ios/IndoorAtlas/IndoorAtlas.framework/Headers/IAFloor.h @@ -34,7 +34,7 @@ INDOORATLAS_API @property (nonatomic, readonly) NSInteger level; /** - * Certainty that floor has the correct value. + * Certainty that `IALocation` floor has the correct value. */ @property (nonatomic, readonly) IACertainty certainty; diff --git a/src/ios/IndoorAtlas/IndoorAtlas.framework/Headers/IALocationManager.h b/src/ios/IndoorAtlas/IndoorAtlas.framework/Headers/IALocationManager.h index ab57fb0..f621498 100644 --- a/src/ios/IndoorAtlas/IndoorAtlas.framework/Headers/IALocationManager.h +++ b/src/ios/IndoorAtlas/IndoorAtlas.framework/Headers/IALocationManager.h @@ -6,17 +6,30 @@ #import #import #import +#import #define INDOORATLAS_API __attribute__((visibility("default"))) /** - * Use this key to obtain the session trace id from the [IALocationManager extraInfo](Classes/IALocationManager.html#/c:objc(cs)IALocationManager(py)extraInfo) dictionary. + * Indicates that a feature that is still subject to change in a future release. + */ +@protocol IABeta +@end +/** + * Indicates a feature that is not available by default but can be enabled by contacting IndoorAtlas sales. + */ +@protocol IARestricted +@end + +/** + * Use this key to obtain the session trace id from the `[IALocationManager extraInfo]` dictionary. */ INDOORATLAS_API extern NSString * _Nonnull const kIATraceId; @class IALocationManager; @class IAGeofence; @class IAPOI; +@class IALatLngFloor; /** * Defines the type of region. @@ -95,6 +108,33 @@ typedef NS_ENUM(NSInteger, ia_location_accuracy) { kIALocationAccuracyBestForCart }; +/** + * Defines the error status of a wayfinding request + */ +typedef NS_ENUM(NSInteger, ia_route_error) { + /** Routing was successful */ + kIARouteErrorNoError = 0, + /** Route could not be computed */ + kIARouteErrorRoutingFailed = 1, + /** Wayfinding graph not available */ + kIARouteErrorGraphNotAvailable = 2 +}; + +@protocol IALatLngFloorCompatible +@property (nonatomic,readonly) IALatLngFloor * _Nonnull latLngFloor; +@end + +/** + * Represents generic location with latitude, longitude and floor number + */ +INDOORATLAS_API +@interface IALatLngFloor : NSObject +@property (nonatomic,readonly) CLLocationDegrees latitude; +@property (nonatomic,readonly) CLLocationDegrees longitude; +@property (nonatomic,readonly) NSInteger floor; ++ (nonnull IALatLngFloor *)latLngFloorWithLatitude:(CLLocationDegrees)latitude andLongitude:(CLLocationDegrees)longitude andFloor:(NSInteger)floor; ++ (nonnull IALatLngFloor *)latLngFloorWithCoordinate:(CLLocationCoordinate2D)coordinate andFloor:(NSInteger)floor; +@end /** * Represents a venue in IndoorAtlas system @@ -139,8 +179,7 @@ INDOORATLAS_API @property (nonatomic, strong, nullable) NSString *name; /** * Region type - * - * @param type See possible values at [ia_region_type](/Constants/ia_region_type.html) + * See possible values at `ia_region_type` */ @property (nonatomic, assign) enum ia_region_type type; /** @@ -197,7 +236,7 @@ INDOORATLAS_API /** * Creates a new polygonal region from unique edges. * @param identifier Identifier for the geofence. - * @param `IAFloor` object with level information. Nil `IAFloor` means that the floor is unknown. + * @param floor `IAFloor` object with level information. Nil `IAFloor` means that the floor is unknown. * @param edges Coordinates specifying the polygon. * * The edges must be supplied in clockwise order for the polygon to be valid. @@ -209,7 +248,7 @@ INDOORATLAS_API * Represents a point of interest. */ INDOORATLAS_API -@interface IAPOI : IAGeofence +@interface IAPOI : IAGeofence /** * The floor the POI is located on. */ @@ -220,7 +259,7 @@ INDOORATLAS_API * Provides wayfinding destination for the SDK. */ INDOORATLAS_API -@interface IAWayfindingRequest : NSObject +@interface IAWayfindingRequest : NSObject /** * Wayfinding request's destination coordinate. */ @@ -236,7 +275,7 @@ INDOORATLAS_API * Represents a point in a route. */ INDOORATLAS_API -@interface IARoutePoint : NSObject +@interface IARoutePoint : NSObject /** * Coordinate of this point. */ @@ -301,6 +340,10 @@ INDOORATLAS_API * An array of `IARouteLeg` objects connecting user's location to destination. */ @property (nonatomic, readonly, nonnull) NSArray *legs; +/** Whether route is available */ +@property (nonatomic, readonly) bool isSuccessful; +/** Error status for routing */ +@property (nonatomic, readonly) enum ia_route_error error; @end /** @@ -324,7 +367,7 @@ INDOORATLAS_API * This class is designed to be used as is and should not be subclassed. */ INDOORATLAS_API -@interface IALocation : NSObject +@interface IALocation : NSObject /** * @name Initializing a Location Object @@ -343,7 +386,7 @@ INDOORATLAS_API /** * Initializes and returns a location object with specified CoreLocation information. * @param location CLLocation object. Might be initialized in code or from CLLocationManager. - * @param `IAFloor` object with level information. Nil `IAFloor` means that the floor is unknown. + * @param floor `IAFloor` object with level information. Nil `IAFloor` means that the floor is unknown. * * An explicit location is used as a hint in the system. This means that the inputted location is used only to determine the initial position and setting the location does not lock the floor or venue context. */ @@ -417,6 +460,178 @@ INDOORATLAS_API @property(readonly, nonatomic, copy, nullable) NSDate *timestamp; @end +/** + * Object describing an object in the AR space. + * The methods of an instance of this class can be called from any thread. + * + * NOTE! To enable AR features, please contact IndoorAtlas sales. + */ +INDOORATLAS_API +@interface IAARObject : NSObject +/** + * Get the current model matrix for this object. + * + * @param outModelMatrix Output: a 4x4 homogeneous model-to-world matrix. . + * @return true if the outtModelMatrix was set and the object can be displayed, + false if the model matrix is not available or object should not be displayed for + * some other reason. + */ +- (bool)updateModelMatrix:(nonnull simd_float4x4*)outModelMatrix; +@end + +/** + * IndoorAtlas AR fusion API + * + * NOTE! To enable AR features, please contact IndoorAtlas sales. + * + * The AR API provides you with convenient means of converting between two important + * coordinate systems: + * * The global coordinates encoded in IALocation objects and their components. + * In practice, this means latitude, longitude, floor number and heading/orientation + * information. + * * An augmented reality (AR) coordinate system, which is assumed to be a right-handed + * 3D metric coordinate system, where the Y axis points up (towards the sky). + * + * The local tracking of the device in the AR coordinate system is assumed to be handled by an + * external AR solution like ARCore, whose certain outputs are given to this class. The IndoorAtlas + * platform fuses this information with the IndoorAtlas position estimates and provides the + * relevant coordinate transforms in a stable and visually consistent manner, which allows you + * to easily place geographically referenced content in the AR world. + * + * The methods of an instance of this class can be called from any thread. + */ +INDOORATLAS_API +@interface IAARSession : NSObject +/** + * Wayfinding arrow AR object. + */ +@property (nonatomic, readonly, nonnull) IAARObject *wayfindingCompassArrow; + +/** + * Wayfinding target (goal) AR object. + */ +@property (nonatomic, readonly, nonnull) IAARObject *wayfindingTarget; + +/** + * Array of waypoint AR objects constructed from the wayfinding route. + */ +@property (nonatomic, readonly, nullable) NSArray *wayfindingTurnArrows; + +/** + * Check if the positioning session has approximately converged. If false, it + * is recommended to advise the user to walk for a couple of meters to any direction so + * that they coordinate systems can be oriented correctly. This is optional, but the + * `IAARObject` instances may first appear in clearly incorrect directions on positions + * otherwise. + */ +@property (nonatomic, readonly) bool converged; + + +/** + * Create an AR Point-of-Interest in the given geographical coordinates. The coordinates of + * the object in the AR world update in a visually pleasing manner. + * + * @param coords latitude in degrees + * @param floorNumber IndoorAtlas integer floor number + * @return IAARObject + */ +- (nonnull IAARObject*)createPoi:(CLLocationCoordinate2D)coords floorNumber:(int)floorNumber; + +/** + * Create an AR Point-of-Interest in the given geographical coordinates. The coordinates of + * the object in the AR world update in a visually pleasing manner. + * + * @param coords latitude in degrees + * @param floorNumber IndoorAtlas integer floor number + * @param heading Heading in degrees 0=North, 90=East, 180=South, 270=West + * @param zOffset Vertical offset from the floor plane in meters + * @return IAARObject + */ +- (nonnull IAARObject*)createPoi:(CLLocationCoordinate2D)coords floorNumber:(int)floorNumber heading:(double)heading zOffset:(double)zOffset; + +/** + * Create an AR Point-of-Interest in the given geographical coordinates. The coordinates of + * the object in the AR world update in a visually pleasing manner. + * + * @param location location of the POI + * @return IAARObject + */ +- (nonnull IAARObject*)createPoi:(nonnull IALocation*)location; + +/** + * Convert from geographical to AR coordinates. + * + * @param coords Geographical coordinates + * @param floorNumber IndoorAtlas integer floor number + * @param heading heading in degrees 0=North, 90=East, 180=South, 270=West + * @param zOffset Vertical offset from the floor plane in meters + * @return a 4x4 homogeneous model-to-world matrix. The matrix will be a identity + * matrix in case of conversion failure. The conversion fails unless both + * IndoorAtlas positioning and the AR input have been obtained (i.e., + * before the first fix or before setArCameraMatrix has been called for the + * first time). + */ +- (simd_float4x4)geoToAr:(const CLLocationCoordinate2D)coords floorNumber:(int)floorNumber heading:(double)heading zOffset:(double)zOffset; + +/** + * Convert from AR to geographic coordinates. + * + * @param matrix 4x4 homogeneous model-to-world matrix + * @return geographical coordinates. Nil if not available + */ +- (nonnull IALocation*)arToGeo:(const simd_float4x4)matrix; + +/** + * Convert from AR to geographic coordinates. + * + * @param x AR coordinate system X-coordinate (horizontal) + * @param y AR coordinate system Y-coordinate (vertical) + * @param z AR coordinate system Z-coordinate (horizontal) + * + * @return geographical coordinates. Nil if not available + */ +- (nonnull IALocation*)arToGeo:(double)x Y:(double)y Z:(double)z; + +/** + * Input current pose from the external AR tracking. This method should be called on each + * successfully tracked AR camera frame. + * + * @param poseMatrix Current "sensor pose" from AR tracking. + a 4x4 homogeneous local-to-world matrix, equivalent to ARKit's ARCamera.transform[1] + This matrix does not change with UI orientation. + 1: https://developer.apple.com/documentation/arkit/arcamera/2866108-transform. + */ +- (void)setPoseMatrix:(const simd_float4x4)poseMatrix; + +/** + * Set the current camera-to-world matrix. Should be called regularly as long as one wishes + * to render objects. Calling on each AR frame is recommended. + * + * @param cameraToWorldMatrix a 4x4 homogeneous camera-to-world matrix, where the the negative + * Z axis is points "into the screen" in camera coordinates. Unlike + * the poseMatrix method, this matrix may change with UI orientation. + * This matrix is equivalent to inverse of ARKit's ARCamera.viewMatrix[1] + * 1: https://developer.apple.com/documentation/arkit/arcamera/2921672-viewmatrix + */ +- (void)setCameraToWorldMatrix:(const simd_float4x4)cameraToWorldMatrix; + +/** + * Input AR plane tracking information. This input is optional, but allows more accurate + * vertical tracking, e.g., placing geo-referenced AR objects so that they appear to be on + * the floor. If used, should be called on each AR frame for each tracked horizontal + * upward-facing planes as input. + * + * The planes will be applied on the next `setPoseMatrix:` call. + * + * @param centerX Center X coordinate of the plane + * @param centerY Center Y coordinate of the plane + * @param centerZ Center Z coordinate of the plane + * @param extentX Extent X of the plane + * @param extentZ Extent Z of the plane + */ +- (void)addPlaneWithCenterX:(float)centerX withCenterY:(float)centerY withCenterZ:(float)centerZ withExtentX:(float)extentX withExtentZ:(float)extentZ; +@end + /** * The IALocationManagerDelegate protocol defines the methods used to receive location updates from an `IALocationManager` object. * @@ -525,13 +740,22 @@ INDOORATLAS_API @property (nonatomic, readwrite, nullable) IAHeading *heading; /** - * The minimum distance measured in meters that the device must move - * horizontally before an update event is generated. + * The minimum distance measured in meters that the device must move horizontally before an update event is generated. + * Setting this to 0 disables distance based updates. + * Maximum update frequency is determined from values of distanceFilter and timeFilter. Update is generated when either of conditions specified by these filters are met. * Default value is 0.7 meters. - * Uses CoreLocation [CLLocationDistance](https://developer.apple.com/library/ios/documentation/CoreLocation/Reference/CoreLocationDataTypesRef/index.html#//apple_ref/c/tdef/CLLocationDistance). + * Uses CoreLocation [CLLocationDistance](https://developer.apple.com/documentation/corelocation/cllocationdistance?language=objc). */ @property (assign, nonatomic) CLLocationDistance distanceFilter; +/** + * The minimum amount of time measured in seconds that must be elapsed before an update event is generated. + * Setting this to 0 disables time based updates. + * Maximum update frequency is determined from values of distanceFilter and timeFilter. Update is generated when either of conditions specified by these filters are met. + * Default value is 2. + */ +@property (assign, nonatomic) NSTimeInterval timeFilter; + /** * The minimum angular change in degrees required to generate new didUpdateHeading event. * Default value is 1 degree. @@ -553,7 +777,7 @@ INDOORATLAS_API * * Default value is kIALocationAccuracyBest. * - * @param desiredAccuracy See possible values at [ia_location_accuracy](/Constants/ia_location_accuracy) + * See possible values at `ia_location_accuracy` */ @property(assign, nonatomic) enum ia_location_accuracy desiredAccuracy; @@ -567,8 +791,11 @@ INDOORATLAS_API * key in your app's Info.plist file, and the user must authorize the always on background location permission in order for this flag to have any effect. * * For more info, see: + * * https://developer.apple.com/documentation/corelocation/getting_the_user_s_location/handling_location_events_in_the_background + * * https://developer.apple.com/documentation/corelocation/cllocationmanager/1620568-allowsbackgroundlocationupdates + * * https://developer.apple.com/documentation/corelocation/choosing_the_authorization_level_for_location_services/requesting_always_authorization * * Default value is false, i.e. background location updates are not explicitly enabled @@ -689,15 +916,15 @@ INDOORATLAS_API /** * Start monitoring for wayfinding updates. - * Calling this method causes the location manager to obtain a route from user's current location to destination defined in parameter "request" (this may take several seconds). + * Calling this method causes the location manager to obtain a route from user's current location to destination defined in parameter "to" (this may take several seconds). * Calling this method notify your delegate by calling its method. After that, the receiver generates update events whenever the route changes. * * Calling this method several times in succession overwrites the previously done requests. * Calling in-between, however, does cause a new initial event to be sent the next time you call this method. * - * @param request An specifying the type of wayfinding updates to monitor. + * @param to An object specifying the wayfinding destination */ -- (void)startMonitoringForWayfinding:(nonnull IAWayfindingRequest*)request; +- (void)startMonitoringForWayfinding:(nonnull id)to; /** * Stops monitoring for wayfinding updates. @@ -707,6 +934,29 @@ INDOORATLAS_API */ - (void)stopMonitoringForWayfinding; +/** + * Request a single-shot wayfinding route. Callback with route result is called from the application main thread. + * + * @param from An object specifying the wayfinding starting location + * @param to An object specifying the wayfinding destination + * @param callback callback to call with route result + */ +- (void)requestWayfindingRouteFrom:(nonnull id)from to:(nonnull id)to callback:(void(^_Nonnull)(IARoute *_Nonnull))callback; + +/** + * Lazily creates AR session. + * + * To release memory and stop all AR related processing you must call `releaseArSession`. + * + * NOTE! To enable AR features, please contact IndoorAtlas sales. + */ +@property (nonatomic, readonly, nullable) IAARSession *arSession; + +/** + * Stops all AR related activity and releases the memory allocated for it. + */ +- (void)releaseArSession; + /** * Returns the shared instance. */ diff --git a/src/ios/IndoorAtlas/IndoorAtlas.framework/IndoorAtlas b/src/ios/IndoorAtlas/IndoorAtlas.framework/IndoorAtlas index a3d521f..e2523f4 100755 Binary files a/src/ios/IndoorAtlas/IndoorAtlas.framework/IndoorAtlas and b/src/ios/IndoorAtlas/IndoorAtlas.framework/IndoorAtlas differ diff --git a/src/ios/IndoorAtlas/IndoorAtlas.framework/Info.plist b/src/ios/IndoorAtlas/IndoorAtlas.framework/Info.plist index fbcb673..bf428b3 100644 Binary files a/src/ios/IndoorAtlas/IndoorAtlas.framework/Info.plist and b/src/ios/IndoorAtlas/IndoorAtlas.framework/Info.plist differ diff --git a/src/ios/IndoorAtlasLocationService.h b/src/ios/IndoorAtlasLocationService.h index 2707a25..4838172 100644 --- a/src/ios/IndoorAtlasLocationService.h +++ b/src/ios/IndoorAtlasLocationService.h @@ -74,7 +74,7 @@ typedef NSUInteger IndoorLocationTransitionType; @property (nonatomic, weak) id delegate; -- (id)init:(NSString *)apikey; +- (id)init:(NSString *)apikey pluginVersion:(NSString *)pluginVersion; /** * Start positioning * @@ -124,10 +124,14 @@ typedef NSUInteger IndoorLocationTransitionType; */ - (void)stopMonitoringForWayfinding; +- (void)requestWayfindingRouteFrom:(nonnull id)from to:(nonnull id)to callback:(void(^_Nonnull)(IARoute *_Nonnull))callback; + - (void)startMonitoringGeofences:(IAGeofence *)geofence; - (void)stopMonitoringGeofences:(IAGeofence *)geofence; - (void)valueForDistanceFilter:(float *)distance; +- (void)valueForTimeFilter:(float *)interval; +- (void)setDesiredAccuracy:(ia_location_accuracy)accuracy; - (float)fetchFloorCertainty; - (NSString *)fetchTraceId; - (void)setSensitivities:(double *)orientationSensitivity headingSensitivity:(double *)headingSensitivity; diff --git a/src/ios/IndoorAtlasLocationService.m b/src/ios/IndoorAtlasLocationService.m index 90927ec..ced4af6 100644 --- a/src/ios/IndoorAtlasLocationService.m +++ b/src/ios/IndoorAtlasLocationService.m @@ -2,6 +2,10 @@ #import #import "IndoorAtlasLocationService.h" +@interface IALocationManager () +- (void)setObject:(id)object forKey:(NSString*)key; +@end + @interface IndoorAtlasLocationService() { } @@ -25,17 +29,20 @@ - (id)init * IndoorAtlas Navigation * * @param apikey IndoorAtlas key - * @param apisecret IndoorAtlas secret + * @param pluginVersion IA cordova plugin version * * @return Object for indoor navigation */ -- (id)init:(NSString *)apikey +- (id)init:(NSString *)apikey pluginVersion:(NSString *)pluginVersion { self = [super init]; if (self) { self.apikey = apikey; // Create IALocationManager and point delegate to receiver - self.manager = [IALocationManager new]; + self.manager = [IALocationManager sharedInstance]; + + // Set cordova plugin info + [self.manager setObject:@{ @"name": @"cordova", @"version": pluginVersion} forKey:@"IAWrapper"]; // Set IndoorAtlas API key [self.manager setApiKey:self.apikey andSecret:@""]; @@ -161,6 +168,16 @@ - (void)valueForDistanceFilter:(float *)distance self.manager.distanceFilter = (CLLocationDistance) *(distance); } +- (void)valueForTimeFilter:(float *)interval +{ + self.manager.timeFilter = (NSTimeInterval) *(interval); +} + +- (void)setDesiredAccuracy:(ia_location_accuracy)accuracy +{ + self.manager.desiredAccuracy = accuracy; +} + - (float)fetchFloorCertainty { return [IALocationManager sharedInstance].location.floor.certainty; @@ -241,6 +258,11 @@ - (void)stopMonitoringForWayfinding [self.manager stopMonitoringForWayfinding]; } +- (void)requestWayfindingRouteFrom:(nonnull id)from to:(nonnull id)to callback:(void(^_Nonnull)(IARoute *_Nonnull))callback +{ + [self.manager requestWayfindingRouteFrom:from to:to callback:callback]; +} + - (void)startMonitoringGeofences:(IAGeofence *)geofence { NSLog(@"startMonitoringGeofence: %@, floor %ld", geofence.name, geofence.floor.level); diff --git a/src/ios/IndoorLocation.h b/src/ios/IndoorLocation.h index 12c4a75..5e479ca 100644 --- a/src/ios/IndoorLocation.h +++ b/src/ios/IndoorLocation.h @@ -50,7 +50,6 @@ typedef NSUInteger IndoorLocationStatus; @interface IndoorLocation : CDVPlugin { } -@property (nonatomic, strong) CLLocationManager *locationManager; @property (nonatomic, strong) IndoorLocationInfo *locationData; @property (nonatomic, strong) IndoorRegionInfo *regionData; @property (nonatomic, strong) NSMutableArray *wayfinderInstances; @@ -69,12 +68,14 @@ typedef NSUInteger IndoorLocationStatus; - (void)addStatusChangedCallback:(CDVInvokedUrlCommand *)command; - (void)removeStatusCallback:(CDVInvokedUrlCommand *)command; - (void)setPosition:(CDVInvokedUrlCommand *)command; -- (void)setDistanceFilter:(CDVInvokedUrlCommand *)command; +- (void)setOutputThresholds:(CDVInvokedUrlCommand *)command; +- (void)setPositioningMode:(CDVInvokedUrlCommand *)command; - (void)getFloorCertainty:(CDVInvokedUrlCommand *)command; - (void)getTraceId:(CDVInvokedUrlCommand *)command; - (void)setSensitivities:(CDVInvokedUrlCommand *)command; - (void)requestWayfindingUpdates:(CDVInvokedUrlCommand *)command; - (void)removeWayfindingUpdates:(CDVInvokedUrlCommand *)command; +- (void)requestWayfindingRoute:(CDVInvokedUrlCommand *)command; - (void)watchGeofences:(CDVInvokedUrlCommand *)command; - (void)clearGeofenceWatch:(CDVInvokedUrlCommand *)command; - (void)addDynamicGeofence:(CDVInvokedUrlCommand *)command; diff --git a/src/ios/IndoorLocation.m b/src/ios/IndoorLocation.m index 1e18395..bf7e955 100644 --- a/src/ios/IndoorLocation.m +++ b/src/ios/IndoorLocation.m @@ -45,7 +45,7 @@ @interface IndoorLocation () { @property (nonatomic, strong) NSString *floorPlanCallbackID; @property (nonatomic, strong) NSString *coordinateToPointCallbackID; @property (nonatomic, strong) NSString *pointToCoordinateCallbackID; -@property (nonatomic, strong) NSString *setDistanceFilterCallbackID; +@property (nonatomic, strong) NSString *setOutputThresholdsCallbackID; @property (nonatomic, strong) NSString *getFloorCertaintyCallbackID; @property (nonatomic, strong) NSString *getTraceIdCallbackID; @property (nonatomic, strong) NSString *addAttitudeUpdateCallbackID; @@ -64,7 +64,6 @@ @implementation IndoorLocation - (void)pluginInitialize { - self.locationManager = [[CLLocationManager alloc] init]; __locationStarted = NO; self.locationData = nil; self.regionData = nil; @@ -87,10 +86,7 @@ - (void)_stopLocation stopLocationservice = NO; } if (stopLocationservice) { - if (__locationStarted) { - [self.locationManager stopUpdatingLocation]; - __locationStarted = NO; - } + __locationStarted = NO; [self.IAlocationInfo stopPositioning]; } } @@ -206,9 +202,9 @@ - (void)returnHeadingInformation:(double)heading timestamp:(NSDate *)timestamp } } -- (void)returnRouteInformation:(IARoute *)route +- (void)returnRouteInformation:(IARoute *)route callbackId:(NSString *)callbackId andKeepCallback:(BOOL)keepCallback { - if (_addRouteUpdateCallbackID == nil) { + if (callbackId == nil) { NSLog(@"No wayfinding callback found"); return; } @@ -217,11 +213,17 @@ - (void)returnRouteInformation:(IARoute *)route for (int i = 0; i < [route.legs count]; i++) { [routingLegs addObject:[self dictionaryFromRouteLeg:route.legs[i]]]; } - NSDictionary *result = @{@"legs": routingLegs}; + NSString *error; + switch ([route error]) { + case kIARouteErrorNoError: error = @"NO_ERROR"; break; + case kIARouteErrorRoutingFailed: error = @"ROUTING_FAILED"; break; + case kIARouteErrorGraphNotAvailable: error = @"GRAPH_NOT_AVAILABLE"; break; + } + NSDictionary *result = @{@"legs": routingLegs, @"error": error}; CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:result]; - [pluginResult setKeepCallbackAsBool:YES]; - [self.commandDelegate sendPluginResult:pluginResult callbackId:self.addRouteUpdateCallbackID]; + [pluginResult setKeepCallbackAsBool:keepCallback]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; } - (void)returnStatusInformation:(NSString *)statusString code:(NSUInteger) code @@ -298,15 +300,9 @@ - (NSDictionary *)floorPlanToDictionary:(IAFloorPlan *)floorplan return dict; } -- (void)dealloc -{ - self.locationManager.delegate = nil; -} - - (void)onReset { [self _stopLocation]; - [self.locationManager stopUpdatingHeading]; } @@ -317,6 +313,7 @@ - (void)initializeIndoorAtlas:(CDVInvokedUrlCommand *)command NSString *callbackId = command.callbackId; CDVPluginResult *pluginResult; NSString *iakey = [command.arguments objectAtIndex:0]; + NSString *pluginVersion = [command.arguments objectAtIndex:2]; if (iakey == nil) { NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:2]; @@ -326,7 +323,7 @@ - (void)initializeIndoorAtlas:(CDVInvokedUrlCommand *)command [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; } else { - self.IAlocationInfo = [[IndoorAtlasLocationService alloc] init:iakey]; + self.IAlocationInfo = [[IndoorAtlasLocationService alloc] init:iakey pluginVersion:pluginVersion]; self.IAlocationInfo.delegate = self; NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:2]; @@ -522,19 +519,40 @@ - (void)setPosition:(CDVInvokedUrlCommand *)command [self.IAlocationInfo setPosition:iaLoc]; } -- (void)setDistanceFilter:(CDVInvokedUrlCommand *)command +- (void)setOutputThresholds:(CDVInvokedUrlCommand *)command { - self.setDistanceFilterCallbackID = command.callbackId; + self.setOutputThresholdsCallbackID = command.callbackId; + NSString *distance = [command argumentAtIndex:0]; - float d = [distance floatValue]; + NSString *interval = [command argumentAtIndex:1]; + float t = [interval floatValue]; + [self.IAlocationInfo valueForDistanceFilter: &d]; + [self.IAlocationInfo valueForTimeFilter: &t]; CDVPluginResult *pluginResult; NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:1]; - [result setObject:@"DistanceFilter set" forKey:@"message"]; + [result setObject:@"Output thresholds set" forKey:@"message"]; pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:result]; - [self.commandDelegate sendPluginResult:pluginResult callbackId:self.setDistanceFilterCallbackID]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.setOutputThresholdsCallbackID]; +} + +- (void)setPositioningMode:(CDVInvokedUrlCommand *)command +{ + NSString *mode = [command argumentAtIndex:0]; + + if ([mode isEqualToString:@"HIGH_ACCURACY"]) { + [self.IAlocationInfo setDesiredAccuracy:kIALocationAccuracyBest]; + } else if ([mode isEqualToString:@"LOW_POWER"]) { + [self.IAlocationInfo setDesiredAccuracy:kIALocationAccuracyLow]; + } else if ([mode isEqualToString:@"CART"]) { + [self.IAlocationInfo setDesiredAccuracy:kIALocationAccuracyBestForCart]; + } else { + NSLog(@"Invalid positioning mode %@", mode); + return; + } + NSLog(@"Positioning mode set to %@", mode); } - (void)getFloorCertainty:(CDVInvokedUrlCommand *)command @@ -603,6 +621,25 @@ - (void)removeWayfindingUpdates:(CDVInvokedUrlCommand *)command [self.IAlocationInfo stopMonitoringForWayfinding]; } +- (void)requestWayfindingRoute:(CDVInvokedUrlCommand *)command +{ + NSDictionary *from_ = [command argumentAtIndex:0]; + NSDictionary *to_ = [command argumentAtIndex:1]; + + IALatLngFloor *from = [IALatLngFloor latLngFloorWithLatitude:[[from_ valueForKey:@"latitude"] doubleValue] + andLongitude:[[from_ valueForKey:@"longitude"] doubleValue] + andFloor:[[from_ valueForKey:@"floor"] integerValue]]; + IALatLngFloor *to = [IALatLngFloor latLngFloorWithLatitude:[[to_ valueForKey:@"latitude"] doubleValue] + andLongitude:[[to_ valueForKey:@"longitude"] doubleValue] + andFloor:[[to_ valueForKey:@"floor"] integerValue]]; + + NSLog(@"locationManager::requestWayfindingRoute from %f, %f, %d to %f, %f, %d", from.latitude, from.longitude, from.floor, to.latitude, to.longitude, to.floor); + + [self.IAlocationInfo requestWayfindingRouteFrom:from to:to callback:^(IARoute *route) { + [self returnRouteInformation:route callbackId:command.callbackId andKeepCallback:NO]; + }]; +} + - (IAPolygonGeofence *)geofenceFromDictionary:(NSDictionary *)geofence { NSDictionary *geometry = [geofence objectForKey:@"geometry"]; @@ -790,7 +827,7 @@ - (void)location:(IndoorAtlasLocationService *)manager didUpdateLocation:(IALoca - (void)location:(IndoorAtlasLocationService *)manager didUpdateRoute:(nonnull IARoute *)route { - [self returnRouteInformation:route]; + [self returnRouteInformation:route callbackId:_addRouteUpdateCallbackID andKeepCallback:YES]; } - (void)location:(IndoorAtlasLocationService *)manager didFailWithError:(NSError *)error diff --git a/www/IndoorAtlas.js b/www/IndoorAtlas.js index b74e0c7..92ba5d9 100644 --- a/www/IndoorAtlas.js +++ b/www/IndoorAtlas.js @@ -86,7 +86,7 @@ function IndoorAtlas() { var indoorLock = false; var floorLock = null; var wayfindingDestination = null; - var distanceFilter = null; + var positioningOptions = null; var orientationFilter = null; var regionChangeObserver = null; var statusHasBeenAvailable = false; @@ -129,6 +129,12 @@ function IndoorAtlas() { if (callbacks.onWayfindingRoute) callbacks.onWayfindingRoute(new Route(route)); }); } + + function requestWayfindingRoute(from, to, onWayfindingRoute) { + native('requestWayfindingRoute', [from, to], function (route) { + onWayfindingRoute(new Route(route)); + }); + } function stopWayfinding() { native('removeWayfindingUpdates', []); @@ -162,8 +168,17 @@ function IndoorAtlas() { }, error); // before addWatch to avoid unnecessary pos restart - if (distanceFilter && distanceFilter.minChangeMeters > 0) { - native('setDistanceFilter', [distanceFilter.minChangeMeters]); + if (positioningOptions && (positioningOptions.minChangeMeters > 0 || positioningOptions.minIntervalSeconds > 0)) { + var minChangeMeters = positioningOptions.minChangeMeters || 0; + var minIntervalSeconds = positioningOptions.minIntervalSeconds || 0; + native('setOutputThresholds', [minChangeMeters, minIntervalSeconds]); + } + + if (positioningOptions && positioningOptions.positioningMode) { + if (!['HIGH_ACCURACY','LOW_POWER','CART'].includes(positioningOptions.positioningMode)) { + throw new Error('Invalid positioning mode: ' + positioningOptions.positioningMode); + } + native('setPositioningMode', [positioningOptions.positioningMode]); } native('addWatch', [DEFAULT_WATCH_ID, null], function (data) { @@ -209,7 +224,7 @@ function IndoorAtlas() { native('removeStatusCallback', []); native('removeWayfindingUpdates', []); // just in case - distanceFilter = null; + positioningOptions = null; orientationFilter = null; // reset locks @@ -276,10 +291,10 @@ function IndoorAtlas() { } var config = [apiKey, 'dummy-secret']; + // plugin version + config.push(cordovaPluginMetadata['cordova-plugin-indooratlas']); if (getDeviceType() == 'Android') { // get permission - config.push(cordovaPluginMetadata['cordova-plugin-indooratlas']); - native('getPermissions', [], function () { native('initializeIndoorAtlas', config, initSuccess); }); @@ -297,11 +312,18 @@ function IndoorAtlas() { * * @param {function(Position)} onPosition a callback that executes when the * position changes with an `IndoorAtlas.Position` object as the parameter. - * @param {object} options distance filter options (optional) + * @param {object} options positioning options (optional) * @param {number} options.minChangeMeters (optional) Distance filter. * If set, determines the minimum distance (in meters) the position has to * change before the next position is reported. If not set, all changes in * position are returned, which happens approximately once a second. + * @param {number} options.minIntervalSeconds (optional) Time filter. + * The minimum amount of time measured in seconds that must be elapsed + * before an update event is generated. Setting this to 0 disables time based updates. + * Maximum update frequency is determined from values of distance filter and time filter. + * Update is generated when either of conditions specified by these filters are met. Default value is 2. + * @param {string} options.positioningMode (optional) positioning mode. + * One of: `HIGH_ACCURACY`, `LOW_POWER` or `CART`. Default is `HIGH_ACCURACY` * @return {object} returns `this` to allow chaining * @example * IndoorAtlas.watchPosition(position => { @@ -318,8 +340,7 @@ function IndoorAtlas() { } callbacks.onLocation = onPosition; - // TODO: low-power mode - distanceFilter = options; + positioningOptions = options; if (initialized) startPositioning(); // otherwise successful initialization will start positioning @@ -511,6 +532,37 @@ function IndoorAtlas() { return self; }; + /** + * Request a single-shot wayfinding route. + * Also a {@link #POI} can be given as a source or destination. + * + * @param {(object | POI)} from + * @param {number} from.latitude Starting location latitude in degrees + * @param {number} from.longitude Starting location longitude in degrees + * @param {number} from.floor Starting location floor number as defined in + * the mapping phase + * @param {(object | POI)} to + * @param {number} to.latitude Destination latitude in degrees + * @param {number} to.longitude Destination longitude in degrees + * @param {number} to.floor Destination floor number as defined in + * the mapping phase + * @param {function(Route)} onWayfindingRoute a callback that executes + * with the shortest route to the + * given destination as an object `{ legs }`, where `legs` is a + * list of `IndoorAtlas.RouteLeg` objects. + * @return {object} returns `this` to allow chaining + * @example + * const from = { latitude: 60.16, longitude: 24.95, floor: 2 }; + * const to = { latitude: 60.161, longitude: 24.951, floor: 3 }; + * IndoorAtlas.requestWayfindingRoute(from, to, route => { + * console.log(`the route has ${route.legs.length} leg(s)`); + * }); + */ + this.requestWayfindingRoute = function (from, to, onWayfindingRoute) { + if (initialized) requestWayfindingRoute(from, to, onWayfindingRoute); + return self; + }; + // --- Geofences /** diff --git a/www/Route.js b/www/Route.js index f10c175..90bafd3 100644 --- a/www/Route.js +++ b/www/Route.js @@ -2,15 +2,25 @@ var RoutingLeg = require('./RoutingLeg'); /** * Describes a wayfinding route consisting of `IndoorAtlas.RoutingLeg`s. - * Obtained with {@link #requestWayfindingUpdates} + * Obtained with {@link #requestWayfindingUpdates} or {@link #requestWayfindingRoute} */ -var Route = function(data) { - if (data) { +var Route = function(route) { + if (route) { + /** + * Defines the error status of a wayfinding request . One of `NO_ERROR`, `ROUTING_FAILED` or `GRAPH_NOT_AVAILABLE`. + * @type {string} + */ + this.error = route.error; + /** + * Whether route is available. + * @type {boolean} + */ + this.isSuccessful = this.error == 'NO_ERROR'; /** * List of legs in this route * @type {RoutingLeg[]} */ - this.legs = data.legs.map(function (leg) { + this.legs = route.legs.map(function (leg) { return new RoutingLeg(leg); }); }