diff --git a/CHANGELOG.md b/CHANGELOG.md index 181444f..118ace9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +Version 3.2.1 - March 2020 +---------------- + * Add geofence and POI support + * Update IndoorAtlas SDKs to 3.2.1 + Version 3.1.4 - February 2020 ---------------- * Update IndoorAtlas SDKs to 3.1.4 diff --git a/package.json b/package.json index 89854df..c9ed322 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-plugin-indooratlas", - "version": "3.1.4", + "version": "3.2.1", "description": "Cordova plugin using IndoorAtlas SDK.", "cordova": { "id": "cordova-plugin-indooratlas", diff --git a/plugin.xml b/plugin.xml index aa936c5..fa66615 100644 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="3.2.1"> IndoorAtlas IndoorAtlas Cordova Plugin. @@ -41,6 +41,12 @@ + + + + + + @@ -50,14 +56,6 @@ Platform location requested for better indoor positioning experience. - - - name - cordova - version - 3.0.2 - - diff --git a/src/android/IALocationPlugin.java b/src/android/IALocationPlugin.java index f887605..ba56f29 100644 --- a/src/android/IALocationPlugin.java +++ b/src/android/IALocationPlugin.java @@ -1,5 +1,10 @@ package com.ialocation.plugin; +import java.util.List; +import java.util.ArrayList; +import java.util.stream.Collectors; +import java.util.Collections; + import android.Manifest; import android.content.pm.PackageManager; import android.os.Build; @@ -15,6 +20,8 @@ import com.indooratlas.android.sdk.IAOrientationRequest; import com.indooratlas.android.sdk.IAOrientationListener; import com.indooratlas.android.sdk.IAWayfindingRequest; +import com.indooratlas.android.sdk.IAGeofence; +import com.indooratlas.android.sdk.IAGeofenceRequest; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; @@ -191,6 +198,16 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo requestWayfindingUpdates(lat, lon, floor, callbackContext); } else if ("removeWayfindingUpdates".equals(action)) { removeWayfindingUpdates(); + } else if ("watchGeofences".equals(action)) { + watchGeofences(callbackContext); + } else if ("clearGeofenceWatch".equals(action)) { + clearGeofenceWatch(); + } else if ("addDynamicGeofence".equals(action)) { + JSONObject geofence = args.getJSONObject(0); + addDynamicGeofence(geofence); + } else if ("removeDynamicGeofence".equals(action)) { + JSONObject geofence = args.getJSONObject(0); + removeDynamicGeofence(geofence); } else if ("lockFloor".equals(action)) { int floorNumber = args.getInt(0); lockFloor(floorNumber); @@ -370,6 +387,91 @@ public void run() { }); } + private IAGeofence geofenceFromJsonObject(JSONObject geoJson) { + try { + List iaEdges = new ArrayList<>(); + JSONObject geometry = geoJson.getJSONObject("geometry"); + JSONObject properties = geoJson.getJSONObject("properties"); + JSONArray coordinates = geometry.getJSONArray("coordinates"); + JSONArray linearRing = coordinates.getJSONArray(0); + for (int i = 0; i < linearRing.length(); i++) { + JSONArray vertex = linearRing.getJSONArray(i); + iaEdges.add(new double[] {vertex.getDouble(1), vertex.getDouble(0)}); + } + JSONObject payload = new JSONObject(); + if (properties.has("payload")) { + payload = properties.getJSONObject("payload"); + } + IAGeofence iaGeofence = new IAGeofence.Builder() + .withId(geoJson.getString("id")) + .withName(properties.getString("name")) + .withFloor(properties.getInt("floor")) + .withPayload(payload) + .withEdges(iaEdges) + .build(); + return iaGeofence; + } catch (JSONException e) { + Log.e(TAG, "error reading geofence geojson: " + e.getMessage()); + throw new IllegalStateException(e.getMessage()); + } + } + + private void watchGeofences(CallbackContext callbackContext) { + getListener(this).addGeofences(callbackContext); + cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + mLocationManager.addGeofences( + new IAGeofenceRequest.Builder() + .withCloudGeofences(true) + .build(), + getListener(IALocationPlugin.this) + ); + } + }); + } + + private void clearGeofenceWatch() { + getListener(this).removeGeofenceUpdates(); + cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + mLocationManager.removeGeofenceUpdates(getListener(IALocationPlugin.this)); + } + }); + } + + private void addDynamicGeofence(JSONObject geoJson) { + IAGeofence iaGeofence = geofenceFromJsonObject(geoJson); + cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + mLocationManager.addGeofences( + new IAGeofenceRequest.Builder() + .withGeofence(iaGeofence) + .withCloudGeofences(true) + .build(), + getListener(IALocationPlugin.this) + ); + } + }); + } + + private void removeDynamicGeofence(JSONObject geoJson) { + List geofenceIds = new ArrayList<>(); + try { + geofenceIds.add(geoJson.getString("id")); + } catch (JSONException e) { + throw new IllegalStateException(e.getMessage()); + } + cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + mLocationManager.removeGeofences(geofenceIds); + } + }); + } + private void lockFloor(final int floorNumber) { cordova.getActivity().runOnUiThread(new Runnable() { @Override diff --git a/src/android/IndoorLocationListener.java b/src/android/IndoorLocationListener.java index 7e3fe19..76af4db 100644 --- a/src/android/IndoorLocationListener.java +++ b/src/android/IndoorLocationListener.java @@ -10,6 +10,10 @@ import com.indooratlas.android.sdk.IARoute; import com.indooratlas.android.sdk.IAOrientationListener; import com.indooratlas.android.sdk.IAWayfindingListener; +import com.indooratlas.android.sdk.IAGeofence; +import com.indooratlas.android.sdk.IAGeofenceEvent; +import com.indooratlas.android.sdk.IAGeofenceListener; +import com.indooratlas.android.sdk.IAPOI; import com.indooratlas.android.sdk.resources.IAFloorPlan; import com.indooratlas.android.sdk.resources.IALatLng; import com.indooratlas.android.sdk.resources.IAVenue; @@ -26,7 +30,8 @@ /** * Handles events from IALocationListener and IARegion.Listener and relays them to Javascript callbacks. */ -public class IndoorLocationListener implements IALocationListener, IARegion.Listener, IAOrientationListener, IAWayfindingListener { +public class IndoorLocationListener implements IALocationListener, IARegion.Listener, IAOrientationListener, + IAWayfindingListener, IAGeofenceListener { private static final String TAG = "IndoorLocationListener"; private static final int TRANSITION_TYPE_UNKNOWN = 0; @@ -39,6 +44,7 @@ public class IndoorLocationListener implements IALocationListener, IARegion.List private CallbackContext headingUpdateCallbackContext; private CallbackContext statusUpdateCallbackContext; private CallbackContext wayfindingUpdateCallbackContext; + private CallbackContext geofenceCallbackContext; private ArrayList mCallbacks = new ArrayList(); private CallbackContext mCallbackContext; public IALocation lastKnownLocation = null; @@ -134,6 +140,14 @@ public void requestWayfindingUpdates(CallbackContext callbackContext) { wayfindingUpdateCallbackContext = callbackContext; } + public void addGeofences(CallbackContext callbackContext) { + geofenceCallbackContext = callbackContext; + } + + public void removeGeofenceUpdates() { + geofenceCallbackContext = null; + } + /** * Returns the sum of the all callback collections * @return @@ -269,6 +283,16 @@ private JSONObject getRegionJSONFromIARegion(IARegion iaRegion, int transitionTy venueFloorPlans.put(getFloorPlanJSONFromIAFloorPlan(venueFloorPlan)); } venueData.put("floorPlans", venueFloorPlans); + JSONArray venueGeofences = new JSONArray(); + for (IAGeofence geofence : venue.getGeofences()) { + venueGeofences.put(getGeojsonFromIaGeofence(geofence)); + } + venueData.put("geofences", venueGeofences); + JSONArray venuePois = new JSONArray(); + for (IAPOI poi : venue.getPOIs()) { + venuePois.put(getPoiJsonFromIaPoi(poi)); + } + venueData.put("pois", venuePois); regionData.put("venue", venueData); } @@ -565,4 +589,98 @@ public void onWayfindingUpdate(IARoute route) { wayfindingUpdateCallbackContext.sendPluginResult(pluginResult); } } + + private JSONObject getGeojsonFromIaGeofence(IAGeofence iaGeofence) { + try { + JSONObject geoJson = new JSONObject(); + + JSONObject properties = new JSONObject(); + properties.put("name", iaGeofence.getName()); + properties.put("floor", iaGeofence.getFloor()); + properties.put("payload", iaGeofence.getPayload()); + geoJson.put("properties", properties); + + JSONObject geometry = new JSONObject(); + JSONArray coordinates = new JSONArray(); + JSONArray linearRing = new JSONArray(); + for (double[] iaEdge : iaGeofence.getEdges()) { + JSONArray vertex = new JSONArray(); + vertex.put(iaEdge[1]); + vertex.put(iaEdge[0]); + linearRing.put(vertex); + } + geometry.put("type", "Polygon"); + coordinates.put(linearRing); + geometry.put("coordinates", coordinates); + + geoJson.put("type", "Feature"); + geoJson.put("id", iaGeofence.getId()); + geoJson.put("geometry", geometry); + + return geoJson; + } catch (JSONException e) { + throw new IllegalStateException(e.getMessage()); + } + } + + private JSONObject getPoiJsonFromIaPoi(IAPOI iaPoi) { + try { + JSONObject poi = new JSONObject(); + + JSONObject properties = new JSONObject(); + properties.put("name", iaPoi.getName()); + properties.put("floor", iaPoi.getFloor()); + properties.put("payload", iaPoi.getPayload()); + poi.put("properties", properties); + + JSONObject geometry = new JSONObject(); + JSONArray coordinates = new JSONArray(); + coordinates.put(iaPoi.getLocation().longitude); + coordinates.put(iaPoi.getLocation().latitude); + geometry.put("type", "Point"); + geometry.put("coordinates", coordinates); + + poi.put("type", "Feature"); + poi.put("id", iaPoi.getId()); + poi.put("geometry", geometry); + + return poi; + } catch (JSONException e) { + throw new IllegalStateException(e.getMessage()); + } + } + + // Cordova plugin uses string literals "ENTER" and "EXIT" instead of enums. + private String geofenceToRegionTransitionType(int geofenceTransitionType) { + if (geofenceTransitionType == IAGeofence.GEOFENCE_TRANSITION_ENTER) { + return "ENTER"; + } else if (geofenceTransitionType == IAGeofence.GEOFENCE_TRANSITION_EXIT) { + return "EXIT"; + } else { + return "UNKNOWN"; + } + } + + @Override + public void onGeofencesTriggered(IAGeofenceEvent event) { + if (geofenceCallbackContext != null) { + for (IAGeofence iaGeofence : event.getTriggeringGeofences()) { + try { + String transitionType = geofenceToRegionTransitionType(event.getGeofenceTransition()); + JSONObject geoJson = getGeojsonFromIaGeofence(iaGeofence); + JSONObject result = new JSONObject(); + result.put("transitionType", transitionType); + result.put("geoJson", geoJson); + PluginResult pluginResult = new PluginResult( + PluginResult.Status.OK, + result + ); + pluginResult.setKeepCallback(true); + geofenceCallbackContext.sendPluginResult(pluginResult); + } catch (JSONException e) { + throw new IllegalStateException(e.getMessage()); + } + } + } + } } diff --git a/src/android/indooratlas.gradle b/src/android/indooratlas.gradle index 485bed9..b04da5d 100644 --- a/src/android/indooratlas.gradle +++ b/src/android/indooratlas.gradle @@ -3,12 +3,9 @@ repositories { maven { url "https://indooratlas-ltd.bintray.com/mvn-public/" } - maven { - url "https://indooratlas-ltd.bintray.com/mvn-public-beta/" - } } dependencies { - compile 'com.android.support:appcompat-v7:27.0.2' - compile 'com.indooratlas.android:indooratlas-android-sdk:3.1.4' + implementation 'com.android.support:appcompat-v7:27.0.2' + implementation 'com.indooratlas.android:indooratlas-android-sdk:3.2.1' } diff --git a/src/ios/IndoorAtlas/IndoorAtlas.framework/Headers/IALocationManager.h b/src/ios/IndoorAtlas/IndoorAtlas.framework/Headers/IALocationManager.h index 903648f..1eabe08 100644 --- a/src/ios/IndoorAtlas/IndoorAtlas.framework/Headers/IALocationManager.h +++ b/src/ios/IndoorAtlas/IndoorAtlas.framework/Headers/IALocationManager.h @@ -13,6 +13,7 @@ INDOORATLAS_API extern NSString * _Nonnull const kIATraceId; @class IALocationManager; +@class IAGeofence; /** * Defines the type of region. @@ -69,31 +70,6 @@ typedef NS_ENUM(NSInteger, ia_status_type) { kIAStatusServiceLimited = 10, }; -/** - * Defines the device calibration quality. - * The quality of calibration affects location accuracy. - * @deprecated Deprecated since SDK 3.0 - */ -typedef NS_ENUM(NSInteger, ia_calibration) { - /** - * Quality is poor. - * @deprecated Deprecated since SDK 3.0 - */ - kIACalibrationPoor, - - /** - * Quality is good. - * @deprecated Deprecated since SDK 3.0 - */ - kIACalibrationGood, - - /** - * Quality is excellent. - * @deprecated Deprecated since SDK 3.0 - */ - kIACalibrationExcellent, -} __attribute__((deprecated("Deprecated since SDK 3.0"))); - /** * Defines the accuracy of location. */ @@ -108,8 +84,41 @@ typedef NS_ENUM(NSInteger, ia_location_accuracy) { * Locations with this accuracy are typically obtained with lowest amount of processing to reduce device power drain. */ kIALocationAccuracyLow, + + /** + * Best accuracy for cart use case. + * Use when device is mounted to a shopping cart or similar platform with wheels. + */ + kIALocationAccuracyBestForCart }; +/** + * Represents a point of interest. + */ +INDOORATLAS_API +@interface IAPOI : NSObject +/** + * Identifier of the point of interest + */ +@property (nonatomic, readonly, nonnull) NSString *identifier; +/** + * Name of the point of interest, can be empty + */ +@property (nonatomic, readonly, nullable) NSString *name; +/** + * The JSON payload for this point of interest. + */ +@property (nonatomic, readonly, nullable) NSDictionary *payload; +/** + * Location of the point of interest + */ +@property (nonatomic, readonly) CLLocationCoordinate2D coordinate; +/** + * Floor the point of interest is located on. + */ +@property (nonatomic, readonly, nonnull) IAFloor *floor; +@end + /** * Represents a venue in IndoorAtlas system */ @@ -127,6 +136,15 @@ INDOORATLAS_API * ID of the venue in IndoorAtlas developer console */ @property (nonatomic, strong, nonnull) NSString *id; +/** + * Geofences for this venue + */ +@property (nonatomic, readonly, nullable) NSArray *geofences; +/** + * Point of interests for this venue + */ +@property (nonatomic, readonly, nullable) NSArray *pois; + @end /** @@ -153,11 +171,11 @@ INDOORATLAS_API */ @property (nonatomic, strong, nullable) NSDate *timestamp; /** - * If there is a venue related to this region this is the venue of that. + * If this is a venue region then this will point to the venue object. */ @property (nonatomic, strong, nullable) IAVenue *venue; /** - * If there is a floorplan related to this region this is the floorplan of that. + * If this is a floorplan region then this will point to the floorplan object. */ @property (nonatomic, strong, nullable) IAFloorPlan *floorplan; @end @@ -478,14 +496,6 @@ INDOORATLAS_API */ - (void)indoorLocationManager:(nonnull IALocationManager*)manager statusChanged:(nonnull IAStatus*)status; -/** - * Tells that calibration quality changed. - * @param manager The location manager object that generated the event. - * @param quality The calibration quality at the time of the event. - * @deprecated Deprecated since SDK 3.0 - */ -- (void)indoorLocationManager:(nonnull IALocationManager*)manager calibrationQualityChanged:(enum ia_calibration)quality __attribute__((deprecated("Deprecated since SDK 3.0"))); - /** * Tells that extra information dictionary was received. This dictionary contains * identifier for debugging positioning. @@ -517,13 +527,6 @@ INDOORATLAS_API */ INDOORATLAS_API @interface IALocationManager : NSObject -/** - * The latest calibration quality value - * - * @param calibration See possible values at [ia_calibration](/Constants/ia_calibration.html) - * @deprecated Deprecated since SDK 3.0 - */ -@property (nonatomic, readonly) enum ia_calibration calibration __attribute__((deprecated("Deprecated since SDK 3.0"))); /** * The latest location update. @@ -647,8 +650,9 @@ INDOORATLAS_API - (void)unlockFloor; /** - * Lock or unlock positioning indoors. Disables indoor-outdoor detection as well as GPS scanning - * if locked. + * Lock or unlock positioning indoors. Disables indoor-outdoor detection when locked. + * Indoor lock is enabled by default. + * * @param lockIndoor boolean value indicating whether to lock or unlock indoor positioning */ - (void)lockIndoors:(bool)lockIndoor; @@ -660,7 +664,8 @@ INDOORATLAS_API /** * Set IndoorAtlas API key and secret for authentication. * - * This method must be called before further requests with server requiring authentication. + * This method must be called once before starting location updates. + * Changing API key at runtime will stop location updates and reset state. * * @param key API key used for authentication. * @param secret API secret used for authentication. diff --git a/src/ios/IndoorAtlas/IndoorAtlas.framework/IndoorAtlas b/src/ios/IndoorAtlas/IndoorAtlas.framework/IndoorAtlas index 3703383..55a07ce 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 ee9da1a..2d7c551 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 6064c20..2707a25 100644 --- a/src/ios/IndoorAtlasLocationService.h +++ b/src/ios/IndoorAtlasLocationService.h @@ -124,6 +124,9 @@ typedef NSUInteger IndoorLocationTransitionType; */ - (void)stopMonitoringForWayfinding; +- (void)startMonitoringGeofences:(IAGeofence *)geofence; +- (void)stopMonitoringGeofences:(IAGeofence *)geofence; + - (void)valueForDistanceFilter:(float *)distance; - (float)fetchFloorCertainty; - (NSString *)fetchTraceId; diff --git a/src/ios/IndoorAtlasLocationService.m b/src/ios/IndoorAtlasLocationService.m index 6863352..90927ec 100644 --- a/src/ios/IndoorAtlasLocationService.m +++ b/src/ios/IndoorAtlasLocationService.m @@ -240,4 +240,17 @@ - (void)stopMonitoringForWayfinding { [self.manager stopMonitoringForWayfinding]; } + +- (void)startMonitoringGeofences:(IAGeofence *)geofence +{ + NSLog(@"startMonitoringGeofence: %@, floor %ld", geofence.name, geofence.floor.level); + [self.manager startMonitoringForGeofence:geofence]; +} + +- (void)stopMonitoringGeofences:(IAGeofence *)geofence +{ + NSLog(@"stopMonitoringGeofence: %@, floor %ld", geofence.name, geofence.floor.level); + [self.manager stopMonitoringForGeofence:geofence]; +} + @end diff --git a/src/ios/IndoorLocation.h b/src/ios/IndoorLocation.h index 6ad15f7..12c4a75 100644 --- a/src/ios/IndoorLocation.h +++ b/src/ios/IndoorLocation.h @@ -20,6 +20,7 @@ enum IACurrentStatus { STATUS_AVAILABLE = 2, STATUS_LIMITED = 10 }; + typedef NSUInteger IndoorLocationStatus; // simple object to keep track of location information @@ -74,6 +75,10 @@ typedef NSUInteger IndoorLocationStatus; - (void)setSensitivities:(CDVInvokedUrlCommand *)command; - (void)requestWayfindingUpdates:(CDVInvokedUrlCommand *)command; - (void)removeWayfindingUpdates:(CDVInvokedUrlCommand *)command; +- (void)watchGeofences:(CDVInvokedUrlCommand *)command; +- (void)clearGeofenceWatch:(CDVInvokedUrlCommand *)command; +- (void)addDynamicGeofence:(CDVInvokedUrlCommand *)command; +- (void)removeDynamicGeofence:(CDVInvokedUrlCommand *)command; - (void)lockFloor:(CDVInvokedUrlCommand *)command; - (void)unlockFloor:(CDVInvokedUrlCommand *)command; - (void)lockIndoors:(CDVInvokedUrlCommand *)command; diff --git a/src/ios/IndoorLocation.m b/src/ios/IndoorLocation.m index 22305a4..c162831 100644 --- a/src/ios/IndoorLocation.m +++ b/src/ios/IndoorLocation.m @@ -52,6 +52,7 @@ @interface IndoorLocation () { @property (nonatomic, strong) NSString *addHeadingUpdateCallbackID; @property (nonatomic, strong) NSString *addStatusUpdateCallbackID; @property (nonatomic, strong) NSString *addRouteUpdateCallbackID; +@property (nonatomic, strong) NSString *addGeofenceUpdateCallbackID; @end @@ -254,7 +255,7 @@ - (void)returnAttitudeInformation:(double)x y:(double)y z:(double)z w:(double)w { if (_addAttitudeUpdateCallbackID != nil) { CDVPluginResult *pluginResult; - + NSNumber *secondsSinceRefDate = [NSNumber numberWithDouble:[timestamp timeIntervalSinceReferenceDate]]; NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:5]; [result setObject:secondsSinceRefDate forKey:@"timestamp"]; @@ -272,7 +273,7 @@ - (void)returnHeadingInformation:(double)heading timestamp:(NSDate *)timestamp { if (_addHeadingUpdateCallbackID != nil) { CDVPluginResult *pluginResult; - + NSNumber *secondsSinceRefDate = [NSNumber numberWithDouble:[timestamp timeIntervalSinceReferenceDate]]; NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:2]; [result setObject:secondsSinceRefDate forKey:@"timestamp"]; @@ -305,7 +306,7 @@ - (void)returnStatusInformation:(NSString *)statusString code:(NSUInteger) code { if (_addStatusUpdateCallbackID != nil) { CDVPluginResult *pluginResult; - + NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:2]; [result setObject:statusString forKey:@"message"]; [result setObject:[NSNumber numberWithUnsignedInteger:code] forKey:@"code"]; @@ -333,6 +334,16 @@ - (NSDictionary *)formatRegionInfo:(IARegion *)regionInfo andTransitionType:(Ind [floorplans addObject:[self floorPlanToDictionary:[regionInfo.venue.floorplans objectAtIndex:i]]]; } [venue setObject:floorplans forKey:@"floorPlans"]; + NSMutableArray *geofences = [[NSMutableArray alloc] initWithCapacity:regionInfo.venue.geofences.count]; + for (size_t i = 0; i < regionInfo.venue.geofences.count; i++) { + [geofences addObject:[self dictionaryFromGeofence:[regionInfo.venue.geofences objectAtIndex:i]]]; + } + [venue setObject:geofences forKey:@"geofences"]; + NSMutableArray *pois = [[NSMutableArray alloc] initWithCapacity:regionInfo.venue.pois.count]; + for (size_t i = 0; i < regionInfo.venue.pois.count; i++) { + [pois addObject:[self dictionaryFromPoi:[regionInfo.venue.pois objectAtIndex:i]]]; + } + [venue setObject:pois forKey:@"pois"]; [result setObject:venue forKey:@"venue"]; } if (regionInfo.floorplan != nil) { @@ -595,7 +606,7 @@ - (void)setPosition:(CDVInvokedUrlCommand *)command NSString *oFloor = [command argumentAtIndex:2]; NSString *oAcc = [command argumentAtIndex:3]; NSLog(@"locationManager::setPosition:: %@, %@, %@, %@", oLat, oLon, oFloor, oAcc); - + const double lat = [oLat doubleValue]; const double lon = [oLon doubleValue]; CLLocation *loc = [[CLLocation alloc] initWithLatitude:lat longitude:lon]; @@ -658,12 +669,12 @@ - (void)setSensitivities:(CDVInvokedUrlCommand *)command { NSString *oSensitivity = [command argumentAtIndex:0]; NSString *hSensitivity = [command argumentAtIndex:1]; - + double orientationSensitivity = [oSensitivity doubleValue]; double headingSensitivity = [hSensitivity doubleValue]; - + [self.IAlocationInfo setSensitivities: &orientationSensitivity headingSensitivity:&headingSensitivity]; - + CDVPluginResult *pluginResult; NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:1]; [result setObject:@"Sensitivities set" forKey:@"message"]; @@ -681,7 +692,7 @@ - (void)requestWayfindingUpdates:(CDVInvokedUrlCommand *)command const double lat = [oLat doubleValue]; const double lon = [oLon doubleValue]; const int floor = [oFloor intValue]; - + NSLog(@"locationManager::requestWayfindingUpdates %f, %f, %d", lat, lon, floor); IAWayfindingRequest *req = [IAWayfindingRequest alloc]; req.coordinate = CLLocationCoordinate2DMake(lat, lon); @@ -694,6 +705,113 @@ - (void)removeWayfindingUpdates:(CDVInvokedUrlCommand *)command [self.IAlocationInfo stopMonitoringForWayfinding]; } +- (IAPolygonGeofence *)geofenceFromDictionary:(NSDictionary *)geofence { + + NSDictionary *geometry = [geofence objectForKey:@"geometry"]; + NSArray *coordinates = [geometry valueForKey:@"coordinates"]; + NSArray *linearRing = coordinates[0]; + NSMutableArray *points = [[NSMutableArray alloc] init]; + for (int i = 0; i < [linearRing count]; i++) { + NSArray *vertex = linearRing[i]; + [points addObject:vertex[1]]; + [points addObject:vertex[0]]; + } + NSDictionary *properties = [geofence objectForKey:@"properties"]; + + IAFloor *iaFloor = [IAFloor floorWithLevel:[[properties valueForKey:@"floor"] integerValue]]; + IAPolygonGeofence *iaGeofence = [ + IAPolygonGeofence + polygonGeofenceWithIdentifier:[geofence objectForKey:@"id"] + andFloor:iaFloor + edges:points + ]; + iaGeofence.name = [properties objectForKey:@"name"]; + iaGeofence.payload = [properties objectForKey:@"payload"]; + return iaGeofence; +} + +- (NSDictionary *)dictionaryFromGeofence:(IAPolygonGeofence *)iaGeofence +{ + NSMutableDictionary *geoJson = [NSMutableDictionary dictionaryWithCapacity:4]; + [geoJson setObject:@"Feature" forKey:@"type"]; + [geoJson setObject:iaGeofence.identifier forKey:@"id"]; + + NSMutableDictionary *properties = [NSMutableDictionary dictionaryWithCapacity:2]; + [properties setObject:iaGeofence.name forKey:@"name"]; + [properties setObject:[NSNumber numberWithInt:iaGeofence.floor.level] forKey:@"floor"]; + if (iaGeofence.payload != nil) { + [properties setObject:iaGeofence.payload forKey:@"payload"]; + } + [geoJson setObject:properties forKey:@"properties"]; + + NSMutableDictionary *geometry = [NSMutableDictionary dictionaryWithCapacity:2]; + [geometry setObject:@"Polygon" forKey:@"type"]; + NSMutableArray *coordinates = [[NSMutableArray alloc] init]; + NSMutableArray *linearRing = [[NSMutableArray alloc] init]; + for (int i = 0; i < [iaGeofence.points count]; i += 2) { + NSMutableArray *vertex = [[NSMutableArray alloc] init]; + [vertex addObject:iaGeofence.points[i + 1]]; + [vertex addObject:iaGeofence.points[i]]; + [linearRing addObject:vertex]; + } + [coordinates addObject:linearRing]; + [geometry setObject:coordinates forKey:@"coordinates"]; + + [geoJson setObject:geometry forKey:@"geometry"]; + + return geoJson; +} + +- (NSDictionary *)dictionaryFromPoi:(IAPOI *)iaPoi +{ + NSMutableDictionary *poi = [NSMutableDictionary dictionaryWithCapacity:4]; + [poi setObject:@"Feature" forKey:@"type"]; + [poi setObject:iaPoi.identifier forKey:@"id"]; + + NSMutableDictionary *properties = [NSMutableDictionary dictionaryWithCapacity:3]; + [properties setObject:iaPoi.name forKey:@"name"]; + [properties setObject:[NSNumber numberWithInt:iaPoi.floor.level] forKey:@"floor"]; + if (iaPoi.payload != nil) { + [properties setObject:iaPoi.payload forKey:@"payload"]; + } + [poi setObject:properties forKey:@"properties"]; + + NSMutableDictionary *geometry = [NSMutableDictionary dictionaryWithCapacity:2]; + [geometry setObject:@"Point" forKey:@"type"]; + NSMutableArray *coordinates = [[NSMutableArray alloc] init]; + [coordinates addObject:[NSNumber numberWithDouble:iaPoi.coordinate.longitude]]; + [coordinates addObject:[NSNumber numberWithDouble:iaPoi.coordinate.latitude]]; + [geometry setObject:coordinates forKey:@"coordinates"]; + + [poi setObject:geometry forKey:@"geometry"]; + + return poi; +} + +- (void)watchGeofences:(CDVInvokedUrlCommand *)command +{ + self.addGeofenceUpdateCallbackID = command.callbackId; +} + +- (void)clearGeofenceWatch:(CDVInvokedUrlCommand *)command +{ + self.addGeofenceUpdateCallbackID = nil; +} + +- (void)addDynamicGeofence:(CDVInvokedUrlCommand *)command +{ + NSDictionary *geoJson = [command argumentAtIndex:0]; + IAPolygonGeofence *iaGeofence = [self geofenceFromDictionary:geoJson]; + [self.IAlocationInfo startMonitoringGeofences:iaGeofence]; +} + +- (void)removeDynamicGeofence:(CDVInvokedUrlCommand *)command +{ + NSDictionary *geoJson = [command argumentAtIndex:0]; + IAPolygonGeofence *iaGeofence = [self geofenceFromDictionary:geoJson]; + [self.IAlocationInfo stopMonitoringGeofences:iaGeofence]; +} + - (void)lockFloor:(CDVInvokedUrlCommand *)command { NSString *oFloor = [command argumentAtIndex:0]; @@ -791,21 +909,45 @@ - (void)location:(IndoorAtlasLocationService *)manager didFailWithError:(NSError } } +- (NSString *)getGeofenceTransitionType:(IndoorLocationTransitionType)enterOrExit +{ + if (enterOrExit == TRANSITION_TYPE_ENTER) { + return @"ENTER"; + } else if (enterOrExit == TRANSITION_TYPE_EXIT) { + return @"EXIT"; + } else { + return @"UNKNOWN"; + } +} + - (void)location:(IndoorAtlasLocationService *)manager didRegionChange:(IARegion *)region type:(IndoorLocationTransitionType)enterOrExit { if (region == nil) { return; } - IndoorRegionInfo *cData = self.regionData; - cData.region = region; - cData.regionStatus = enterOrExit; - if (self.regionData.watchCallbacks.count > 0) { - for (NSString *timerId in self.regionData.watchCallbacks) { - [self returnRegionInfo:[self.regionData.watchCallbacks objectForKey:timerId] andKeepCallback:YES]; + // Is the `isKindOfClass` check required? Are all geofences IAPolygonGeofences? + if (region.type == kIARegionTypeGeofence && [region isKindOfClass:[IAPolygonGeofence class]]) { + NSMutableDictionary *geofenceDict = [self dictionaryFromGeofence:region]; + NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:2]; + [result setObject:geofenceDict forKey:@"geoJson"]; + [result setObject:[self getGeofenceTransitionType:enterOrExit] forKey:@"transitionType"]; + CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:result]; + [pluginResult setKeepCallbackAsBool:YES]; + if (self.addGeofenceUpdateCallbackID) { + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.addGeofenceUpdateCallbackID]; } } else { - // No callbacks waiting on us anymore, turn off listening. - [self _stopLocation]; + IndoorRegionInfo *cData = self.regionData; + cData.region = region; + cData.regionStatus = enterOrExit; + if (self.regionData.watchCallbacks.count > 0) { + for (NSString *timerId in self.regionData.watchCallbacks) { + [self returnRegionInfo:[self.regionData.watchCallbacks objectForKey:timerId] andKeepCallback:YES]; + } + } else { + // No callbacks waiting on us anymore, turn off listening. + [self _stopLocation]; + } } } @@ -816,7 +958,7 @@ - (void)location:(IndoorAtlasLocationService *)manager didUpdateAttitude:(IAAtti double z = attitude.quaternion.z; double w = attitude.quaternion.w; NSDate *timestamp = attitude.timestamp; - + [self returnAttitudeInformation:x y:y z:z w:w timestamp:timestamp]; } @@ -824,7 +966,7 @@ - (void)location:(IndoorAtlasLocationService *)manager didUpdateHeading:(IAHeadi { double direction = heading.trueHeading; NSDate *timestamp = heading.timestamp; - + [self returnHeadingInformation:direction timestamp:timestamp]; } @@ -853,7 +995,7 @@ - (void)location:(IndoorAtlasLocationService *)manager statusChanged:(IAStatus * statusDisplay = @"Unspecified Status"; break; } - + [self returnStatusInformation:statusDisplay code:statusCode]; NSLog(@"IALocationManager status %d %@", status.type, statusDisplay) ; } diff --git a/www/Geofence.js b/www/Geofence.js new file mode 100644 index 0000000..4d34e68 --- /dev/null +++ b/www/Geofence.js @@ -0,0 +1,87 @@ +/** + * A geofence describes a region of interest. Entering or exiting geofences + * will trigger SDK events caught by {@link #watchGeofences}. The geofences + * contained in a {@link Venue} are sent together with the venue (see {@link #watchVenue} and {@link #watchPosition}). + */ +var Geofence = function(data) { + /** + * Unique identifier of the geofence + * @type {string} + * @example '12345678-90ab-cdef-1234-567890abcdef' + */ + this.id = data.id; + + /** + * Name of the geofence + * @type {string} + * @example 'Meeting room A101' + */ + this.name = data.name; + + /** + * Floor number of the geofence + * @type {number} + */ + this.floor = data.floor; + + /** + * WGS84 coordinates of the vertices of the geofence polygon + * @type {object[]} + * @example + * [ + * { latitude: 65.01, longitude: 25.50 }, + * { latitude: 65.02, longitude: 25.51 }, + * { latitude: 65.01, longitude: 25.52 }, + * { latitude: 65.01, longitude: 25.50 }, + * ] + */ + this.coordinates = data.coordinates; + + /** + * A JavaScript object parsed from the free-form JSON payload + */ + this.payload = data.payload; + + /** + * Convert geofence to GeoJSON representation (see {@link https://tools.ietf.org/html/rfc7946}). + * @return {object} The geofence as a GeoJSON Polygon feature. + */ + this.toGeoJSON = function () { + return { + type: 'Feature', + id: this.id, + properties: { + name: this.name, + floor: this.floor, + payload: this.payload + }, + geometry: { + type: 'Polygon', + coordinates: [this.coordinates.map(function (coord) { + return [coord.longitude, coord.latitude]; + })] + } + }; + }; +}; + +/** + * Convert a GeoJSON Polygon (see {@link https://tools.ietf.org/html/rfc7946}) to a geofence. + * @return {Geofence} Returns a {@link Geofence} object. + */ +Geofence.fromGeoJSON = function (geoJson) { + return new Geofence({ + id: geoJson.id, + name: geoJson.properties.name, + floor: geoJson.properties.floor, + coordinates: geoJson.geometry.coordinates[0].map(function (coord) { + return { + latitude: coord[1], + longitude: coord[0] + }; + }), + payload: geoJson.properties.payload || {} + }); +}; + +module.exports = Geofence; diff --git a/www/IndoorAtlas.js b/www/IndoorAtlas.js index 058399c..b74e0c7 100644 --- a/www/IndoorAtlas.js +++ b/www/IndoorAtlas.js @@ -5,9 +5,9 @@ * under the `IndoorAtlas` singleton object. For example the function * {@link #initialize} can be used in code as `IndoorAtlas.initialize(...)`. * - * The classes returned by the plugin, such as {@link FloorPlan} + * Most classes returned by the plugin, such as {@link FloorPlan} * (= `IndoorAtlas.FloorPlan`) are not supposed to be constructed by the user - * directly. + * directly. The only exception is {@link Geofence|IndoorAtlas.Geofence}. * * ## Basic usage * @@ -49,6 +49,7 @@ var RegionChangeObserver = require('./RegionChangeObserver'); var CurrentStatus = require('./CurrentStatus'); var Orientation = require('./Orientation'); var Route = require('./Route'); +var Geofence = require('./Geofence'); // --- Helper functions and constants (*not* in the global scope) @@ -133,6 +134,20 @@ function IndoorAtlas() { native('removeWayfindingUpdates', []); } + function requestGeofenceUpdates() { + if (debug) debug('add geofence watch'); + native('watchGeofences', [], function (result) { + if (callbacks.onTriggeredGeofence) { + callbacks.onTriggeredGeofence(result.transitionType, Geofence.fromGeoJSON(result.geoJson)); + } + }); + } + + function removeGeofenceUpdates() { + if (debug) debug('clear geofence watch'); + native('clearGeofenceWatch', []); + } + function startPositioning() { if (debug) debug('starting positioning'); @@ -256,6 +271,8 @@ function IndoorAtlas() { }); if (callbacks.onLocation) startPositioning(); + + if (callbacks.onTriggeredGeofence) requestGeofenceUpdates(); } var config = [apiKey, 'dummy-secret']; @@ -454,9 +471,9 @@ function IndoorAtlas() { /** * Request wayfinding from the current location of the user to the given - * coordinates + * coordinates. Also a {@link #POI} can be given as a destination. * - * @param {object} destination + * @param {(object | POI)} destination * @param {number} destination.latitude Destination latitude in degrees * @param {number} destination.longitude Destination longitude in degrees * @param {number} destination.floor Destination floor number as defined in @@ -494,6 +511,88 @@ function IndoorAtlas() { return self; }; + // --- Geofences + + /** + * Callback function triggered on geofence events + * + * @callback geofenceCallback + * @param {string} transitionType Event type, either `'ENTER'`, `'EXIT'` or `'UNKNOWN'` + * @param {#Geofence} geofence Triggered geofence. + */ + + /** + * Start monitoring for geofence events (entering or exiting geofences). + * + * @param {geofenceCallback} onTriggeredGeofence A callback which + * is executed when a geofence is entered or exited. + * @return {object} Returns `this` to allow chaining + */ + + this.watchGeofences = function (onTriggeredGeofence) { + callbacks.onTriggeredGeofence = onTriggeredGeofence; + if (initialized) { + requestGeofenceUpdates(); + } + return self; + }; + + /** + * Stop monitoring enter/exit events for geofences. + * + * @return {object} Returns `this` to allow chaining + */ + + this.clearGeofenceWatch = function() { + delete callbacks.onTriggeredGeofence; + if (initialized) { + removeGeofenceUpdates(); + } + return self; + }; + + /** + * Add a geofence to be monitored for enter/exit. + * + * @param {Geofence} geofence The geofence to be monitored. + * @return {object} Returns `this` to allow chaining + * + * @example + * IndoorAtlas.addDynamicGeofence(new IndoorAtlas.Geofence({ + * id: '12345678-90ab-cdef-1234-567890abcdef', + * name: 'My geofence', + * floor: 1, + * coordinates: [ + * [65.1234, 25.51234], + * [65.1254, 25.51234], + * [65.1254, 25.51334], + * [65.1234, 25.51334], + * [65.1234, 25.51234] + * ] + * })); + */ + this.addDynamicGeofence = function (geofence) { + native('addDynamicGeofence', [geofence.toGeoJSON()]); + return self; + } + + /** + * Removes a dynamic geofence from being monitored. + * + * @param {(string|Geofence)} geofence Either a {@link Geofence} object or a geofence ID string. + * @return Returns `this` to allow chaining + * @example Provide a whole geofence object + * var geofence = new IndoorAtlas.Geofence({ ... }); + * // ... + * IndoorAtlas.removeDynamicGeofence(geofence); + * @example Or just an geofence ID string + * IndoorAtlas.removeDynamicGeofence('12345678-90ab-cdef-1234-567890abcdef'); + */ + this.removeDynamicGeofence = function (geofence) { + native('removeDynamicGeofence', [geofence.toGeoJSON()]); + return self; + } + // --- Locks and explicit positions /** @@ -523,11 +622,10 @@ function IndoorAtlas() { }; /** - * Disable or re-enable indoor-outdoor detection + * Enable or disable indoor-outdoor detection. Disabled by default. * - * @param {boolean} locked if `true` keep positioning indoors and disable - * automatic indoor-outdoor detection. If `false`, re-enable automatic - * indoor-outdoor detection + * @param {boolean} locked if `false`, enable automatic indoor-outdoor detection. + * If `true`, disable automatic indoor-outdoor detection and keep positioning indoors. * @return {object} returns `this` to allow chaining * @example IndoorAtlas.lockIndoors(true); */ diff --git a/www/POI.js b/www/POI.js new file mode 100644 index 0000000..7d168be --- /dev/null +++ b/www/POI.js @@ -0,0 +1,76 @@ +/** + * POI, or Point Of Interest, is a special position inside a building + */ +var POI = function (data) { + /** + * Unique identifier of the POI + * @type {string} + */ + this.id = data.id; + + /** + * Name of the POI + * @type {string} + * @example 'Reception desk' + */ + this.name = data.name; + + /** + * Floor number where the POI is located + */ + this.floor = data.floor; + + /** + * WGS84 latitude coordinate of the POI + * @type {number} + */ + this.latitude = data.latitude; + + /** + * WGS84 longitude coordinate of the POI + * @type {number} + */ + this.longitude = data.longitude; + + /** + * A JavaScript object parsed from the free-form JSON payload + */ + this.payload = data.payload; + + /** + * Convert POI to GeoJSON representation (see {@link https://tools.ietf.org/html/rfc7946}). + * @return {object} The POI as a GeoJSON Point feature. + */ + this.toGeoJSON = function () { + return { + type: 'Feature', + id: this.id, + properties: { + name: this.name, + floor: this.floor, + payload: this.payload + }, + geometry: { + type: 'Point', + coordinates: [this.longitude, this.latitude] + } + }; + }; +}; + +/** + * Convert a GeoJSON Point feature (see {@link https://tools.ietf.org/html/rfc7946}) to a POI. + * @return {POI} Returns a {@link POI} object. + */ +POI.fromGeoJSON = function (geoJson) { + return new POI({ + id: geoJson.id, + name: geoJson.properties.name, + floor: geoJson.properties.floor, + latitude: geoJson.geometry.coordinates[1], + longitude: geoJson.geometry.coordinates[0], + payload: geoJson.properties.payload + }); +}; + +module.exports = POI; diff --git a/www/Venue.js b/www/Venue.js index 8124bbf..1dad18d 100644 --- a/www/Venue.js +++ b/www/Venue.js @@ -1,4 +1,6 @@ var FloorPlan = require('./FloorPlan'); +var Geofence = require('./Geofence'); +var POI = require('./POI'); /** * A data object describing a venue, also known as a _building_ or _location_. @@ -27,6 +29,22 @@ var Venue = function(data) { this.floorPlans = data.floorPlans.map(function (floorPlanData) { return new FloorPlan(floorPlanData); }); + + /** + * List of geofences in this venue + * @type {Geofence[]} + */ + this.geofences = data.geofences.map(function (geofenceData) { + return Geofence.fromGeoJSON(geofenceData); + }); + + /** + * List of POIs in this venue + * @type {POI[]} + */ + this.pois = data.pois.map(function (poiData) { + return POI.fromGeoJSON(poiData); + }); }; module.exports = Venue;