Skip to content

Commit

Permalink
added data defined overrides to geodesic transformation tool.
Browse files Browse the repository at this point in the history
  • Loading branch information
hamiltoncj committed Aug 24, 2021
1 parent f23d333 commit 1af4512
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 131 deletions.
6 changes: 2 additions & 4 deletions doc/GeodesicTransformationsAlgorithm.help
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
Geodesically transform a shape. It supports scaling, rotation and translation. The size and geometry of each shape will be retained regardless of the projection.
Geodesically transform a shape. It supports scaling, rotation and translation. Each of these properties can come from individual data defined override expressions, making this a very powerful algorithm. The ralative size and geometry of each shape will be retained regardless of the projection.

* Input vector layer - Select an existing point, line, or polygon vector layer.
* Input layer - Select an existing point, line, or polygon vector layer.
* Selected features only - Checking this box will cause the algorithm to only transform the selected features.
* Rotation angle about the centroid - Rotate the feature about its centroid. A positive angle rotates in a clockwise direction.
* Scale factor about the centroid - Scale the shape about its centroid. A scale factor of 1 retains its same size.
* Translation distance - Distance the shape will be moved along a geodesic path.
* Translation azimuth - Azimuth or direction the shape will be moved along a geodesic path.
* Translation distance units - Units of distance the shape will be move.
* Output layer - The output layer that will be created in QGIS.

Binary file added doc/datadefined.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified doc/geodesictransform.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/geodesictransformexample.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
288 changes: 166 additions & 122 deletions geodesicTransformation.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import os
from geographiclib.geodesic import Geodesic

from qgis.core import (QgsPoint, QgsProject, QgsCoordinateTransform)
from qgis.core import (QgsPoint, QgsProject, QgsCoordinateTransform, QgsPropertyDefinition)

from qgis.core import (
QgsProcessing,
QgsProcessingAlgorithm,
QgsProcessingFeatureBasedAlgorithm,
QgsProcessingParameters,
QgsProcessingParameterNumber,
QgsProcessingParameterEnum,
QgsProcessingParameterFeatureSource,
Expand All @@ -17,134 +18,19 @@
from .settings import epsg4326, geod
from .utils import tr, conversionToMeters, DISTANCE_LABELS

class GeodesicTransformationsAlgorithm(QgsProcessingAlgorithm):
class GeodesicTransformationsAlgorithm(QgsProcessingFeatureBasedAlgorithm):
"""
Algorithm to transform geometry shapes.
"""

PrmInputLayer = 'InputLayer'
PrmOutputLayer = 'OutputLayer'
PrmTransformRotation = 'TransformRotation'
PrmTransformScale = 'TransformScale'
PrmTransformAzimuth = 'TransformAzimuth'
PrmTransformDistance = 'TransformDistance'
PrmTransformUnits = 'TransformUnits'

def initAlgorithm(self, config):
self.addParameter(
QgsProcessingParameterFeatureSource(
self.PrmInputLayer,
tr('Input vector layer'),
[QgsProcessing.TypeVectorAnyGeometry])
)
self.addParameter(
QgsProcessingParameterNumber(
self.PrmTransformRotation,
tr('Rotation angle about the centroid'),
QgsProcessingParameterNumber.Double,
defaultValue=0,
optional=True)
)
self.addParameter(
QgsProcessingParameterNumber(
self.PrmTransformScale,
tr('Scale factor about the centroid'),
QgsProcessingParameterNumber.Double,
defaultValue=1,
optional=True)
)
self.addParameter(
QgsProcessingParameterNumber(
self.PrmTransformDistance,
tr('Translation distance'),
QgsProcessingParameterNumber.Double,
defaultValue=0,
optional=True)
)
self.addParameter(
QgsProcessingParameterNumber(
self.PrmTransformAzimuth,
tr('Translation azimuth'),
QgsProcessingParameterNumber.Double,
defaultValue=0,
optional=True)
)
self.addParameter(
QgsProcessingParameterEnum(
self.PrmTransformUnits,
tr('Translation distance units'),
options=DISTANCE_LABELS,
defaultValue=0,
optional=False)
)
self.addParameter(
QgsProcessingParameterFeatureSink(
self.PrmOutputLayer,
tr('Output layer'))
)

def processAlgorithm(self, parameters, context, feedback):
source = self.parameterAsSource(parameters, self.PrmInputLayer, context)
angle = self.parameterAsDouble(parameters, self.PrmTransformRotation, context)
scale = self.parameterAsDouble(parameters, self.PrmTransformScale, context)
azimuth = self.parameterAsDouble(parameters, self.PrmTransformAzimuth, context)
distance = self.parameterAsDouble(parameters, self.PrmTransformDistance, context)
units = self.parameterAsInt(parameters, self.PrmTransformUnits, context)

to_meters = conversionToMeters(units)
distance = distance * to_meters
src_crs = source.sourceCrs()
wkbtype = source.wkbType()

(sink, dest_id) = self.parameterAsSink(
parameters, self.PrmOutputLayer, context, source.fields(), wkbtype, src_crs)

geom_to_4326 = QgsCoordinateTransform(src_crs, epsg4326, QgsProject.instance())
to_sink_crs = QgsCoordinateTransform(epsg4326, src_crs, QgsProject.instance())

featureCount = source.featureCount()
total = 100.0 / featureCount if featureCount else 0

iterator = source.getFeatures()
for cnt, feature in enumerate(iterator):
if feedback.isCanceled():
break
geom = feature.geometry()
# Find the centroid of the vector shape. We will resize everything based on this
centroid = geom.centroid().asPoint()
centroid = geom_to_4326.transform(centroid.x(), centroid.y())
cy = centroid.y()
cx = centroid.x()
if distance != 0:
g = geod.Direct(cy, cx, azimuth, distance, Geodesic.LATITUDE | Geodesic.LONGITUDE)
new_centroid = QgsPoint(g['lon2'], g['lat2'])
else:
new_centroid = centroid

# Find the x & y coordinates of the new centroid
ncy = new_centroid.y()
ncx = new_centroid.x()

vertices = geom.vertices()
for vcnt, vertex in enumerate(vertices):
v = geom_to_4326.transform(vertex.x(), vertex.y())
gline = geod.Inverse(cy, cx, v.y(), v.x())
vdist = gline['s12']
vazi = gline['azi1']
if scale != 1:
vdist = vdist * scale
if angle != 0:
vazi += angle
g = geod.Direct(ncy, ncx, vazi, vdist, Geodesic.LATITUDE | Geodesic.LONGITUDE)
new_vertex = to_sink_crs.transform(g['lon2'], g['lat2'])
geom.moveVertex(new_vertex.x(), new_vertex.y(), vcnt)
feature.setGeometry(geom)
sink.addFeature(feature)

if cnt % 100 == 0:
feedback.setProgress(int(cnt * total))

return {self.PrmOutputLayer: dest_id}
def createInstance(self):
return GeodesicTransformationsAlgorithm()

def name(self):
return 'geodesictransformations'
Expand All @@ -161,6 +47,9 @@ def group(self):
def groupId(self):
return 'vectorgeometry'

def outputName(self):
return tr('Transformed layer')

def helpUrl(self):
file = os.path.dirname(__file__) + '/index.html'
if not os.path.exists(file):
Expand All @@ -175,5 +64,160 @@ def shortHelpString(self):
help = helpf.read()
return help

def createInstance(self):
return GeodesicTransformationsAlgorithm()
def inputLayerTypes(self):
return [QgsProcessing.TypeVectorAnyGeometry]

def outputWkbType(self, input_wkb_type):
return input_wkb_type

def initParameters(self, config=None):
param = QgsProcessingParameterNumber(
self.PrmTransformRotation,
tr('Rotation angle about the centroid'),
QgsProcessingParameterNumber.Double,
defaultValue=0,
optional=True)
param.setIsDynamic(True)
param.setDynamicPropertyDefinition( QgsPropertyDefinition(
self.PrmTransformRotation,
tr('Rotation angle about the centroid'),
QgsPropertyDefinition.Double ))
param.setDynamicLayerParameterName('INPUT')
self.addParameter(param)

param = QgsProcessingParameterNumber(
self.PrmTransformScale,
tr('Scale factor about the centroid'),
QgsProcessingParameterNumber.Double,
defaultValue=1,
optional=True)
param.setIsDynamic(True)
param.setDynamicPropertyDefinition( QgsPropertyDefinition(
self.PrmTransformScale,
tr('Scale factor about the centroid'),
QgsPropertyDefinition.Double ))
param.setDynamicLayerParameterName('INPUT')
self.addParameter(param)

param = QgsProcessingParameterNumber(
self.PrmTransformDistance,
tr('Translation distance'),
QgsProcessingParameterNumber.Double,
defaultValue=0,
optional=True)
param.setIsDynamic(True)
param.setDynamicPropertyDefinition( QgsPropertyDefinition(
self.PrmTransformDistance,
tr('Translation distance'),
QgsPropertyDefinition.Double ))
param.setDynamicLayerParameterName('INPUT')
self.addParameter(param)

param = QgsProcessingParameterNumber(
self.PrmTransformAzimuth,
tr('Translation azimuth'),
QgsProcessingParameterNumber.Double,
defaultValue=0,
optional=True)
param.setIsDynamic(True)
param.setDynamicPropertyDefinition( QgsPropertyDefinition(
self.PrmTransformAzimuth,
tr('Translation azimuth'),
QgsPropertyDefinition.Double ))
param.setDynamicLayerParameterName('INPUT')
self.addParameter(param)

self.addParameter(
QgsProcessingParameterEnum(
self.PrmTransformUnits,
tr('Translation distance units'),
options=DISTANCE_LABELS,
defaultValue=0,
optional=False)
)

def prepareAlgorithm(self, parameters, context, feedback):
self.angle = self.parameterAsDouble(parameters, self.PrmTransformRotation, context)
self.angle_dyn = QgsProcessingParameters.isDynamic(parameters, self.PrmTransformRotation)
if self.angle_dyn:
self.angle_property = parameters[ self.PrmTransformRotation ]

self.scale = self.parameterAsDouble(parameters, self.PrmTransformScale, context)
self.scale_dyn = QgsProcessingParameters.isDynamic(parameters, self.PrmTransformScale)
if self.scale_dyn:
self.scale_property = parameters[ self.PrmTransformScale ]

distance = self.parameterAsDouble(parameters, self.PrmTransformDistance, context)
self.distance_dyn = QgsProcessingParameters.isDynamic(parameters, self.PrmTransformDistance)
if self.distance_dyn:
self.distance_property = parameters[ self.PrmTransformDistance ]

self.azimuth = self.parameterAsDouble(parameters, self.PrmTransformAzimuth, context)
self.azimuth_dyn = QgsProcessingParameters.isDynamic(parameters, self.PrmTransformAzimuth)
if self.azimuth_dyn:
self.azimuth_property = parameters[ self.PrmTransformAzimuth ]

units = self.parameterAsInt(parameters, self.PrmTransformUnits, context)

self.to_meters = conversionToMeters(units)
self.distance = distance * self.to_meters

source = self.parameterAsSource(parameters, 'INPUT', context)
src_crs = source.sourceCrs()
self.geom_to_4326 = QgsCoordinateTransform(src_crs, epsg4326, QgsProject.instance())
self.to_sink_crs = QgsCoordinateTransform(epsg4326, src_crs, QgsProject.instance())
return True

def processFeature(self, feature, context, feedback):
# Check each parameter to see if it is useing data defined expressions
if self.angle_dyn:
angle,_ = self.angle_property.valueAsDouble(context.expressionContext(), self.angle)
else:
angle = self.angle

if self.scale_dyn:
scale,_ = self.scale_property.valueAsDouble(context.expressionContext(), self.scale)
else:
scale = self.scale

if self.azimuth_dyn:
azimuth,_ = self.azimuth_property.valueAsDouble(context.expressionContext(), self.azimuth)
else:
azimuth = self.azimuth

if self.distance_dyn:
distance,_ = self.distance_property.valueAsDouble(context.expressionContext(), self.distance)
distance = distance * self.to_meters
else:
distance = self.distance
geom = feature.geometry()
# Find the centroid of the vector shape. We will resize everything based on this
centroid = geom.centroid().asPoint()
centroid = self.geom_to_4326.transform(centroid.x(), centroid.y())
cy = centroid.y()
cx = centroid.x()
if distance != 0:
g = geod.Direct(cy, cx, azimuth, distance, Geodesic.LATITUDE | Geodesic.LONGITUDE)
new_centroid = QgsPoint(g['lon2'], g['lat2'])
else:
new_centroid = centroid

# Find the x & y coordinates of the new centroid
ncy = new_centroid.y()
ncx = new_centroid.x()

vertices = geom.vertices()
for vcnt, vertex in enumerate(vertices):
v = self.geom_to_4326.transform(vertex.x(), vertex.y())
gline = geod.Inverse(cy, cx, v.y(), v.x())
vdist = gline['s12']
vazi = gline['azi1']
if scale != 1:
vdist = vdist * scale
if angle != 0:
vazi += angle
g = geod.Direct(ncy, ncx, vazi, vdist, Geodesic.LATITUDE | Geodesic.LONGITUDE)
new_vertex = self.to_sink_crs.transform(g['lon2'], g['lat2'])
geom.moveVertex(new_vertex.x(), new_vertex.y(), vcnt)
feature.setGeometry(geom)
return [feature]
8 changes: 6 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -223,11 +223,11 @@ <h2><a name="geodesic-measure-layer"></a> <img alt="Geodesic Measurement Layer"
<div style="text-align:center"><img src="doc/measurement-attributes.jpg" alt="Measurement Attributes"></div>

<h2><a name="geodesic-transformations"></a> <img alt="Geodesic Transformations" src="images/transformShape.svg" /> Geodesic Transformations Tool</h2>
<p>This tool provides the ability to geodesically transform a shape. It supports scaling, rotation and translation. The size and geometry of each shape will be retained regardless of the projection. </p>
<p>This tool provides the ability to geodesically transform a shape. It supports scaling, rotation and translation. Each of these can use data defined override expressions. The relative size and geometry of each shape will be retained regardless of the projection. </p>
<div style="text-align:center"><img src="doc/geodesictransform.jpg" alt="Geodesic Transformations"></div>

<ul>
<li><strong>Input vector layer</strong> - Select an existing point, line, or polygon vector layer.</li>
<li><strong>Input layer</strong> - Select an existing point, line, or polygon vector layer.</li>
<li><strong>Selected features only</strong> - Checking this box will cause the algorithm to only transform the selected features.</li>
<li><strong>Rotation angle about the centroid</strong> - Rotate the feature about its centroid. A positive angle rotates in a clockwise direction.</li>
<li><strong>Scale factor about the centroid</strong> - Scale the shape about its centroid. A scale factor of 1 retains its same size.</li>
Expand All @@ -236,6 +236,10 @@ <h2><a name="geodesic-transformations"></a> <img alt="Geodesic Transformations"
<li><strong>Translation distance units</strong> - Units of distance the shape will be move.</li>
<li><strong>Output layer</strong> - The output layer that will be created in QGIS.</li>
</ul>
<p>To the right of each parameter is the data defined override button <img alt="" src="doc/datadefined.jpg" /> where the default value is overridden with a value from one of the attributes or an expression. This leads to some powerful and creative applications.</p>
<div style="text-align:center"><img src="doc/geodesictransformexample.jpg" alt="Geodesic Transformation Example"></div>

<p>This was created by clicking on the <strong><em>Rotation angle about the centroid</em></strong> data defined override button, clicking on <strong>Edit</strong> and using the expression <code><span style="font-family:'Courier New'"><b>randf( -10, 10)</b></span></code> and for <strong><em>Scale factor about the centroid</em></strong> using the expression <code><span style="font-family:'Courier New'"><b>randf(0.65, 0.85)</b></style></code>. For each polygin in the input layer it rotates them using a random value between -10 and 10 degrees and scales them by a factor betweeen 0.65 and 0.85.</p>
<h2><a name="geodesic-flip"></a> <img alt="Geodesic Flip and Rotate Tools" src="images/flip.svg" /> Geodesic Flip and Rotate Tools</h2>
<p>This is a collection of geodesic tools that transform vector features including the ability to flip horizontally, flip vertically, rotate by 180 degrees, rotate clockwise by 90 degrees, and rotate counter clockwise by 90 degrees. The first is a processing toolbox algorithm that allows the selection of one of these five transforms.</p>
<div style="text-align:center"><img src="doc/fliptool.jpg" alt="Geodesic Flip and Rotate"></div>
Expand Down
3 changes: 2 additions & 1 deletion metadata.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name=Shape Tools
qgisMinimumVersion=3.4
description=Shape Tools is a collection of geodesic shapes and tools. Create ellipse, line of bearing, pie wedge, donut, arc wedge, polygon, star, ellipse rose, hypocyloid, polyfoil, epicycloid, radial line, and heart shapes. Tools include "XY to Line" tool, densify lines and polygons along geodesic paths, geodesic line break, geodesic measuring and create a measurement layer, geodesic scale, rotate, flip and translate tools, and digitize points at an azimuth & distance tools.
version=3.3.17
version=3.3.18
author=C Hamilton
[email protected]
about=Shape Tools is a collection of geodesic shapes and tools. Shape Tools is installed in the Vector menu.
Expand All @@ -26,6 +26,7 @@ experimental=False
deprecated=False
hasProcessingProvider=yes
changelog=
3.3.18 - Added data defined overrides to the geodesic transformations algorithm.
3.3.17 - Addressed issues #37 and #38.
3.3.16 - Arc wedge now creates a donut if a full 360 degrees are specified.
3.3.15 - Replace PNG icons with SVG icons and resize other PNG icons.
Expand Down
Loading

0 comments on commit 1af4512

Please sign in to comment.