Source code for apps.geofencing.middleware.atlas.atlas

import json
from bson.binary import UUID
import uuid
from datetime import datetime
from apps.geofencing.dbmodels.mongodb.content_delivery.geo_regions import geofences, beacons
from apps.geofencing.extensions.mongoengine import mongodb
from flask import current_app

# Mongo DB spherical coordinate system uses Radians as the unit
# of distance. So we need to divide the distance in specified
# units by the radius of the earth in the same units to get the
# distance in radians
RADIUS_EARTH_M = 3963
RADIUS_EARTH_MTR = 6378100
RADIUS_EARTH_FT = 20925524.9
RADIUS = {
    'meter': RADIUS_EARTH_MTR,
    'mile': RADIUS_EARTH_M,
    'feet': RADIUS_EARTH_FT
}


[docs]class Atlas(object): """ API wrapper for queries for mongodb Fence database """ @classmethod def generateFenceId(cls): return UUID(uuid.uuid4().hex) @classmethod def createGeoPoint(cls, lat, lon): return dict(type='Point', coordinates=[lon, lat]) @classmethod def createGeoPolygon(cls, rings): poly = dict(type='Polygon', coordinates=[]) for ring in rings: r = [] for point in ring: p = [point['lon'], point['lat']] r.append(p) poly['coordinates'].append(r) return poly def createAddressComponents(self, vertex): try: return geofences.AddressComponents(**vertex) except KeyError: current_app.logger.exception("Key Error in parsing the address information")
[docs] def createFence(self, center, group_id, radius=0, address=None, save=True, reload=False, **kwargs): """ Create a 2D circular geofence object in the geofence databases. :: :param geojson center: geojson point object - { type: "Point", coordinates: [ lon, lat ] } :param int group_id: :param float radius: radius in meters. Defaults to 0 if not initialized :param address: AddressComponents embedded document or dictionary of address components. :param [string] labels: :param dict tags: dictionary of tags to add to the geofence .i.e name tag, industry tags etc :param kwargs: :return: newly saved geofence object """ if address and not isinstance(address, geofences.AddressComponents): address = self.createAddressComponents(address) fence = geofences.Fence( fence_id=self.generateFenceId(), group_id=group_id, radius=radius, center=center ) if address: fence.address_components = address for k, v in kwargs.items(): try: setattr(fence, k, v) except KeyError: current_app.logger.warning('Key {0} is not a valid geofence attribute'.format(k)) if save: fence.save() if reload: fence.reload() return fence
@classmethod def calculateCentroid(cls, linear_ring): x_sum = 0 y_sum = 0 size = len(linear_ring) for vertex in linear_ring: x_sum = x_sum + vertex[0] y_sum = y_sum + vertex[1] return [x_sum / size, y_sum / size]
[docs] def createPolygonalFence(self, group_id=None, polygon=None, address=None, **kwargs): """ Accepts a polygon in geojson format to create a polygonal geofence. Example of a geojson polygon is given below. :: { "type": "Polygon", "coordinates": [ [ [ -87.67525, 41.990973 ], [ -87.674799, 41.98331 ], [ -87.689733, 41.990249 ], [ -87.67525, 41.990973 ] ] ] } :param group_id: string value :param polygon: geo-json polygon :param address: dictionary of address information :return: """ if address and not isinstance(address, geofences.AddressComponents): address = self.createAddressComponents(address) center = self.calculateCentroid(polygon['coordinates'][0]) fence = geofences.PolygonalFence( fence_id=self.generateFenceId(), center=center, radius=0, geometry=polygon, group_id=group_id, ) if address: fence.address_components = address for k, v in kwargs.items(): try: setattr(fence, k, v) except KeyError: current_app.logger.warning('Key {0} is not a valid geofence attribute'.format(k)) fence.save() fence.reload() return fence
def createBeaconRegion(self, group_id=None, address_info=None, details=None, labels=[]): beacon = beacons.Beacon() beacon_region = beacons.BeaconRegion(center=address_info['coordinates'], beacon=beacon) beacon_region.save()
[docs] def createActiveTimeWindows(self, time_windows): """ Takes a list of time window dictionries and creates a list of time window embedded documents. Below is an example of a time window. :: [ { "start": { "sec": 0, "hour": 0, "min": 0 }, "end": { "sec": 59, "hour": 23, "min": 59 }, "tzname": "Africa/Abidjan" } ] :param [dict] time_windows: list ditcionaries. Each dictionarty should include hour_st, min_st, sec_st, hour_end, min_end and sec_end keys :param string tzname: :return: list of timewindow embedded documents """ out = [] for window in time_windows: time_win_doc = geofences.ActiveTimeWindows( start={ 'hour': int(window['start']['hour']), 'min': int(window['start']['min']), 'sec': int(window['start']['sec']) }, end={ 'hour': int(window['end']['hour']), 'min': int(window['end']['min']), 'sec': int(window['end']['sec']) }, tzname=window['tzname'] ) current_app.logger.info(time_win_doc.tzname) out.append(time_win_doc) return out
def createActiveTempRanges(self, temp_range, **kwargs): return [geofences.ActiveTemperatureRange(**args) for args in temp_range]
[docs] def createFenceTriggers(self, active_time_windows=None, temp_ranges=None, precipitation=None, **kwargs): """ Create an embedded document containing all of the fence trigger parameters. :param [dict] active_time_windows: :param [dict] temp_range: :param precipitation: dictionary {'rain': value, 'snow': %, 'sleet': %, 'hail': %} :param kwargs: """ trigger = geofences.FenceTriggers() if kwargs: # Todo: Add validation for k, v in kwargs.items(): setattr(trigger, k, v) if active_time_windows: trigger.active_time_windows = self.createActiveTimeWindows(active_time_windows) if temp_ranges: temp_range = self.createActiveTempRanges(temp_ranges) trigger.temp_ranges = temp_range if precipitation: trigger.precipitation = precipitation # precipitation --> return trigger
[docs] def getGeoFence(self, fence_id, deleted=False, type=''): """ Grabs a geofence from the DB by the fence_id field :param fence_id: client facing fence id value :return: a single geofence object """ collection = geofences.GeoFence if type.lower() == 'fence': collection = geofences.Fence if type.lower() == 'polygonal': collection = geofences.PolygonalFence # may need to wrap the fence_id in the objectId field fence_query = collection.objects(mongodb.Q(fence_id=fence_id) & mongodb.Q(deleted=deleted)) if fence_query.count() > 1: current_app.logger.warning( '{1} Duplicate fence ID exists for non deleted fence: {0}'.format(fence_id, fence_query.count())) return fence_query.first()
def getFence(self, fence_id, deleted=False): return self.getGeoFence(fence_id, deleted=deleted, type='fence') def getPolygonalFence(self, fence_id, deleted=False): return self.getGeoFence(fence_id, deleted=deleted, type='polygonal')
[docs] def getGeoFences(self, deleted=False, fence_type='', **kwargs): """ Query for all fences matching passed filter parameters :param deleted: :param string fence_type: fence or polygonal :param kwargs: key-value pairs of attributes to filter the collection by. :return: Mongoengine Base Query set """ collection = geofences.GeoFence.objects if fence_type: if fence_type.lower == 'fence': collection = geofences.Fence.objects if fence_type.lower == 'polygonal': collection == geofences.PolygonalFence.objects fence_query = collection(mongodb.Q(**kwargs) & mongodb.Q(deleted=deleted)) return fence_query.all()
[docs] def getGeofencesInGroups(self, group_ids, deleted=False, type='', **kwargs): """ Query for all fences matching passed filter parameters and in the campaign groups passed in :param deleted: :param string type: fence or polygonal :param kwargs: key-value pairs of attributes to filter the collection by. :return: Mongoengine Base Query set """ collection = geofences.GeoFence if type: if type.lower == 'fence': collection = geofences.Fence if type.lower == 'polygonal': collection == geofences.PolygonalFence fence_query = collection.objects( mongodb.Q(group_id__in=group_ids) & mongodb.Q(**kwargs) & mongodb.Q(deleted=deleted)) return fence_query.all()
[docs] def getFencesWithinSphere(self, group_ids, lat, lon, radius, unit='meter', **kwargs): """ Retrieves geofences within a given radius :param [int] group_ids: list of group ids to include in query :param lat: :param lon: :param radius: radial search distance :param unit: feet, miles or meters :param kwargs: additional filter attributes :return: mongoengine query object """ # Todo: incorporate kwargs filters query = mongodb.Q(center__geo_within_sphere=[(lon, lat), float(radius / Atlas.RADIUS[unit])]) \ & mongodb.Q(status__nin=['inactive']) \ & mongodb.Q(deleted=False) if group_ids: query = query & mongodb.Q(group_id__in=group_ids) return geofences.GeoFence.objects(query)
[docs] def getClosestFences(self, lat, lon, radius=1000, limit=19, group_ids=None, **kwargs): """ Query the database for the set of fences closest to a location. :param float lat: WGS84 latitude coordinate :param float lon: WGS84 longitude coordinate :param radius: radial search distance in meters :param limit: max number of fences to be returned :param [int] group_ids: list of group ids to be returned :param kwargs: additional keyword arguments to further filter the query by :return: base queryset object with the results """ query = { 'center': { '$near': { '$geometry': { 'type': 'Point', 'coordinates': [lon, lat] }, '$maxDistance': radius # Assumed meters by MongoDB } }, 'deleted': False } if group_ids: query['group_id'] = {'$in': group_ids} for k, v in kwargs.items(): # Todo: validate and clean allowed key-values before adding to the query query[k] = v # Note, the limit() no longer limits the results on the at the query level only at the application level. # Ex. results.count != len(results.all()) fences = geofences.GeoFence.objects(__raw__=query).limit(limit) return fences.all_fields()
def updateFence(self, fence): if fence is None: raise TypeError('None type object is not valid') # Delete the old copy oid = fence.id old_fence = geofences.GeoFence.objects(id=oid).first() old_fence.deleted = True old_fence.last_modified_dt = datetime.utcnow() old_fence.save() # Create new version object based on updated values fence.id = None fence.last_modified_dt = datetime.utcnow() fence.save() return fence
[docs] def updateFenceRadius(self, fence, radius, delay_save=False): """ update radius of existing fence :param geofence object with radius attribute :param radius in meters """ try: fence.radius = radius except AttributeError: fence = self.getGeoFence(fence) fence.radius = radius if not delay_save: self.updateFence(fence) return fence
[docs] def updateFenceTriggerDirection(self, fence, direction, delay_save=False): """ Update trigger direction of existing fence Keyword Arguments: fence_id -- unique id for the fence document: string direction -- indicates whether the fence should be triggered on entry or exit """ if not hasattr(fence, 'fence_id'): fence = self.getGeoFence(fence) if direction == 'exit': fence.direction = 'exit' if direction == 'entry': fence.direction = 'entry' if not delay_save: self.updateFence(fence)
@classmethod
[docs] def deleteFence(cls, fence): """ delete a fence given a document id Keyword Arguments: fence_id -- unique id for the fence document: string """ try: fence.deleted = True except AttributeError: fence = cls().getFence(fence) fence.deleted = True fence.last_modified_dt = datetime.utcnow() fence.save()
def is_inside_fence(self, lat, lon, group_ids=None): # 1. search for closest fence if not group_ids: group_ids = None closest_fence = self.getClosestFences(lat, lon, radius=1000, limit=1, group_ids=group_ids).first() if closest_fence is None: return None if closest_fence._class_name == 'GeoFence.Fence': radius = closest_fence.radius inside_fence = self.getClosestFences(lat, lon, radius=radius, limit=1, group_ids=group_ids).first() return inside_fence if closest_fence._class_name == 'GeoFence.PolygonalFence': inside_fence = self.is_inside_polygonal_fence(closest_fence.geometry) # Todo: Filter by fence group ids! return inside_fence return None @classmethod def is_inside_polygonal_fence(cls, polygon): fence = geofences.GeoFence.objects(__raw__={ 'center': { '$geoWithin': { '$geometry': polygon } }, 'deleted': False }).limit(1) return fence
[docs] def updateFenceGroupId(self, fence, group_id): """ Takes a fence id and updates it group id value. """ if not hasattr(fence, 'fence_id'): fence = self.getGeoFence(fence) fence.group_id = group_id self.updateFence(fence)
[docs] def updateContentPool(self, fence, pool, delay_save=False): """ Add a content pool to a geofence :param fence: Geofence or fence_id :param pool: Content Pool object to add :return: None """ if not hasattr(fence, 'fence_id'): fence = self.getGeoFence(fence) fence.content_pool.append(pool) if not delay_save: self.updateFence(fence)
def updateFenceContent(self, fence, content, delay_save=False): if not hasattr(fence, 'fence_id'): fence = self.getGeoFence(fence) fence.content = content if not delay_save: self.updateFence(fence) def dropCollections(self, *args, **kwargs): geofences.GeoFence.drop_collection() geofences.Fence.drop_collection() geofences.PolygonalFence.drop_collection() geofences.FenceTriggerHistory.drop_collection() return self