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