annotations_server.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 # Software License Agreement (BSD License)
00003 #
00004 # Copyright (c) 2014, Yujin Robot
00005 # All rights reserved.
00006 #
00007 # Redistribution and use in source and binary forms, with or without
00008 # modification, are permitted provided that the following conditions
00009 # are met:
00010 #
00011 #  * Redistributions of source code must retain the above copyright
00012 #    notice, this list of conditions and the following disclaimer.
00013 #  * Redistributions in binary form must reproduce the above
00014 #    copyright notice, this list of conditions and the following
00015 #    disclaimer in the documentation and/or other materials provided
00016 #    with the distribution.
00017 #  * Neither the name of Willow Garage, Inc. nor the names of its
00018 #    contributors may be used to endorse or promote products derived
00019 #    from this software without specific prior written permission.
00020 #
00021 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
00022 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
00023 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
00024 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
00025 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
00026 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
00027 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
00028 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
00029 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
00030 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
00031 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00032 # POSSIBILITY OF SUCH DAMAGE.
00033 #
00034 # Author: Jorge Santos
00035 
00036 import roslib.message
00037 import rospy
00038 import unique_id
00039 
00040 import warehouse_ros as wr
00041 
00042 from map_manager import *
00043 from yaml_database import *
00044 
00045 from world_canvas_msgs.msg import *
00046 from world_canvas_msgs.srv import *
00047 from world_canvas_utils.serialization import *
00048 
00049 
00050 class AnnotationsServer:
00051     
00052     ##########################################################################
00053     # Initialization
00054     ##########################################################################
00055 
00056     def __init__(self):
00057         # Set up collections: for every annotation, we store an AnnotationData message plus 1 or
00058         # more Annotation messages, the first containing the an id and a serialized representation
00059         # of a message of type annotation.type (data field)
00060         self.world_collection = \
00061             wr.MessageCollection("world_canvas", "worlds", WorldCanvas)
00062         self.world_collection.ensure_index("id", unique=True)
00063 
00064         self.anns_collection = \
00065             wr.MessageCollection("world_canvas", "annotations", Annotation)
00066         self.anns_collection.ensure_index("id", unique=True)
00067 
00068         self.data_collection = \
00069             wr.MessageCollection("world_canvas", "annotations_data", AnnotationData)
00070         self.data_collection.ensure_index("id", unique=True)
00071         
00072         # Set up services
00073         self.get_anns_srv = \
00074             rospy.Service('get_annotations',      GetAnnotations,     self.get_annotations)
00075         self.get_data_srv = \
00076             rospy.Service('get_annotations_data', GetAnnotationsData, self.get_annotations_data)
00077         self.pub_data_srv = \
00078             rospy.Service('pub_annotations_data', PubAnnotationsData, self.pub_annotations_data)
00079 
00080         self.del_anns_srv = \
00081             rospy.Service('delete_annotations',    DeleteAnnotations,   self.delete_annotations)
00082         self.save_data_srv = \
00083             rospy.Service('save_annotations_data', SaveAnnotationsData, self.save_annotations_data)
00084 
00085         self.list_worlds_srv = \
00086             rospy.Service('list_worlds',      ListWorlds,      self.list_worlds)
00087 
00088         self.set_keyword_srv = \
00089             rospy.Service('set_keyword',      SetKeyword,      self.set_keyword)
00090         self.set_related_srv = \
00091             rospy.Service('set_relationship', SetRelationship, self.set_relationship)
00092 
00093         self.reset_database_srv = \
00094             rospy.Service('reset_database', ResetDatabase, self.reset_database)
00095 
00096         # Configure services for import from/export to YAML file
00097         self.yaml_db = YAMLDatabase(self.anns_collection, self.data_collection)
00098 
00099         # Configure services for backward compatible map management
00100         if rospy.get_param('~start_map_manager', False):
00101             self.map_mng = MapManager()
00102         
00103         rospy.loginfo("Annotations server : initialized.")
00104 
00105 
00106     ##########################################################################
00107     # Services callbacks
00108     ##########################################################################
00109 
00110     def get_annotations(self, request):
00111 
00112         response = GetAnnotationsResponse()
00113         
00114         # Compose query concatenating filter criteria in an '$and' operator
00115         # Except world, all criteria are lists: operator '$in' makes a N to N matching
00116         # Empty fields are ignored
00117         query = {'$and':[]}
00118         query['$and'].append({'world': {'$in': [request.world]}})
00119         if len(request.ids) > 0:
00120             query['$and'].append({'id': {'$in': [unique_id.toHexString(id) for id in request.ids]}})
00121         if len(request.names) > 0:
00122             query['$and'].append({'name': {'$in': request.names}})
00123         if len(request.types) > 0:
00124             query['$and'].append({'type': {'$in': request.types}})
00125         if len(request.keywords) > 0:
00126             query['$and'].append({'keywords': {'$in': request.keywords}})
00127         if len(request.relationships) > 0:
00128             query['$and'].append({'relationships': {'$in': [unique_id.toHexString(r) for r in request.relationships]}})
00129 
00130         # Execute the query and retrieve results
00131         rospy.logdebug("Find annotations with query %s" % query)
00132         matching_anns = self.anns_collection.query(query)            
00133 
00134         i = 0
00135         while True:
00136             try:
00137                 response.annotations.append(matching_anns.next()[0])
00138                 i += 1
00139             except StopIteration:
00140                 if (i == 0):
00141                     rospy.loginfo("No annotations found")
00142                     return self.service_success(response)  # we don't consider this an error
00143                 break
00144     
00145     
00146 #         if (len(matching_anns) != len(matching_data)):
00147 #             # we consider this an error by now, as we assume a 1 to 1 relationship;
00148 #             # but in future implementations this will change, probably, to a N to 1 relationship
00149 #             rospy.logerr("Pulled annotations and associated data don't match (%lu != %lu)",
00150 #                      len(matching_anns), len(matching_data))
00151 #             response.message = "Pulled annotations and associated data don't match"
00152 #             response.result = False
00153 #             return response
00154 #     
00155 #         response.annotations = matching_anns
00156 #         response.data        = matching_data
00157     
00158         rospy.loginfo("%lu annotations loaded" % i)
00159         return self.service_success(response)
00160         
00161     def get_annotations_data(self, request):
00162         response = GetAnnotationsDataResponse()
00163         
00164         if len(request.annotation_ids) == 0:
00165             return self.service_error(response, "No annotation ids on request; you must be kidding!")
00166         
00167         query = {'id': {'$in': [unique_id.toHexString(id) for id in request.annotation_ids]}}                
00168         matching_data = self.data_collection.query(query)
00169         rospy.logdebug("Load annotations data with query %s" % query)
00170 
00171         i = 0
00172         while True:
00173             try:
00174                 response.data.append(matching_data.next()[0])
00175                 i += 1
00176             except StopIteration:
00177                 if (i == 0):
00178                      # we don't consider this an error
00179                     rospy.loginfo("No data found for %d requested annotations" % len(request.annotation_ids))
00180                 else:
00181                     rospy.loginfo("%d objects found for %d annotations" % (i, len(request.annotation_ids)))
00182                 break
00183 
00184         return self.service_success(response)
00185         
00186     def pub_annotations_data(self, request):
00187         response = PubAnnotationsDataResponse()
00188         
00189         if len(request.annotation_ids) == 0:
00190             return self.service_error(response, "No annotation ids on request; you must be kidding!")
00191 
00192         # Verify that all annotations on list belong to the same type (as we will publish them in
00193         # the same topic) and that at least one is really present in database
00194         query = {'data_id': {'$in': [unique_id.toHexString(id) for id in request.annotation_ids]}}
00195         matching_anns = self.anns_collection.query(query, metadata_only=True)
00196         while True:
00197             try:
00198                 # Get annotation metadata; we just need the annotation type
00199                 ann_md = matching_anns.next()
00200                 if 'topic_type' not in locals():
00201                     topic_type = ann_md['type']
00202                 elif topic_type != ann_md['type']:
00203                     return self.service_error(response, "Cannot publish annotations of different types (%s, %s)"
00204                                               % (topic_type, ann_md['type']))
00205             except StopIteration:
00206                 break
00207 
00208         if 'topic_type' not in locals():
00209             return self.service_error(response, "None of the %d requested annotations was found in database"
00210                                       % len(request.annotation_ids))
00211 
00212         # Keep the class of the messages to be published; we need it later when deserializing them
00213         msg_class = roslib.message.get_message_class(topic_type)
00214         if msg_class is None:
00215             # This happens if the topic type is wrong or not known for the server (i.e. the package describing it is
00216             # absent from ROS_PACKAGE_PATH). The second condition is a tricky one, as is a big known limitation of WCF
00217             # (https://github.com/corot/world_canvas/issues/5)
00218             return self.service_error(response, "Topic type %s definition not found" % topic_type)
00219         
00220         # Advertise a topic with message type request.topic_type if we will publish results as a list (note that we
00221         # ignore list's type) or use the retrieved annotations type otherwise (we have verified that it's unique) 
00222         if request.pub_as_list:
00223             topic_type = request.topic_type
00224             topic_class = roslib.message.get_message_class(topic_type)
00225             if topic_class is None:
00226                 # Same comment as in previous service_error call applies here
00227                 return self.service_error(response, "Topic type %s definition not found" % topic_type)
00228         else:
00229             topic_class = msg_class
00230 
00231         pub = rospy.Publisher(request.topic_name, topic_class, latch=True, queue_size=5)
00232         
00233         # Now retrieve data associated to the requested annotations; reuse query to skip toHexString calls
00234         query['id'] = query.pop('data_id')                
00235         matching_data = self.data_collection.query(query)
00236         rospy.logdebug("Publish data for annotations on query %s" % query)
00237     
00238         i = 0
00239         object_list = list()
00240         while True:
00241             try:
00242                 # Get annotation data and deserialize data field to get the original message of type request.topic_type
00243                 ann_data = matching_data.next()[0]
00244                 ann_msg = deserialize_msg(ann_data.data, msg_class)
00245                 if request.pub_as_list:
00246                     object_list.append(ann_msg)
00247                 else:
00248                     pub.publish(ann_msg)
00249                     
00250                 i += 1
00251             except SerializationError as e:
00252                 rospy.logerr("Deserialization failed: %s" % str(e))
00253                 continue
00254             except StopIteration:
00255                 if (i == 0):
00256                     # This must be an error cause we verified before that at least one annotation is present!
00257                     return self.service_error(response, "No data found for %d requested annotations"
00258                                               % len(request.annotation_ids))
00259                 if i != len(request.annotation_ids):
00260                     # Don't need to be an error, as multiple annotations can reference the same data
00261                     rospy.logwarn("Only %d objects found for %d annotations" % (i, len(request.annotation_ids)))
00262                 else:
00263                     rospy.loginfo("%d objects found for %d annotations" % (i, len(request.annotation_ids)))
00264                 if request.pub_as_list:
00265                     pub.publish(object_list)
00266                 break
00267 
00268         return self.service_success(response)
00269 
00270     def delete_annotations(self, request):
00271         '''
00272           Deletes the given annotations and its data from database.
00273 
00274           @param request: Service request.
00275         '''
00276         response = DeleteAnnotationsResponse()
00277         
00278         if len(request.annotations) == 0:
00279             return self.service_error(response, "No annotation ids on request; you must be kidding!")
00280         
00281         annot_data_ids = [a.data_id for a in request.annotations]
00282         query = {'id': {'$in': [unique_id.toHexString(id) for id in annot_data_ids]}}
00283         rospy.logdebug("Removing %d annotations data with query %s" % (len(annot_data_ids), query))
00284         data_removed = self.data_collection.remove(query)
00285         
00286         annotation_ids = [a.id for a in request.annotations]
00287         query = {'id': {'$in': [unique_id.toHexString(id) for id in annotation_ids]}}
00288         rospy.logdebug("Removing %d annotations with query %s" % (len(annotation_ids), query))
00289         removed = self.anns_collection.remove(query)
00290         rospy.loginfo("%d annotations and %d data removed from database" % (removed, data_removed))
00291         
00292         if removed != len(annotation_ids):
00293             # Not all the doomed annotations where found on database. That's not terrible; can happen
00294             # easily, for example as explained here: https://github.com/corot/world_canvas/issues/38
00295             # TODO: but we should notify the client lib somehow
00296             rospy.logwarn("Requested (%d) and deleted (%d) annotations counts doesn't match"
00297                           % (len(annotation_ids), removed))
00298         
00299         return self.service_success(response)
00300 
00301     def save_annotations_data(self, request):
00302         '''
00303           Legacy method kept for debug purposes: saves together annotations and its data
00304           assuming a 1-1 relationship.
00305 
00306           @param request: Service request.
00307         '''
00308         response = SaveAnnotationsDataResponse()
00309 
00310         print request.annotations
00311         for annotation, data in zip(request.annotations, request.data):
00312             
00313             # Compose metadata: mandatory fields
00314             metadata = { 'world'   : annotation.world,
00315                          'data_id' : unique_id.toHexString(annotation.data_id),
00316                          'id'      : unique_id.toHexString(annotation.id),
00317                          'name'    : annotation.name,
00318                          'type'    : annotation.type
00319                        }
00320 
00321             # Optional fields; note that both are stored as lists of strings
00322             if len(annotation.keywords) > 0:
00323                 metadata['keywords'] = annotation.keywords
00324             if len(annotation.relationships) > 0:
00325                 metadata['relationships'] = [unique_id.toHexString(r) for r in annotation.relationships]
00326 
00327             # Data metadata: just the object id, as all querying is done over the annotations
00328             data_metadata = { 'id' : unique_id.toHexString(annotation.data_id) }
00329 
00330             rospy.logdebug("Saving annotation %s for world %s" % (annotation.id, annotation.world))
00331 
00332             # Insert both annotation and associated data to the appropriate collection
00333             self.anns_collection.remove({'id': {'$in': [unique_id.toHexString(annotation.id)]}})
00334             self.anns_collection.insert(annotation, metadata)
00335             self.data_collection.remove({'id': {'$in': [unique_id.toHexString(annotation.data_id)]}})
00336             self.data_collection.insert(data, data_metadata)
00337 
00338         rospy.loginfo("%lu annotations saved" % len(request.annotations))
00339         return self.service_success(response)
00340 
00341     def list_worlds(self, request):
00342         response = ListWorldsResponse()
00343         
00344         # Query metadata for all annotations in database, shorted by
00345         # world so we simplify the creation of the list of worlds
00346         anns_metadata = self.anns_collection.query({}, metadata_only=True, sort_by='world')
00347         while True:
00348             try:
00349                 metadata = anns_metadata.next()
00350                 if response.names[-1] != metadata['world']:
00351                     response.names.append(metadata['world'])
00352             except IndexError:
00353                 response.names.append(metadata['world'])
00354             except StopIteration:
00355                 return response
00356 
00357     def set_keyword(self, request):
00358         response = SetKeywordResponse()
00359         annot_id, metadata = self.get_metadata(request.id)
00360         
00361         if metadata is None:
00362             response.message = "Annotation not found" 
00363             response.result = False
00364             return response
00365 
00366         if request.action == SetKeywordRequest.ADD:
00367             return self.add_element(annot_id, request.keyword, metadata, 'keywords', response)
00368         elif request.action == SetKeywordRequest.DEL:
00369             return self.del_element(annot_id, request.keyword, metadata, 'keywords', response)
00370         else:
00371             # Sanity check against crazy service clients
00372             rospy.logerr("Invalid action %d", request.action)
00373             response.message = "Invalid action: %d" % request.action
00374             response.result = False
00375             return response
00376 
00377 
00378     def set_relationship(self, request):
00379         response = SetRelationshipResponse()
00380         annot_id, metadata = self.get_metadata(request.id)
00381         relat_id = unique_id.toHexString(request.relationship)
00382         
00383         if metadata is None:
00384             response.message = 'Annotation not found' 
00385             response.result = False
00386             return response
00387 
00388         if request.action == SetRelationshipRequest.ADD:
00389             return self.add_element(annot_id, relat_id, metadata, 'relationships', response)
00390         elif request.action == SetRelationshipRequest.DEL:
00391             return self.del_element(annot_id, relat_id, metadata, 'relationships', response)
00392         else:
00393             # Sanity check against crazy service clients
00394             rospy.logerr("Invalid action: %d" % request.action)
00395             response.message = "Invalid action: %d" % request.action
00396             response.result = False
00397             return response
00398 
00399 
00400     def add_element(self, annot_id, element, metadata, md_field, response):
00401 
00402         # Look on metadata for md_field field, for the target element
00403         field = metadata.get(md_field)
00404 
00405         # Add the new element to metadata field 
00406         if field is None:
00407             field = []
00408         if element not in field:
00409             field.append(element)
00410             metadata[md_field] = field
00411             self.anns_collection.update(metadata)
00412             rospy.loginfo("%s added to %s for annotation %s" % (element, md_field, annot_id))
00413         else:
00414             # Already present; nothing to do (not an error)
00415             rospy.loginfo("%s already set on %s for annotation %s" % (element, md_field, annot_id))
00416 
00417         return self.service_success(response)
00418 
00419     def del_element(self, annot_id, element, metadata, md_field, response):
00420         # Look on metadata for md_field field, for the target element
00421         field = metadata.get(md_field)
00422 
00423         # Remove the element from metadata field, if already present 
00424         if field is None or element not in field:
00425             # Requested element not found; nothing to do, but we answer as error
00426             rospy.logerr("%s not found on %s for annotation %s" % (element, md_field, annot_id))
00427             response.message = element + ' not found'
00428             response.result = False
00429             return response
00430         else:
00431             # Found the requested element; remove from metadata
00432             rospy.loginfo("%s deleted on %s for annotation %s" % (element, md_field, annot_id))
00433             field.remove(element)
00434             if len(field) == 0:
00435                 del metadata[md_field]
00436             else:
00437                 metadata[md_field] = field
00438             self.anns_collection.update(metadata)
00439 
00440         # No error so return success
00441         return self.service_success(response)
00442 
00443     def reset_database(self, request):
00444         # Clear existing database content
00445         self.world_collection.remove({})
00446         self.anns_collection.remove({})
00447         self.data_collection.remove({})
00448 
00449         # No error so return success
00450         return self.service_success(ResetDatabaseResponse(), 'Database cleared!')
00451 
00452 
00453     ##########################################################################
00454     # Auxiliary methods
00455     ##########################################################################
00456 
00457     def get_metadata(self, uuid):
00458         # Get metadata for the given annotation id
00459         annot_id = unique_id.toHexString(uuid)
00460         matching_anns = self.anns_collection.query({'id': {'$in': [annot_id]}}, True)
00461 
00462         try:
00463             return annot_id, matching_anns.next()
00464         except StopIteration:
00465             rospy.logwarn("Annotation %s not found" % annot_id)
00466             return annot_id, None
00467 
00468     def service_success(self, response, message=None):
00469         if message is not None:
00470             rospy.loginfo(message)
00471         response.result = True
00472         return response
00473 
00474     def service_error(self, response, message):
00475         rospy.logerr(message)
00476         response.message = message
00477         response.result = False
00478         return response


world_canvas_server
Author(s): Jorge Santos Simón
autogenerated on Thu Jun 6 2019 21:25:06