yaml_database.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 genpy
00039 import os
00040 import re
00041 import yaml
00042 import uuid
00043 import unique_id
00044 import warehouse_ros as wr
00045 
00046 from world_canvas_msgs.msg import *
00047 from world_canvas_msgs.srv import *
00048 from world_canvas_utils.serialization import *
00049 
00050 
00051 class YAMLDatabase:
00052     
00053     ##########################################################################
00054     # Initialization
00055     ##########################################################################
00056 
00057     def __init__(self, anns_collection, data_collection):
00058         # Set up collections
00059         self.anns_collection = anns_collection
00060         self.data_collection = data_collection
00061 
00062         # Set up services
00063         self.import_srv = rospy.Service('yaml_import', YAMLImport, self.import_from_yaml)
00064         self.export_srv = rospy.Service('yaml_export', YAMLExport, self.export_to_yaml)
00065 
00066     
00067     ##########################################################################
00068     # Services callbacks
00069     ##########################################################################
00070 
00071     def import_from_yaml(self, request):
00072 
00073         response = YAMLImportResponse()
00074         
00075         if not os.path.isfile(request.filename):
00076             return self.service_error(response, "File does not exist: %s" % (request.filename))
00077 
00078         try:
00079             with open(request.filename, 'r') as f:
00080                 # load all documents
00081                 yaml_data = yaml.load(f)
00082                 if yaml_data is None:
00083                     raise yaml.YAMLError("Empty files not allowed")
00084         except yaml.YAMLError as e:
00085             return self.service_error(response, "Invalid YAML file: %s" % (str(e)))
00086     
00087         # Clear existing database content  TODO: flag to choose whether keep content
00088         self.anns_collection.remove({})
00089         self.data_collection.remove({})
00090 
00091         # Parse file: a database is composed of a list of elements, each one containing a list of annotations plus
00092         # their referenced data. TODO: Can change according to issue https://github.com/corot/world_canvas/issues/9
00093         for t in yaml_data:
00094             # Annotations: a list of one or more annotations
00095             try:
00096                 anns_list = t['annotations']
00097             except KeyError as e:
00098                 return self.service_error(response, "Invalid database file format: %s field not found" % str(e))
00099 
00100             if len(anns_list) == 0:
00101                 # Coherence check: there must be at least one annotation for database entry
00102                 return self.service_error(response, "Invalid database file format: " \
00103                                           "there must be at least one annotation for database entry")
00104 
00105             for a in anns_list:
00106                 annotation = Annotation()
00107                 try:
00108                     genpy.message.fill_message_args(annotation, a)
00109 
00110                     if 'prev_data_id' in locals() and prev_data_id != annotation.data_id.uuid:
00111                         # Coherence check: all data_id fields must be equal between them and to data id
00112                         return self.service_error(response, "Invalid database file format: " \
00113                                                   "data ids must be equal for annotations referencing the same data")
00114 
00115                     prev_data_id = annotation.data_id.uuid
00116                     
00117                     # Forced conversion because UUID expects a string of 16 bytes, not a list
00118                     annotation.data_id.uuid = ''.join(chr(x) for x in annotation.data_id.uuid)
00119                     annotation.id.uuid = ''.join(chr(x) for x in annotation.id.uuid)
00120                     for r in annotation.relationships:
00121                         r.uuid = ''.join(chr(x) for x in r.uuid)
00122         
00123                     # Compose metadata: mandatory fields
00124                     metadata = { 'data_id' : unique_id.toHexString(annotation.data_id),
00125                                  'id'      : unique_id.toHexString(annotation.id),
00126                                  'world'   : annotation.world,
00127                                  'name'    : annotation.name,
00128                                  'type'    : annotation.type
00129                                }
00130         
00131                     # Optional fields; note that both are stored as lists of strings
00132                     if len(annotation.keywords) > 0:
00133                         metadata['keywords'] = annotation.keywords
00134                     if len(annotation.relationships) > 0:
00135                         metadata['relationships'] = [unique_id.toHexString(r) for r in annotation.relationships]
00136                         
00137                     rospy.logdebug("Saving annotation %s for world %s" % (annotation.id, annotation.world))
00138             
00139                     # TODO  recuperate if implement TODO on line 83                   self.anns_collection.remove({'id': {'$in': [unique_id.toHexString(annotation.id)]}})
00140                     
00141                     self.anns_collection.insert(annotation, metadata, safe=True)
00142                     
00143                 except (genpy.MessageException, genpy.message.SerializationError) as e:
00144                     return self.service_error(response, "Invalid annotation msg format: %s" % str(e))
00145                 except Exception as e:
00146                     # Assume collection.insert raised this, as we set safe=True (typically a DuplicateKeyError)
00147                     return self.service_error(response, "Insert annotation failed: %s" % str(e))
00148 
00149             # Annotation data, of message type annotation.type
00150             msg_class = roslib.message.get_message_class(annotation.type)
00151             if msg_class is None:
00152                 # annotation.type doesn't contain a known message type; we cannot insert on database
00153                 return self.service_error(response, "Unknown message type: %s" % annotation.type)
00154             
00155             data = msg_class()
00156 
00157             # Data metadata: just the object id, as all querying is done over the annotations
00158             data_metadata = { 'id' : unique_id.toHexString(annotation.data_id) }
00159 
00160             try:
00161                 genpy.message.fill_message_args(data, t['data'])
00162                 data_msg = AnnotationData()
00163                 data_msg.id = annotation.data_id
00164                 data_msg.type = annotation.type
00165                 data_msg.data = serialize_msg(data)
00166                 self.data_collection.insert(data_msg, data_metadata, safe=True)
00167             except (genpy.MessageException, genpy.message.SerializationError) as e:
00168                 # TODO: here I would have an incoherence in db: annotations without data;
00169                 # do mongo has rollback? do it manually? or just clear database content?
00170                 return self.service_error(response, "Invalid %s msg format: %s" % (annotation.type, str(e)))
00171             except KeyError as e:
00172                 return self.service_error(response, "Invalid database file format: %s field not found" % str(e))
00173             except Exception as e:
00174                 # Assume collection.insert raised this, as we set safe=True (typically a DuplicateKeyError)
00175                 return self.service_error(response, "Insert annotation data failed: %s" % str(e))
00176 
00177 #               self.data_collection.remove({'id': {'$in': [unique_id.toHexString(annotation.id)]}})
00178 
00179             del prev_data_id # clear this so it doen't interfere with next element validation
00180 
00181         return self.service_success(response, "%lu annotations imported on database" % len(yaml_data))
00182 
00183 
00184     def export_to_yaml(self, request):
00185         response = YAMLExportResponse()
00186 
00187         # Query for all annotations in database, shorted by data id, so we can export packed with its referenced data
00188         matching_anns = self.anns_collection.query({}, sort_by='data_id')
00189 
00190         try:
00191             with open(request.filename, 'w') as f:
00192                 entries = []
00193                 annotations = []
00194                 last = False
00195 
00196                 # Loop over annotations retrieved from database
00197                 while True:
00198                     try:
00199                         annotation, metadata = matching_anns.next()
00200                     except StopIteration:
00201                         last = True
00202 
00203                     if not last and (len(annotations) == 0 or annotation.data_id == annotations[-1].data_id):
00204                         # We pack annotations with the same data_id
00205                         annotations.append(annotation)
00206                     elif len(annotations) > 0:
00207                         # When the next annotation have a different data_id, or is the
00208                         # last one, it's time to append a new entry to the output file:
00209                         #  a) retrieve data for current data_id
00210                         data_id_str = unique_id.toHexString(annotations[-1].data_id)
00211                         matching_data = self.data_collection.query({'id': {'$in': [data_id_str]}})
00212                         try:
00213                             d = matching_data.next()[0]
00214                         except StopIteration:
00215                             return self.service_error(response, "Export to file failed: " \
00216                                                       "No data found with id %s" % data_id_str)
00217 
00218                         #  b) pack together with their referencing annotations in a dictionary
00219 
00220                         # We need the annotation data class to deserialize the bytes array stored in database
00221                         data_class = roslib.message.get_message_class(annotations[0].type)
00222                         if data_class is None:
00223                             rospy.logerr("Annotation type %s definition not found" % annotation.type)
00224                             return False
00225 
00226                         data = deserialize_msg(d.data, data_class)
00227                         entry = dict(
00228                             annotations = [yaml.load(genpy.message.strify_message(a)) for a in annotations],
00229                             data = yaml.load(genpy.message.strify_message(data))
00230                         )
00231                         entries.append(entry)
00232 
00233                         if last:
00234                             break;
00235                         else:
00236                             # No the last annotation; note that it's still not stored,
00237                             # in the entries list so add to list for the next iteration
00238                             annotations = [annotation]
00239                     else:
00240                          # just to verify the not-very-clear logic of the loop...
00241                         rospy.logdebug("Final else: %d %d" % (last, len(entries)))
00242                         break;
00243 
00244 
00245                 if len(entries) == 0:
00246                     # we don't consider this an error
00247                     return self.service_success(response, "Database is empty!; nothing to export")
00248                 else:
00249                     # I must use default_flow_style = False so the resulting YAML looks nice, but then
00250                     # the bloody dumper writes lists with an element per-line, (with -), what makes the
00251                     # output very long and not very readable. So method flow_style_lists makes uuids and
00252                     # covariances list flow-styled, i.e. [x, y, z, ...], while flow_style_occ_grid to the
00253                     # same for occupancy grids (maps), but can be easily modified for other big messages
00254                     dump = yaml.dump(entries, default_flow_style=False)
00255                     dump = self.flow_style_lists(dump)
00256                     dump = self.flow_style_occ_grid(dump)
00257                     
00258                     # Add a decimal point to exponential formated floats; if not, get loaded as strings,
00259                     # due to a bug in pyyaml. See this ticket for details: http://pyyaml.org/ticket/359
00260                     dump = self.fix_exp_numbers(dump)
00261                     f.write(dump)
00262                     return self.service_success(response, "%lu annotations exported from database" % len(entries))
00263         except Exception as e:
00264             return self.service_error(response, "Export to file failed: %s" % (str(e)))
00265 
00266     
00267     ##########################################################################
00268     # Auxiliary methods
00269     ##########################################################################
00270 
00271     def service_success(self, response, message = None):
00272         if message is not None:
00273             rospy.loginfo(message)
00274         response.result = True
00275         return response
00276 
00277     def service_error(self, response, message):
00278         rospy.logerr(message)
00279         response.message = message
00280         response.result = False
00281         return response
00282 
00283     def fix_exp_numbers(self, target):
00284         # Compose and compile regex to match exponential notation floats without floating point
00285         regex = '( [-+]?\d+[eE][-+]?\d+)'
00286         offset = 0
00287         comp_re = re.compile(regex)
00288         for match in comp_re.finditer(target):
00289             # Replace matches with a the same float plus a .0 to make pyyaml parser happy
00290             fp_nb = match.group(1)
00291             fp_nb = re.sub('[eE]', '.0e', fp_nb)
00292             target = target[:match.start(1) + offset] + fp_nb + target[match.end(1) + offset:]
00293             offset += 2
00294 
00295         return target
00296     
00297     def flow_style_lists(self, target):
00298         # Compose and compile regex to match uuids: lists of 16 integers
00299         regex = 'uuid: *\n'
00300         for i in range(0, 16):
00301             regex += ' *- +(\d+)\n'
00302         comp_re = re.compile(regex)
00303         while True:
00304             match = comp_re.search(target)
00305             if match is None:
00306                 break;
00307             
00308             # Replace matches with a flow-stile version; group(n) returns the nth integer
00309             flow_list = 'uuid: ['
00310             for i in range(1, 16):
00311                 flow_list += match.group(i) + ', '
00312             flow_list += match.group(16) + ']\n'
00313         
00314             target = comp_re.sub(flow_list, target, 1)
00315 
00316         # Compose and compile regex to match covariances: lists of 36 floats, possibly on exponential notation
00317         regex = 'covariance: *\n'
00318         for i in range(0, 36):
00319             regex += ' *- +([-+]?\d*\.?\d+|\d+[eE][-+]?\d+)\n'
00320         comp_re = re.compile(regex)
00321         while True:
00322             match = comp_re.search(target)
00323             if match is None:
00324                 break;
00325             
00326             # Replace matches with a flow-stile version; group(n) returns the nth float
00327             flow_list = 'covariance: ['
00328             for i in range(1, 36):
00329                 flow_list += match.group(i) + ', '
00330             flow_list += match.group(36) + ']\n'
00331         
00332             target = comp_re.sub(flow_list, target, 1)
00333 
00334         return target
00335     
00336     def flow_style_occ_grid(self, target):
00337         # Compose and compile regex to match 'data:' + a lists of 10 or more integers
00338         # This should apply to other data chunks other than maps; can also replace the
00339         # uuid matcher of method flow_style_lists, but I let it TODO
00340         regex = 'data: *\n( *- +(\d+)\n){10,}'
00341         comp_re = re.compile(regex)
00342         while True:
00343             data_match = comp_re.search(target)
00344             if data_match is None:
00345                 break;
00346             
00347             data = target[data_match.start():data_match.end()]
00348             flow_list = 'data: ['
00349             for byte_match in re.finditer(' *- +(\d+)\n', data):
00350                 flow_list += byte_match.group(1) + ','
00351             flow_list = flow_list[:-1] + ']\n'
00352 
00353             target = comp_re.sub(flow_list, target, 1)
00354 
00355         return target


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