dtmovesidebyarea.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. # -*- coding: utf-8 -*-
  2. """
  3. dtmovesidebyarea
  4. ````````````````
  5. Part of DigitizingTools, a QGIS plugin that
  6. subsumes different tools neded during digitizing sessions
  7. * begin : 2013-08-15
  8. * copyright : (C) 2013 by Angelos Tzotsos
  9. * email : tzotsos@gmail.com
  10. This program is free software; you can redistribute it and/or modify
  11. it under the terms of the GNU General Public License as published by
  12. the Free Software Foundation; either version 2 of the License, or
  13. (at your option) any later version.
  14. """
  15. from __future__ import print_function
  16. from builtins import range
  17. from builtins import object
  18. from qgis.PyQt import QtCore, QtGui, QtWidgets
  19. from qgis.core import *
  20. from qgis.gui import *
  21. import dt_icons_rc
  22. import math
  23. from dttools import DtSelectSegmentTool
  24. from dtmovesidebyarea_dialog import DtMoveSideByArea_Dialog
  25. class DtMoveSideByArea(object):
  26. '''Parallel move polygon side in order to achieve a desired polygon area'''
  27. def __init__(self, iface, toolBar):
  28. # Save reference to the QGIS interface
  29. self.iface = iface
  30. self.canvas = self.iface.mapCanvas()
  31. self.gui = None
  32. self.multipolygon_detected = False
  33. # points of the selected segment
  34. # p1 is always the left point
  35. self.p1 = None
  36. self.p2 = None
  37. self.rb1 = QgsRubberBand(self.canvas, QgsWkbTypes.LineGeometry)
  38. self.selected_feature = None
  39. #create action
  40. self.side_mover = QtWidgets.QAction(QtGui.QIcon(":/ParallelMovePolygonSideByArea.png"),
  41. QtWidgets.QApplication.translate("digitizingtools", "Parallel move of polygon side to target area"), self.iface.mainWindow())
  42. self.side_mover.triggered.connect(self.run)
  43. self.iface.currentLayerChanged.connect(self.enable)
  44. toolBar.addAction(self.side_mover)
  45. self.enable()
  46. self.tool = DtSelectSegmentTool(self.iface)
  47. def showDialog(self):
  48. flags = QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMaximizeButtonHint # QgisGui.ModalDialogFlags
  49. self.gui = DtMoveSideByArea_Dialog(self.iface.mainWindow(), flags)
  50. self.gui.initGui()
  51. self.gui.show()
  52. self.gui.unsetTool.connect(self.unsetTool)
  53. self.gui.moveSide.connect(self.moveSide)
  54. def enableSegmentTool(self):
  55. self.canvas.setMapTool(self.tool)
  56. #Connect to the DtSelectVertexTool
  57. self.tool.segmentFound.connect(self.storeSegmentPoints)
  58. def unsetTool(self):
  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 side 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.enableSegmentTool()
  77. self.showDialog()
  78. self.gui.writeArea(self.selected_feature.geometry().area())
  79. def storeSegmentPoints(self, result):
  80. if result[0].x() < result[1].x():
  81. self.p1 = result[0]
  82. self.p2 = result[1]
  83. elif result[0].x() == result[1].x():
  84. self.p1 = result[0]
  85. self.p2 = result[1]
  86. else:
  87. self.p1 = result[1]
  88. self.p2 = result[0]
  89. def enable(self):
  90. '''Enables/disables the corresponding button.'''
  91. # Disable the Button by default
  92. self.side_mover.setEnabled(False)
  93. layer = self.iface.activeLayer()
  94. if layer != None:
  95. #Only for vector layers.
  96. if layer.type() == QgsMapLayer.VectorLayer:
  97. # only for polygon layers
  98. if layer.geometryType() == 2:
  99. # enable if editable
  100. self.side_mover.setEnabled(layer.isEditable())
  101. try:
  102. layer.editingStarted.disconnect(self.enable) # disconnect, will be reconnected
  103. except:
  104. pass
  105. try:
  106. layer.editingStopped.disconnect(self.enable) # when it becomes active layer again
  107. except:
  108. pass
  109. layer.editingStarted.connect(self.enable)
  110. layer.editingStopped.connect(self.enable)
  111. def moveSide(self):
  112. new_a = -1.0
  113. try:
  114. new_a = float(self.gui.targetArea.text())
  115. except:
  116. pass
  117. if (new_a == -1.0):
  118. QtWidgets.QMessageBox.information(None, QtWidgets.QApplication.translate("digitizingtools", "Cancel"), QtWidgets.QApplication.translate("digitizingtools", "Target Area not valid."))
  119. return
  120. if self.p1 == None or self.p2 == None:
  121. QtWidgets.QMessageBox.information(None, QtWidgets.QApplication.translate("digitizingtools", "Cancel"), QtWidgets.QApplication.translate("digitizingtools", "Polygon side not selected."))
  122. else:
  123. touch_p1_p2 = self.selected_feature.geometry().touches(QgsGeometry.fromPolyline([QgsPoint(self.p1), QgsPoint(self.p2)]))
  124. if (not touch_p1_p2):
  125. QtWidgets.QMessageBox.information(None, QtWidgets.QApplication.translate("digitizingtools", "Cancel"), QtWidgets.QApplication.translate("digitizingtools", "Selected segment should be on the selected polygon."))
  126. else:
  127. #Select tool to create new geometry here
  128. if self.gui.method == "fixed":
  129. new_geom = moveFixed(self.selected_feature.geometry(), self.p1, self.p2, new_a, self.multipolygon_detected)
  130. else:
  131. new_geom = moveVariable(self.selected_feature.geometry(), self.p1, self.p2, new_a, self.multipolygon_detected)
  132. #Store new geometry on the memory buffer
  133. fid = self.selected_feature.id()
  134. layer = self.iface.activeLayer()
  135. layer.beginEditCommand(QtWidgets.QApplication.translate("editcommand", "Move Side By Area"))
  136. layer.changeGeometry(fid,new_geom)
  137. self.canvas.refresh()
  138. layer.endEditCommand()
  139. def moveFixed(geom, p1, p2, new_area, multipolygon):
  140. pointList = []
  141. if(multipolygon):
  142. pointList = geom.asMultiPolygon()[0][0][0:-1]
  143. else:
  144. pointList = geom.asPolygon()[0][0:-1]
  145. #Read input polygon geometry as a list of QgsPoints
  146. mul = 1.0
  147. ind = 0
  148. p1_indx = -1
  149. p2_indx = -1
  150. #find p1 and p2 in the list
  151. for tmp_point in pointList:
  152. if (tmp_point == p1):
  153. p1_indx = ind
  154. elif (tmp_point == p2):
  155. p2_indx = ind
  156. ind += 1
  157. #Calculate the extra area needed
  158. area_init = geom.area()
  159. area_diff = new_area - area_init
  160. if(area_diff > 0):
  161. growing = True
  162. else:
  163. growing = False
  164. #Find the distance between p1 and p2
  165. dist_p1p2 = math.sqrt(p1.sqrDist(p2))
  166. #Find the initiallizer distance to parallel move
  167. test_dist1 = area_diff / dist_p1p2
  168. test_dist2 = (-1.0)*test_dist1
  169. test_geom1 = getParallelGeomByDistance(geom, p1, p2, p1_indx, p2_indx, test_dist1, multipolygon)
  170. test_area1 = test_geom1.area()
  171. test_geom2 = getParallelGeomByDistance(geom, p1, p2, p1_indx, p2_indx, test_dist2, multipolygon)
  172. test_area2 = test_geom2.area()
  173. if growing:
  174. if (test_area1 > test_area2):
  175. dist_end = 2.0 * test_dist1
  176. dist_start = 0.0
  177. else:
  178. dist_end = 2.0 * test_dist2
  179. dist_start = 0.0
  180. else:
  181. if (test_area1 > test_area2):
  182. dist_start = 2.0 * test_dist2
  183. dist_end = 0.0
  184. else:
  185. dist_start = 2.0 * test_dist1
  186. dist_end = 0.0
  187. EPSILON = 1e-7
  188. dist_mid = dist_start + (dist_end - dist_start)/2.0
  189. for i in range(1000):
  190. dist_mid = dist_start + (dist_end - dist_start)/2.0
  191. geom_mid = getParallelGeomByDistance(geom, p1, p2, p1_indx, p2_indx, dist_mid, multipolygon)
  192. area_mid = geom_mid.area()
  193. if ((math.fabs(area_mid-new_area)) < EPSILON):
  194. print ("wanted area reached")
  195. print (area_mid)
  196. break
  197. elif (area_mid < new_area):
  198. dist_start = dist_mid
  199. else:
  200. dist_end = dist_mid
  201. return geom_mid
  202. def getParallelGeomByDistance(geom, p1, p2, p1_indx, p2_indx, dist, multipolygon):
  203. pointList = []
  204. if(multipolygon):
  205. pointList = geom.asMultiPolygon()[0][0][0:-1]
  206. else:
  207. pointList = geom.asPolygon()[0][0:-1]
  208. #Read input polygon geometry as a list of QgsPoints
  209. (p3, p4) = getParallelLinePointsByDistance(p1, p2, dist)
  210. pointList[p1_indx] = p3
  211. pointList[p2_indx] = p4
  212. new_geom = QgsGeometry.fromPolygonXY( [ pointList ] )
  213. return new_geom
  214. def getParallelLinePointsByDistance(p1, p2, dist):
  215. """
  216. This function is adopted/adapted from 'CadTools Plugin', Copyright (C) Stefan Ziegler
  217. """
  218. if dist == 0:
  219. g = (p1, p2)
  220. return g
  221. dn = ( (p1.x()-p2.x())**2 + (p1.y()-p2.y())**2 )**0.5
  222. x3 = p1.x() + dist*(p1.y()-p2.y()) / dn
  223. y3 = p1.y() - dist*(p1.x()-p2.x()) / dn
  224. p3 = QgsPointXY(x3, y3)
  225. x4 = p2.x() + dist*(p1.y()-p2.y()) / dn
  226. y4 = p2.y() - dist*(p1.x()-p2.x()) / dn
  227. p4 = QgsPointXY(x4, y4)
  228. g = (p3,p4)
  229. return g
  230. def moveVariable(geom, p1, p2, new_area, multipolygon):
  231. #Read input polygon geometry as a list of QgsPoints
  232. pointList = []
  233. if(multipolygon):
  234. pointList = geom.asMultiPolygon()[0][0][0:-1]
  235. else:
  236. pointList = geom.asPolygon()[0][0:-1]
  237. #indices
  238. ind = 0
  239. ind_max = len(pointList)-1
  240. p1_indx = -1
  241. p2_indx = -1
  242. p3_indx = -1
  243. p4_indx = -1
  244. #find p1 and p2 in the list
  245. for tmp_point in pointList:
  246. if (tmp_point == p1):
  247. p1_indx = ind
  248. elif (tmp_point == p2):
  249. p2_indx = ind
  250. ind += 1
  251. #locate p3,p4 index based on positioning of p1 and p2
  252. if(p2_indx > p1_indx):
  253. if(p2_indx < ind_max):
  254. p3_indx = p1_indx - 1
  255. p4_indx = p2_indx + 1
  256. elif(p2_indx == ind_max and p1_indx == 0):
  257. p4_indx = p2_indx - 1
  258. p3_indx = p1_indx + 1
  259. elif(p2_indx == ind_max and p1_indx != 0):
  260. p3_indx = p1_indx - 1
  261. p4_indx = 0
  262. elif(p1_indx > p2_indx):
  263. if(p2_indx > 0):
  264. p4_indx = p2_indx - 1
  265. p3_indx = p1_indx + 1
  266. elif(p2_indx == 0 and p1_indx == ind_max):
  267. p4_indx = p2_indx + 1
  268. p3_indx = p1_indx - 1
  269. elif(p2_indx == 0 and p1_indx != ind_max):
  270. p4_indx = ind_max
  271. p3_indx = p1_indx + 1
  272. x2 = p1.x()
  273. y2 = p1.y()
  274. x4 = p2.x()
  275. y4 = p2.y()
  276. x1 = pointList[p3_indx].x()
  277. y1 = pointList[p3_indx].y()
  278. x3 = pointList[p4_indx].x()
  279. y3 = pointList[p4_indx].y()
  280. old_area = geom.area()
  281. area_diff = new_area-old_area
  282. (x5,y5,x6,y6) = move_vertex_trapezoid(x1,y1,x2,y2,x3,y3,x4,y4,area_diff)
  283. p5 = QgsPointXY(x5,y5)
  284. p6 = QgsPointXY(x6,y6)
  285. pointList[p1_indx] = p5
  286. pointList[p2_indx] = p6
  287. new_geom = QgsGeometry.fromPolygonXY( [ pointList ] )
  288. return new_geom
  289. def move_vertex_trapezoid(x1,y1,x2,y2,x3,y3,x4,y4,area):
  290. """
  291. This function moves vertex 2-4 parallel by forming a trapezoid of
  292. area resulting a new 5-6 vertex. Result is returned as [x5,y5,x6,y6].
  293. * copyright : (C) 2013 by Christos Iossifidis
  294. * email : chiossif@yahoo.com
  295. """
  296. EPSILON=1e-9 #This is approximation accuracy
  297. AWAY_STEP=1000.0 #This is the beyond step factor. It is too big already ;-)
  298. k1=(y2-y1)/(x2-x1) #(I)
  299. k2=(y4-y3)/(x4-x3) #(II)
  300. k3=(y4-y2)/(x4-x2) #(III)
  301. #k3=(y6-y5)/(x6-x5) ===>
  302. #x6 = x5 + (y6-y5)/k3 (IVa)
  303. #y6 = y5 + k3*(x6-x5) (IVb)
  304. #k1=(y5-y2)/(x5-x2) ===>
  305. #x5 = x2 + (y5-y2)/k1 (Va)
  306. #y5 = y2 + k1*(x5-x2) (Vb)
  307. #k2=(y6-y4)/(x6-x4) ===>
  308. #x6 = x4 + (y6-y4)/k1 (VIa)
  309. #y6 = y4 + k2*(x6-x4) (VIb)
  310. #2*area=ABS( x5*(y2-y4)+x2*(y4-y5)+x4*(y5-y2) ) + ABS ( x5*(y4-y6)+x4*(y6-y5)+x6*(y5-y4) ) (VII)
  311. #(VIb)==(IVa)==>
  312. #y6 = y4 + k2*( x5 + (y6-y5)/k3 - x4)===>
  313. #y6 = y4 + k2*x5 + k2*y6/k3 - k2*y5/k3 -k2*x4 ===>
  314. #y6 - k2/k3*y6 = y4 + k2*x5 - k2*y5/k3 -k2*x4 ===>
  315. #y6 = (y4 + k2*x5 - k2*y5/k3 -k2*x4) / (1.0 - k2/k3) (VIII)
  316. if (area<0.0):
  317. area=abs(area)
  318. start=x1 #starting values
  319. stop=x2
  320. for i in range(100):
  321. x5=(start+stop)/2.0
  322. #(Vb)===>
  323. y5= y2 + k1*(x5-x2)
  324. #(VIII)===>
  325. y6 = (y4 + k2*x5 - k2*y5/k3 -k2*x4) / (1.0 - k2/k3)
  326. #(VIa)===>
  327. x6 = x4 + (y6-y4)/k2
  328. #(VII)===>
  329. new_area=(abs( x5*(y2-y4)+x2*(y4-y5)+x4*(y5-y2) ) + abs( x5*(y4-y6)+x4*(y6-y5)+x6*(y5-y4) ))/2.0
  330. if (abs(area-new_area)<EPSILON):
  331. break
  332. elif (area > new_area):
  333. stop=x5
  334. else:
  335. start=x5
  336. else:
  337. area=abs(area)
  338. start=x2 #starting values
  339. stop=x2 + AWAY_STEP*(x2-x1) #AWAY_STEP times the 2-1 distance plus x2
  340. for i in range(100):
  341. x5=(start+stop)/2.0
  342. #(Vb)===>
  343. y5= y2 + k1*(x5-x2)
  344. #(VIII)===>
  345. y6 = (y4 + k2*x5 - k2*y5/k3 -k2*x4) / (1.0 - k2/k3)
  346. #(VIa)===>
  347. x6 = x4 + (y6-y4)/k2
  348. #(VII)===>
  349. new_area=(abs( x5*(y2-y4)+x2*(y4-y5)+x4*(y5-y2) ) + abs( x5*(y4-y6)+x4*(y6-y5)+x6*(y5-y4) ))/2.0
  350. if (abs(area-new_area)<EPSILON):
  351. break
  352. elif area<new_area:
  353. stop=x5
  354. else:
  355. start=x5
  356. return (x5,y5,x6,y6)