PointsDisplacement.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. """
  2. ***************************************************************************
  3. PointsDisplacement.py
  4. ---------------------
  5. Date : July 2013
  6. Copyright : (C) 2013 by Alexander Bruy
  7. Email : alexander dot bruy at gmail dot com
  8. ***************************************************************************
  9. * *
  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. ***************************************************************************
  16. """
  17. __author__ = 'Alexander Bruy'
  18. __date__ = 'July 2013'
  19. __copyright__ = '(C) 2013, Alexander Bruy'
  20. import math
  21. from qgis.core import (QgsFeatureSink,
  22. QgsGeometry,
  23. QgsPointXY,
  24. QgsSpatialIndex,
  25. QgsRectangle,
  26. QgsProcessing,
  27. QgsProcessingException,
  28. QgsProcessingParameterFeatureSource,
  29. QgsProcessingParameterDistance,
  30. QgsProcessingParameterBoolean,
  31. QgsProcessingParameterFeatureSink)
  32. from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
  33. class PointsDisplacement(QgisAlgorithm):
  34. INPUT = 'INPUT'
  35. DISTANCE = 'DISTANCE'
  36. PROXIMITY = 'PROXIMITY'
  37. HORIZONTAL = 'HORIZONTAL'
  38. OUTPUT = 'OUTPUT'
  39. def group(self):
  40. return self.tr('Vector geometry')
  41. def groupId(self):
  42. return 'vectorgeometry'
  43. def __init__(self):
  44. super().__init__()
  45. def initAlgorithm(self, config=None):
  46. self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT,
  47. self.tr('Input layer'), [QgsProcessing.TypeVectorPoint]))
  48. param = QgsProcessingParameterDistance(self.PROXIMITY,
  49. self.tr('Minimum distance to other points'), parentParameterName='INPUT',
  50. minValue=0.00001, defaultValue=1.0)
  51. param.setMetadata({'widget_wrapper': {'decimals': 5}})
  52. self.addParameter(param)
  53. param = QgsProcessingParameterDistance(self.DISTANCE,
  54. self.tr('Displacement distance'), parentParameterName='INPUT',
  55. minValue=0.00001, defaultValue=1.0)
  56. param.setMetadata({'widget_wrapper': {'decimals': 5}})
  57. self.addParameter(param)
  58. self.addParameter(QgsProcessingParameterBoolean(self.HORIZONTAL,
  59. self.tr('Horizontal distribution for two point case')))
  60. self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, self.tr('Displaced'), QgsProcessing.TypeVectorPoint))
  61. def name(self):
  62. return 'pointsdisplacement'
  63. def displayName(self):
  64. return self.tr('Points displacement')
  65. def processAlgorithm(self, parameters, context, feedback):
  66. source = self.parameterAsSource(parameters, self.INPUT, context)
  67. if source is None:
  68. raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT))
  69. proximity = self.parameterAsDouble(parameters, self.PROXIMITY, context)
  70. radius = self.parameterAsDouble(parameters, self.DISTANCE, context)
  71. horizontal = self.parameterAsBoolean(parameters, self.HORIZONTAL, context)
  72. (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
  73. source.fields(), source.wkbType(), source.sourceCrs())
  74. if sink is None:
  75. raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT))
  76. features = source.getFeatures()
  77. total = 100.0 / source.featureCount() if source.featureCount() else 0
  78. def searchRect(p):
  79. return QgsRectangle(p.x() - proximity, p.y() - proximity, p.x() + proximity, p.y() + proximity)
  80. index = QgsSpatialIndex()
  81. # NOTE: this is a Python port of QgsPointDistanceRenderer::renderFeature. If refining this algorithm,
  82. # please port the changes to QgsPointDistanceRenderer::renderFeature also!
  83. clustered_groups = []
  84. group_index = {}
  85. group_locations = {}
  86. for current, f in enumerate(features):
  87. if feedback.isCanceled():
  88. break
  89. if not f.hasGeometry():
  90. continue
  91. point = f.geometry().asPoint()
  92. other_features_within_radius = index.intersects(searchRect(point))
  93. if not other_features_within_radius:
  94. index.addFeature(f)
  95. group = [f]
  96. clustered_groups.append(group)
  97. group_index[f.id()] = len(clustered_groups) - 1
  98. group_locations[f.id()] = point
  99. else:
  100. # find group with closest location to this point (may be more than one within search tolerance)
  101. min_dist_feature_id = other_features_within_radius[0]
  102. min_dist = group_locations[min_dist_feature_id].distance(point)
  103. for i in range(1, len(other_features_within_radius)):
  104. candidate_id = other_features_within_radius[i]
  105. new_dist = group_locations[candidate_id].distance(point)
  106. if new_dist < min_dist:
  107. min_dist = new_dist
  108. min_dist_feature_id = candidate_id
  109. group_index_pos = group_index[min_dist_feature_id]
  110. group = clustered_groups[group_index_pos]
  111. # calculate new centroid of group
  112. old_center = group_locations[min_dist_feature_id]
  113. group_locations[min_dist_feature_id] = QgsPointXY((old_center.x() * len(group) + point.x()) / (len(group) + 1.0),
  114. (old_center.y() * len(group) + point.y()) / (len(group) + 1.0))
  115. # add to a group
  116. clustered_groups[group_index_pos].append(f)
  117. group_index[f.id()] = group_index_pos
  118. feedback.setProgress(int(current * total))
  119. current = 0
  120. total = 100.0 / len(clustered_groups) if clustered_groups else 1
  121. feedback.setProgress(0)
  122. fullPerimeter = 2 * math.pi
  123. for group in clustered_groups:
  124. if feedback.isCanceled():
  125. break
  126. count = len(group)
  127. if count == 1:
  128. sink.addFeature(group[0], QgsFeatureSink.FastInsert)
  129. else:
  130. angleStep = fullPerimeter / count
  131. if count == 2 and horizontal:
  132. currentAngle = math.pi / 2
  133. else:
  134. currentAngle = 0
  135. old_point = group_locations[group[0].id()]
  136. for f in group:
  137. if feedback.isCanceled():
  138. break
  139. sinusCurrentAngle = math.sin(currentAngle)
  140. cosinusCurrentAngle = math.cos(currentAngle)
  141. dx = radius * sinusCurrentAngle
  142. dy = radius * cosinusCurrentAngle
  143. # we want to keep any existing m/z values
  144. point = f.geometry().constGet().clone()
  145. point.setX(old_point.x() + dx)
  146. point.setY(old_point.y() + dy)
  147. f.setGeometry(QgsGeometry(point))
  148. sink.addFeature(f, QgsFeatureSink.FastInsert)
  149. currentAngle += angleStep
  150. current += 1
  151. feedback.setProgress(int(current * total))
  152. return {self.OUTPUT: dest_id}