dtmovenodebyarea.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. # -*- coding: utf-8 -*-
  2. """
  3. dtmovenodebyarea
  4. ````````````````
  5. """
  6. """
  7. Part of DigitizingTools, a QGIS plugin that
  8. subsumes different tools neded during digitizing sessions
  9. * begin : 2013-08-14
  10. * copyright : (C) 2013 by Angelos Tzotsos
  11. * email : tzotsos@gmail.com
  12. This program is free software; you can redistribute it and/or modify
  13. it under the terms of the GNU General Public License as published by
  14. the Free Software Foundation; either version 2 of the License, or
  15. (at your option) any later version.
  16. """
  17. from builtins import object
  18. from qgis.PyQt import QtCore, QtGui, QtWidgets
  19. from qgis.core import *
  20. import dt_icons_rc
  21. from dttools import DtSelectVertexTool
  22. from dtmovenodebyarea_dialog import DtMoveNodeByArea_Dialog
  23. class DtMoveNodeByArea(object):
  24. '''Automatically move polygon node (along a given side of polygon) in order to achieve a desired polygon area'''
  25. def __init__(self, iface, toolBar):
  26. # Save reference to the QGIS interface
  27. self.iface = iface
  28. self.canvas = self.iface.mapCanvas()
  29. self.gui = None
  30. self.multipolygon_detected = False
  31. # Points and Markers
  32. self.p1 = None
  33. self.p2 = None
  34. self.m1 = None
  35. self.m2 = None
  36. self.selected_feature = None
  37. #create action
  38. self.node_mover = QtWidgets.QAction(QtGui.QIcon(":/MovePolygonNodeByArea.png"),
  39. QtWidgets.QApplication.translate("digitizingtools", "Move polygon node (along a side) to achieve target area"), self.iface.mainWindow())
  40. self.node_mover.triggered.connect(self.run)
  41. self.iface.currentLayerChanged.connect(self.enable)
  42. toolBar.addAction(self.node_mover)
  43. self.enable()
  44. self.tool = DtSelectVertexTool(self.iface, 2)
  45. def showDialog(self):
  46. flags = QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMaximizeButtonHint # QgisGui.ModalDialogFlags
  47. self.gui = DtMoveNodeByArea_Dialog(self.iface.mainWindow(), flags)
  48. self.gui.initGui()
  49. self.gui.show()
  50. self.gui.unsetTool.connect(self.unsetTool)
  51. self.gui.moveNode.connect(self.moveNode)
  52. def enableVertexTool(self):
  53. self.canvas.setMapTool(self.tool)
  54. #Connect to the DtSelectVertexTool
  55. self.tool.vertexFound.connect(self.storeVertexPointsAndMarkers)
  56. def unsetTool(self):
  57. self.m1 = None
  58. self.m2 = None
  59. self.p1 = None
  60. self.p2 = None
  61. self.selected_feature = None
  62. self.canvas.unsetMapTool(self.tool)
  63. def run(self):
  64. '''Function that does all the real work'''
  65. layer = self.iface.activeLayer()
  66. if(layer.dataProvider().wkbType() == 6):
  67. self.multipolygon_detected = True
  68. title = QtWidgets.QApplication.translate("digitizingtools", "Move polygon node by area")
  69. if layer.selectedFeatureCount() == 0:
  70. QtWidgets.QMessageBox.information(None, title, QtWidgets.QApplication.translate("digitizingtools", "Please select one polygon to edit."))
  71. elif layer.selectedFeatureCount() > 1:
  72. QtWidgets.QMessageBox.information(None, title, QtWidgets.QApplication.translate("digitizingtools", "Please select only one polygon to edit."))
  73. else:
  74. #One selected feature
  75. self.selected_feature = layer.selectedFeatures()[0]
  76. self.enableVertexTool()
  77. self.showDialog()
  78. self.gui.writeArea(self.selected_feature.geometry().area())
  79. def storeVertexPointsAndMarkers(self, result):
  80. self.p1 = result[0][0]
  81. self.p2 = result[0][1]
  82. self.m1 = result[1][0]
  83. self.m2 = result[1][1]
  84. def enable(self):
  85. '''Enables/disables the corresponding button.'''
  86. # Disable the Button by default
  87. self.node_mover.setEnabled(False)
  88. layer = self.iface.activeLayer()
  89. if layer != None:
  90. #Only for vector layers.
  91. if layer.type() == QgsMapLayer.VectorLayer:
  92. # only for polygon layers
  93. if layer.geometryType() == 2:
  94. # enable if editable
  95. self.node_mover.setEnabled(layer.isEditable())
  96. try:
  97. layer.editingStarted.disconnect(self.enable) # disconnect, will be reconnected
  98. except:
  99. pass
  100. try:
  101. layer.editingStopped.disconnect(self.enable) # when it becomes active layer again
  102. except:
  103. pass
  104. layer.editingStarted.connect(self.enable)
  105. layer.editingStopped.connect(self.enable)
  106. def moveNode(self):
  107. new_a = -1.0
  108. try:
  109. new_a = float(self.gui.targetArea.text())
  110. except:
  111. pass
  112. if (new_a == -1.0):
  113. QtWidgets.QMessageBox.information(None, QtWidgets.QApplication.translate("digitizingtools", "Cancel"), QtWidgets.QApplication.translate("digitizingtools", "Target Area not valid."))
  114. return
  115. if self.p1 == None or self.p2 == None:
  116. QtWidgets.QMessageBox.information(None, QtWidgets.QApplication.translate("digitizingtools", "Cancel"), QtWidgets.QApplication.translate("digitizingtools", "Not enough vertices selected."))
  117. else:
  118. interp1 = self.selected_feature.geometry().intersects(QgsGeometry.fromPointXY(self.p1))
  119. interp2 = self.selected_feature.geometry().intersects(QgsGeometry.fromPointXY(self.p2))
  120. touch_p1_p2 = self.selected_feature.geometry().touches(QgsGeometry.fromPolyline([QgsPoint(self.p1), QgsPoint(self.p2)]))
  121. if (interp1 and interp2):
  122. if (not touch_p1_p2):
  123. QtWidgets.QMessageBox.information(None, QtWidgets.QApplication.translate("digitizingtools", "Cancel"), QtWidgets.QApplication.translate("digitizingtools", "Selected vertices should be consecutive on the selected polygon."))
  124. else:
  125. new_geom = createNewGeometry(self.selected_feature.geometry(), self.p1, self.p2, new_a, self.multipolygon_detected)
  126. fid = self.selected_feature.id()
  127. layer = self.iface.activeLayer()
  128. layer.beginEditCommand(QtWidgets.QApplication.translate("editcommand", "Move Node By Area"))
  129. layer.changeGeometry(fid,new_geom)
  130. self.canvas.refresh()
  131. layer.endEditCommand()
  132. #wkt_tmp1 = self.selected_feature.geometry().exportToWkt()
  133. #wkt_tmp2 = new_geom.exportToWkt()
  134. #tmp_str = wkt_tmp1 + " Initial Area:" + str(self.selected_feature.geometry().area()) + " " + wkt_tmp2 + " Final Area:" + str(new_geom.area())
  135. #title = QtWidgets.QApplication.translate("digitizingtools", "Move polygon node by area")
  136. #QtGui.QMessageBox.information(None, title, QtWidgets.QApplication.translate("digitizingtools", tmp_str))
  137. else:
  138. QtWidgets.QMessageBox.information(None, QtWidgets.QApplication.translate("digitizingtools", "Cancel"), QtWidgets.QApplication.translate("digitizingtools", "Vertices not on the selected polygon."))
  139. # p1 is the stable node (red) and p2 is the node to move (blue)
  140. def createNewGeometry(geom, p1, p2, new_area, multipolygon):
  141. #Read input polygon geometry as a list of QgsPoints
  142. pointList = []
  143. if(multipolygon):
  144. pointList = geom.asMultiPolygon()[0][0][0:-1]
  145. else:
  146. pointList = geom.asPolygon()[0][0:-1]
  147. #indices
  148. ind = 0
  149. ind_max = len(pointList)-1
  150. p1_indx = -1
  151. p2_indx = -1
  152. p3_indx = -1
  153. #find p1 and p2 in the list
  154. for tmp_point in pointList:
  155. if (tmp_point == p1):
  156. p1_indx = ind
  157. elif (tmp_point == p2):
  158. p2_indx = ind
  159. ind += 1
  160. #locate p3 index based on positioning of p1 and p2
  161. if(p2_indx > p1_indx):
  162. if(p2_indx < ind_max):
  163. p3_indx = p2_indx + 1
  164. elif(p2_indx == ind_max and p1_indx == 0):
  165. p3_indx = p2_indx - 1
  166. elif(p2_indx == ind_max and p1_indx != 0):
  167. p3_indx = 0
  168. elif(p1_indx > p2_indx):
  169. if(p2_indx > 0):
  170. p3_indx = p2_indx - 1
  171. elif(p2_indx == 0 and p1_indx == ind_max):
  172. p3_indx = p2_indx + 1
  173. elif(p2_indx == 0 and p1_indx != ind_max):
  174. p3_indx = ind_max
  175. x1 = p1.x()
  176. y1 = p1.y()
  177. x2 = p2.x()
  178. y2 = p2.y()
  179. x3 = pointList[p3_indx].x()
  180. y3 = pointList[p3_indx].y()
  181. old_area = geom.area()
  182. area_diff = new_area-old_area
  183. (x2a,y2a, x2b,y2b)=move_vertex(x1,y1,x2,y2,x3,y3,area_diff)
  184. p2a = QgsPointXY(x2a,y2a)
  185. p2b = QgsPointXY(x2b,y2b)
  186. pointList[p2_indx] = p2a
  187. geom1 = QgsGeometry.fromPolygonXY( [ pointList ] )
  188. pointList[p2_indx] = p2b
  189. geom2 = QgsGeometry.fromPolygonXY( [ pointList ] )
  190. diff_geom1 = abs(geom1.area() - new_area)
  191. diff_geom2 = abs(geom2.area() - new_area)
  192. if(diff_geom1 < diff_geom2):
  193. return geom1
  194. else:
  195. return geom2
  196. def move_vertex(x1,y1,x2,y2,x3,y3,area):
  197. """
  198. This function moves point 2 of 1-2 vertex on 2-3 direction resulting
  199. a new 1-4 vertex. Area is the desired area of the triangle 1-2-4.
  200. Result is returned as [ xa ya xb yb ] due to absolute value.
  201. Use resulted area of new polygon is the final criterion.
  202. * copyright : (C) 2013 by Christos Iossifidis
  203. * email : chiossif@yahoo.com
  204. """
  205. k=(y3-y2)/(x3-x2) #(I)
  206. #k=(y4-y2)/(x4-x2) ===>
  207. #x4 = x2 + (y4-y2)/k (IIa)
  208. #y4 = y2 + k*(x4-x2) (IIb)
  209. #2*area=ABS(x1*(y2-y4)+x2*(y4-y1)+x4*(y1-y2)) (III)
  210. #(III) ==(IIa)==>
  211. #2*area=ABS( x1*(y2-y4)+x2*(y4-y1)+(x2+(y4-y2)/k)*(y1-y2) ) ===>
  212. #2*area=ABS( x1*(y2-y4)+x2*(y4-y1)+ x2*(y1-y2) + (y4-y2)*(y1-y2)/k ) ===>
  213. #2*area=ABS( x1*y2 -x1*y4 +x2*y4 -x2*y1+ x2*y1-x2*y2 +y4*y1/k -y4*y2/k -y2*y1/k +y2*y2/k ) ===>
  214. #2*area=ABS( x1*y2 -x2*y1+ x2*y1-x2*y2 -y2*y1/k +y2*y2/k ) ===>
  215. #x1*y4 -x2*y4 -y4*y1/k +y4*y2/k = +-2*area + ( x1*y2 -x2*y1+ x2*y1-x2*y2 -y2*y1/k +y2*y2/k ) ===>
  216. #y4 = (+-2*area + ( x1*y2 -x2*y1+ x2*y1-x2*y2 -y2*y1/k +y2*y2/k ) ) / ( x1-x2-y1/k+y2/k ) (IV)
  217. #(IV)===>
  218. y4a = ( 2.0*area + ( x1*y2 -x2*y1+ x2*y1-x2*y2 -y2*y1/k +y2*y2/k ) ) / ( x1-x2-y1/k+y2/k )
  219. #(IIa) ===>
  220. x4a = x2 + (y4a-y2)/k
  221. #(IV)===>
  222. y4b = ( -2.0*area + ( x1*y2 -x2*y1+ x2*y1-x2*y2 -y2*y1/k +y2*y2/k ) ) / ( x1-x2-y1/k+y2/k )
  223. #(IIa) ===>
  224. x4b = x2 + (y4b-y2)/k
  225. return (x4a,y4a, x4b,y4b)