00001
00002
00003 from functools import partial
00004 import math
00005 import os.path
00006 from python_qt_binding.QtCore import QPoint, QPointF, QSize, Qt
00007 from python_qt_binding.QtGui import QImage, QLabel, QLineEdit, QPainter, QPolygon, QPushButton
00008 import rospy
00009 import yaml
00010
00011 from bwi_tools import saveMapToFile
00012
00013 from .utils import clearLayoutAndFixHeight, \
00014 getDoorsMapLocationFromDataDirectory, \
00015 scalePoint, \
00016 transformPointToPixelCoordinates, \
00017 transformPointToRealWorldCoordinates
00018
00019 class Door(object):
00020
00021 def __init__(self,
00022 door_corner_pt_1,
00023 door_corner_pt_2,
00024 approach_pt_1,
00025 approach_pt_2):
00026
00027 self.door_corner_pt_1 = door_corner_pt_1
00028 self.door_corner_pt_2 = door_corner_pt_2
00029 self.approach_pt_1 = approach_pt_1
00030 self.approach_pt_2 = approach_pt_2
00031
00032 def clone(self):
00033 return Door(QPoint(self.door_corner_pt_1),
00034 QPoint(self.door_corner_pt_2),
00035 QPoint(self.approach_pt_1),
00036 QPoint(self.approach_pt_2))
00037
00038 class DoorFunction(object):
00039
00040 EDIT_DOOR_PROPERITIES = 'Edit Door Properties'
00041 ADD_NEW_DOOR = 'Add Door'
00042 EDIT_EXISTING_DOOR = 'Edit Door'
00043
00044 def __init__(self,
00045 door_file,
00046 map,
00047 location_function,
00048 widget,
00049 subfunction_layout,
00050 configuration_layout,
00051 image):
00052
00053 self.edit_door_location_button = None
00054 self.selected_door_color = Qt.blue
00055 self.unselected_door_color = Qt.darkGreen
00056
00057
00058 self.doors = {}
00059 self.draw_door = {}
00060 self.unique_door_counter = 1
00061
00062 self.editing_door_location = False
00063 self.edit_existing_door = None
00064
00065 self.editing_properties = False
00066 self.edit_properties_door = None
00067
00068
00069 self.clearCurrentSelection()
00070
00071 self.is_modified = False
00072
00073 self.widget = widget
00074 self.subfunction_layout = subfunction_layout
00075 self.image = image
00076 self.image_size = image.overlay_image.size()
00077 self.configuration_layout = configuration_layout
00078
00079 self.door_file = door_file
00080 self.map = map
00081 self.map_size = QSize(map.map.info.width, map.map.info.height)
00082 self.location_function = location_function
00083 self.readDoorsFromFile()
00084
00085 self.edit_door_location_button = {}
00086
00087 def readDoorsFromFile(self):
00088
00089 if os.path.isfile(self.door_file):
00090 stream = open(self.door_file, 'r')
00091 try:
00092 contents = yaml.load(stream)
00093 for door in contents:
00094 door_key = door["name"]
00095 approach_pts = door["approach"]
00096 if len(approach_pts) != 2:
00097 rospy.logerr("Door " + door_key + " read from file " + self.door_file + " has " + str(len(approach_pts)) + " approach points instead of 2. Ignoring this door.")
00098 continue
00099 approach_pt_1 = QPointF(approach_pts[0]["point"][0], approach_pts[0]["point"][1])
00100 approach_pt_1 = transformPointToPixelCoordinates(approach_pt_1, self.map, self.image_size)
00101 approach_pt_2 = QPointF(approach_pts[1]["point"][0], approach_pts[1]["point"][1])
00102 approach_pt_2 = transformPointToPixelCoordinates(approach_pt_2, self.map, self.image_size)
00103
00104
00105 if "door_corner_pt_1" in door:
00106 door_corner_pt_1 = transformPointToPixelCoordinates(QPointF(*door["door_corner_pt_1"]),
00107 self.map,
00108 self.image_size)
00109 door_corner_pt_2 = transformPointToPixelCoordinates(QPointF(*door["door_corner_pt_2"]),
00110 self.map,
00111 self.image_size)
00112 else:
00113 width = 1.0
00114 if "width" in door:
00115 width = door["width"]
00116 scaled_origin = transformPointToPixelCoordinates(QPointF(0, 0), self.map, self.image_size)
00117 scaled_width_pt = transformPointToPixelCoordinates(QPointF(0, width), self.map, self.image_size)
00118 scaled_width_diff = scaled_width_pt - scaled_origin
00119 scaled_width = math.sqrt(scaled_width_diff.x() * scaled_width_diff.x() + scaled_width_diff.y() * scaled_width_diff.y())
00120 midpoint = (approach_pt_1 + approach_pt_2) / 2
00121 diff = approach_pt_1 - approach_pt_2
00122 segment_angle = math.atan2(diff.y(), diff.x())
00123 perpendicular_angle = segment_angle + math.pi / 2.0
00124 perpendicular_diff_pt = QPoint(int((scaled_width / 2) * math.cos(perpendicular_angle)),
00125 int((scaled_width / 2) * math.sin(perpendicular_angle)))
00126
00127 door_corner_pt_1 = midpoint + perpendicular_diff_pt
00128 door_corner_pt_2 = midpoint - perpendicular_diff_pt
00129
00130
00131 self.doors[door_key] = Door(door_corner_pt_1,
00132 door_corner_pt_2,
00133 approach_pt_1,
00134 approach_pt_2)
00135 self.draw_door[door_key] = True
00136 except yaml.YAMLError, KeyError:
00137 rospy.logerr("File found at " + self.door_file + ", but cannot be parsed by YAML parser. I'm starting doors from scratch.")
00138
00139 stream.close()
00140 else:
00141 rospy.logwarn("Door file not found at " + self.door_file + ". I'm starting doors from scratch and will attempt to write to this door before exiting.")
00142
00143 def saveConfiguration(self):
00144 self.writeDoorsToFile()
00145
00146 def writeDoorsToFile(self):
00147
00148 out_list = []
00149 for door_name in self.doors:
00150 door = self.doors[door_name]
00151 door_corner_pt_1 = transformPointToRealWorldCoordinates(door.door_corner_pt_1, self.map, self.image_size)
00152 door_corner_pt_2 = transformPointToRealWorldCoordinates(door.door_corner_pt_2, self.map, self.image_size)
00153 door_dict = {}
00154 door_dict["name"] = door_name
00155 door_dict["door_corner_pt_1"] = [door_corner_pt_1.x(), door_corner_pt_1.y()]
00156 door_dict["door_corner_pt_2"] = [door_corner_pt_2.x(), door_corner_pt_2.y()]
00157 door_dict["approach"] = []
00158 mid_point = (door_corner_pt_1 + door_corner_pt_2) / 2
00159 for point in [door.approach_pt_1, door.approach_pt_2]:
00160 approach_pt_dict = {}
00161 approach_pt_dict["from"] = self.location_function.getLocationNameFromPoint(point)
00162 transformed_point = transformPointToRealWorldCoordinates(point, self.map, self.image_size)
00163 diff_pt = transformed_point - mid_point
00164 approach_angle = math.atan2(diff_pt.y(), diff_pt.x())
00165 approach_pt_dict["point"] = [transformed_point.x(), transformed_point.y(), approach_angle]
00166 door_dict["approach"].append(approach_pt_dict)
00167 out_list.append(door_dict)
00168
00169 stream = open(self.door_file, 'w')
00170 yaml.dump(out_list, stream)
00171 stream.close()
00172
00173
00174 data_directory = os.path.dirname(os.path.realpath(self.door_file))
00175 map_file = getDoorsMapLocationFromDataDirectory(data_directory)
00176 map_image_file = "doors_map.pgm"
00177
00178 scaled_map_image = self.image.map_image.scaled(self.map_size)
00179 map_with_doors_image = QImage(self.map_size, QImage.Format_RGB32)
00180 painter = QPainter(map_with_doors_image)
00181 painter.drawImage(0,0,scaled_map_image)
00182 painter.setPen(Qt.black)
00183 for door_name in self.doors:
00184 door = self.doors[door_name]
00185
00186 door_corner_pt_1 = scalePoint(door.door_corner_pt_1, self.image_size, self.map_size)
00187 door_corner_pt_2 = scalePoint(door.door_corner_pt_2, self.image_size, self.map_size)
00188 painter.drawLine(door_corner_pt_1, door_corner_pt_2)
00189 painter.end()
00190
00191 saveMapToFile(self.map.map, map_file, map_image_file, False, 0.2, 0.8, map_with_doors_image)
00192
00193 self.is_modified = False
00194
00195 def deactivateFunction(self):
00196
00197 if self.editing_door_location:
00198 self.endDoorLocationEdit("Cancel")
00199 elif self.editing_properties:
00200 self.endPropertyEdit()
00201
00202 clearLayoutAndFixHeight(self.subfunction_layout)
00203 self.edit_door_location_button.clear()
00204 self.image.enableDefaultMouseHooks()
00205
00206
00207 for door in self.draw_door:
00208 self.draw_door[door] = True
00209
00210 def activateFunction(self):
00211
00212
00213 clearLayoutAndFixHeight(self.subfunction_layout)
00214 for button_text in [DoorFunction.ADD_NEW_DOOR,
00215 DoorFunction.EDIT_EXISTING_DOOR]:
00216 button = QPushButton(button_text, self.widget)
00217 button.clicked[bool].connect(partial(self.startDoorLocationEdit, button_text))
00218 button.setCheckable(True)
00219 self.subfunction_layout.addWidget(button)
00220 self.edit_door_location_button[button_text] = button
00221 self.edit_door_location_button[DoorFunction.EDIT_EXISTING_DOOR].setEnabled(False)
00222 self.subfunction_layout.addStretch(1)
00223
00224
00225 self.image.mousePressEvent = self.mousePressEvent
00226 self.image.mouseMoveEvent = self.mouseMoveEvent
00227 self.image.mouseReleaseEvent = self.mouseReleaseEvent
00228
00229 self.updateOverlay()
00230
00231 def getDoorNameFromPoint(self, point):
00232 for door in self.doors:
00233
00234 if self.getPointDistanceToAnotherPoint(point, self.doors[door].approach_pt_1) <= 3:
00235 return door
00236 if self.getPointDistanceToAnotherPoint(point, self.doors[door].approach_pt_2) <= 3:
00237 return door
00238 if self.getPointDistanceToLineSegment(point,
00239 self.doors[door].door_corner_pt_1,
00240 self.doors[door].door_corner_pt_2) <= 3:
00241 return door
00242 return None
00243
00244 def startDoorLocationEdit(self, edit_type):
00245
00246 if self.editing_properties:
00247 self.endPropertyEdit()
00248
00249 self.editing_door_location = True
00250
00251 if edit_type == DoorFunction.ADD_NEW_DOOR:
00252 self.edit_existing_door = None
00253
00254
00255
00256 self.clearCurrentSelection()
00257
00258
00259 if self.edit_existing_door is not None:
00260 self.draw_door[self.edit_existing_door] = False
00261 self.current_selection = self.doors[self.edit_existing_door].clone()
00262 self.edit_existing_door = self.edit_existing_door
00263
00264
00265 clearLayoutAndFixHeight(self.configuration_layout)
00266 for button_text in ["Done", "Cancel"]:
00267 button = QPushButton(button_text, self.widget)
00268 button.clicked[bool].connect(partial(self.endDoorLocationEdit, button_text))
00269 self.configuration_layout.addWidget(button)
00270 self.current_selection_label = QLabel(self.widget)
00271 self.configuration_layout.addWidget(self.current_selection_label)
00272 self.configuration_layout.addStretch(1)
00273
00274 self.edit_door_location_button[DoorFunction.ADD_NEW_DOOR].setEnabled(False)
00275 self.edit_door_location_button[DoorFunction.EDIT_EXISTING_DOOR].setEnabled(False)
00276
00277 self.updateOverlay()
00278
00279 def clearCurrentSelection(self):
00280
00281
00282 self.new_selection_start_point = None
00283 self.new_selection_end_point = None
00284
00285
00286 self.current_selection = None
00287 self.current_selection_label = None
00288 self.move_selection = None
00289
00290 def endDoorLocationEdit(self, button_text):
00291
00292 edit_properties_door = None
00293
00294 if (button_text == "Done") and (self.current_selection is not None):
00295
00296 if self.edit_existing_door == None:
00297
00298 self.edit_existing_door = self.getUniqueName()
00299 self.doors[self.edit_existing_door] = self.current_selection
00300 self.draw_door[self.edit_existing_door] = True
00301 edit_properties_door = self.edit_existing_door
00302
00303
00304 self.is_modified = True
00305 else:
00306
00307 if self.edit_existing_door is not None:
00308 self.draw_door[self.edit_existing_door] = True
00309
00310 self.editing_door_location = False
00311 self.edit_existing_door = None
00312 self.clearCurrentSelection()
00313
00314
00315 self.updateOverlay()
00316
00317 self.edit_door_location_button[DoorFunction.ADD_NEW_DOOR].setEnabled(True)
00318 self.edit_door_location_button[DoorFunction.ADD_NEW_DOOR].setChecked(False)
00319 self.edit_door_location_button[DoorFunction.EDIT_EXISTING_DOOR].setChecked(False)
00320 clearLayoutAndFixHeight(self.configuration_layout)
00321
00322 if edit_properties_door is not None:
00323 self.edit_properties_door = edit_properties_door
00324 self.startPropertyEdit()
00325
00326 def startPropertyEdit(self):
00327
00328 self.editing_properties = True
00329 self.edit_existing_door = self.edit_properties_door
00330
00331 self.edit_door_location_button[DoorFunction.ADD_NEW_DOOR].setEnabled(True)
00332 self.edit_door_location_button[DoorFunction.EDIT_EXISTING_DOOR].setEnabled(True)
00333
00334
00335 clearLayoutAndFixHeight(self.configuration_layout)
00336
00337 connects_text = self.getConnectingText(self.doors[self.edit_properties_door])
00338 self.update_name_label = QLabel("Door (" + self.edit_properties_door + " - " + connects_text + ") New Name: ", self.widget)
00339 self.configuration_layout.addWidget(self.update_name_label)
00340
00341 self.update_name_textedit = QLineEdit(self.widget)
00342 self.update_name_textedit.setText(self.edit_properties_door)
00343 self.update_name_textedit.textEdited.connect(self.doorNameTextEdited)
00344 self.configuration_layout.addWidget(self.update_name_textedit)
00345
00346 self.update_name_button = QPushButton("Update door Name", self.widget)
00347 self.update_name_button.clicked[bool].connect(self.updateDoorName)
00348 self.update_name_button.setEnabled(False)
00349 self.configuration_layout.addWidget(self.update_name_button)
00350
00351 self.remove_door_button = QPushButton("Remove Door", self.widget)
00352 self.remove_door_button.clicked[bool].connect(self.removeCurrentDoor)
00353 self.configuration_layout.addWidget(self.remove_door_button)
00354
00355 self.configuration_layout.addStretch(1)
00356
00357 self.updateOverlay()
00358
00359 def endPropertyEdit(self):
00360
00361 self.edit_door_location_button[DoorFunction.ADD_NEW_DOOR].setEnabled(True)
00362 self.edit_door_location_button[DoorFunction.EDIT_EXISTING_DOOR].setEnabled(False)
00363
00364 clearLayoutAndFixHeight(self.configuration_layout)
00365
00366 self.update_name_label = None
00367 self.update_name_textedit = None
00368 self.update_name_button = None
00369
00370 self.editing_properties = False
00371
00372 self.edit_properties_door = None
00373
00374 self.updateOverlay()
00375
00376 def doorNameTextEdited(self, text):
00377 if str(text) != self.edit_properties_door and str(text) not in self.doors:
00378 self.update_name_button.setEnabled(True)
00379 else:
00380 self.update_name_button.setEnabled(False)
00381
00382 def updateDoorName(self):
00383 old_door_name = self.edit_properties_door
00384 new_door_name = str(self.update_name_textedit.text())
00385
00386
00387 self.doors[new_door_name] = self.doors.pop(old_door_name)
00388 self.draw_door[new_door_name] = self.draw_door.pop(old_door_name)
00389
00390
00391 self.is_modified = True
00392
00393
00394 self.endPropertyEdit()
00395 self.edit_properties_door = new_door_name
00396 self.startPropertyEdit()
00397
00398 def removeCurrentDoor(self):
00399 old_door_name = self.edit_properties_door
00400 self.removeDoor(old_door_name)
00401 self.endPropertyEdit()
00402 self.updateOverlay()
00403
00404
00405 self.is_modified = True
00406
00407 def removeDoor(self, door_name):
00408 if door_name in self.doors:
00409 self.doors.pop(door_name)
00410 if door_name in self.draw_door:
00411 self.draw_door.pop(door_name)
00412
00413 def isModified(self):
00414 return self.is_modified
00415
00416 def mousePressEvent(self, event):
00417 if self.editing_door_location:
00418 if self.current_selection == None:
00419
00420 self.new_selection_start_point = event.pos()
00421 self.new_selection_end_point = event.pos()
00422 self.move_selection = None
00423 else:
00424
00425 self.new_selection_start_point = None
00426 self.new_selection_end_point = None
00427
00428
00429 self.move_selection = None
00430 for pt in [self.current_selection.door_corner_pt_1,
00431 self.current_selection.door_corner_pt_2,
00432 self.current_selection.approach_pt_1,
00433 self.current_selection.approach_pt_2]:
00434 if self.getPointDistanceToAnotherPoint(event.pos(), pt) <= 3:
00435 self.move_selection = pt
00436 break
00437 else:
00438 door = self.getDoorNameFromPoint(event.pos())
00439 if door is not None:
00440 self.edit_properties_door = door
00441 self.startPropertyEdit()
00442 else:
00443 self.endPropertyEdit()
00444
00445 def mouseReleaseEvent(self, event):
00446 if self.editing_door_location:
00447 self.mouseMoveEvent(event)
00448 if self.current_selection is None:
00449 midpoint = (self.new_selection_start_point + self.new_selection_end_point) / 2
00450 diff = self.new_selection_end_point - self.new_selection_start_point
00451 segment_angle = math.atan2(diff.y(), diff.x())
00452 perpendicular_angle = segment_angle + math.pi / 2.0
00453 perpendicular_diff_pt = QPoint(int(10.0 * math.cos(perpendicular_angle)),
00454 int(10.0 * math.sin(perpendicular_angle)))
00455
00456 approach_pt_1 = midpoint + perpendicular_diff_pt
00457 approach_pt_2 = midpoint - perpendicular_diff_pt
00458 self.current_selection = Door(self.new_selection_start_point,
00459 self.new_selection_end_point,
00460 approach_pt_1,
00461 approach_pt_2)
00462 self.current_selection_label.setText(self.getConnectingText(self.current_selection))
00463 self.new_selection_start_pt = None
00464 self.new_selection_end_point = None
00465 self.updateOverlay()
00466
00467 def mouseMoveEvent(self, event):
00468
00469 if self.editing_door_location:
00470 overlay_update_region = None
00471 if self.current_selection is None:
00472
00473 old_overlay_update_rect = self.getRectangularPolygon(self.new_selection_start_point, self.new_selection_end_point)
00474 self.new_selection_end_point = event.pos()
00475
00476 new_overlay_update_rect = self.getRectangularPolygon(self.new_selection_start_point, self.new_selection_end_point)
00477 overlay_update_region = (old_overlay_update_rect + new_overlay_update_rect).boundingRect()
00478 overlay_update_region.setTopLeft(QPoint(overlay_update_region.topLeft().x() - 4,
00479 overlay_update_region.topLeft().y() - 4))
00480 overlay_update_region.setBottomRight(QPoint(overlay_update_region.bottomRight().x() + 4,
00481 overlay_update_region.bottomRight().y() + 4))
00482
00483 self.updateOverlay(overlay_update_region)
00484 elif self.move_selection is not None:
00485
00486 self.move_selection.setX(event.pos().x())
00487 self.move_selection.setY(event.pos().y())
00488
00489
00490
00491 self.current_selection_label.setText(self.getConnectingText(self.current_selection))
00492 self.updateOverlay()
00493
00494 def updateOverlay(self, rect = None):
00495
00496
00497
00498 self.image.overlay_image.fill(Qt.transparent)
00499 painter = QPainter(self.image.overlay_image)
00500 painter.setBackgroundMode(Qt.TransparentMode)
00501 painter.setCompositionMode(QPainter.CompositionMode_Source)
00502
00503 for door in self.doors:
00504 if self.draw_door[door]:
00505 color = self.unselected_door_color
00506 if self.edit_properties_door == door and self.editing_properties:
00507 color = self.selected_door_color
00508 self.drawDoor(self.doors[door], painter, color)
00509
00510 if self.current_selection is not None:
00511 color = self.selected_door_color
00512 self.drawDoor(self.current_selection, painter, color)
00513 elif self.new_selection_start_point is not None:
00514 color = self.selected_door_color
00515 self.drawLine(self.new_selection_start_point, self.new_selection_end_point, painter, color)
00516 painter.end()
00517
00518 if rect is None:
00519 self.image.update()
00520 else:
00521 self.image.update(rect)
00522
00523 def getConnectingText(self, door):
00524
00525
00526 approach_location_1 = self.location_function.getLocationNameFromPoint(door.approach_pt_1)
00527 if approach_location_1 is None:
00528 approach_location_1 = "<None>"
00529 approach_location_2 = self.location_function.getLocationNameFromPoint(door.approach_pt_2)
00530 if approach_location_2 is None:
00531 approach_location_2 = "<None>"
00532 return "Connects: " + approach_location_1 + " <-> " + approach_location_2
00533
00534 def getUniqueName(self):
00535 name = "new_door" + str(self.unique_door_counter)
00536 self.unique_door_counter += 1
00537 return name
00538
00539 def getPointDistanceToAnotherPoint(self, pt1, pt2):
00540 diff = pt1 - pt2
00541 return math.sqrt(diff.x() * diff.x() + diff.y() * diff.y())
00542
00543 def getPointDistanceToLineSegment(self, pt, segment_pt_1, segment_pt_2):
00544 """
00545 http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment
00546 """
00547 px = segment_pt_2.x()-segment_pt_1.x()
00548 py = segment_pt_2.y()-segment_pt_1.y()
00549
00550 something = px*px + py*py
00551
00552 u = ((pt.x() - segment_pt_1.x()) * px + (pt.y() - segment_pt_1.y()) * py) / float(something)
00553
00554 if u > 1:
00555 u = 1
00556 elif u < 0:
00557 u = 0
00558
00559 x = segment_pt_1.x() + u * px
00560 y = segment_pt_1.y() + u * py
00561
00562 dx = x - pt.x()
00563 dy = y - pt.y()
00564
00565 return math.sqrt(dx*dx + dy*dy)
00566
00567 def getRectangularPolygon(self, pt1, pt2):
00568 return QPolygon([pt1, QPoint(pt1.x(), pt2.y()), pt2, QPoint(pt2.x(), pt1.y())])
00569
00570 def drawDoor(self, door, painter, color):
00571 self.drawLine(door.door_corner_pt_1, door.door_corner_pt_2, painter, color)
00572 self.drawPoint(door.approach_pt_1, painter, color)
00573 self.drawPoint(door.approach_pt_2, painter, color)
00574
00575 def drawLine(self, pt1, pt2, painter, color):
00576 painter.setPen(color)
00577 painter.drawLine(pt1, pt2)
00578 self.drawPoint(pt1, painter, color)
00579 self.drawPoint(pt2, painter, color)
00580
00581 def drawPoint(self, pt, painter, color):
00582 painter.setPen(color)
00583 painter.drawPoint(pt)
00584 painter.drawEllipse(pt, 3, 3)