235 lines
7.6 KiB
Python

"""
/***************************************************************************
Climb
begin : 2019-05-15
copyright : (C) 2019 by Håvard Tveite
email : havard.tveite@nmbu.no
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
"""
__author__ = "Håvard Tveite"
__date__ = "2019-03-01"
__copyright__ = "(C) 2019 by Håvard Tveite"
import os
import math
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtCore import QMetaType
from qgis.core import (
QgsProcessing,
QgsFeatureSink,
QgsProcessingAlgorithm,
QgsProcessingParameterFeatureSource,
QgsProcessingParameterFeatureSink,
QgsProcessingOutputNumber,
QgsProcessingException,
QgsProcessingUtils,
QgsWkbTypes,
QgsFields,
QgsField,
)
from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
class Climb(QgisAlgorithm):
INPUT = "INPUT"
OUTPUT = "OUTPUT"
TOTALCLIMB = "TOTALCLIMB"
TOTALDESCENT = "TOTALDESCENT"
MINELEVATION = "MINELEVATION"
MAXELEVATION = "MAXELEVATION"
CLIMBATTRIBUTE = "climb"
DESCENTATTRIBUTE = "descent"
MINELEVATTRIBUTE = "minelev"
MAXELEVATTRIBUTE = "maxelev"
def name(self):
return "climbalongline"
def displayName(self):
return self.tr("Climb along line")
def group(self):
return self.tr("Vector analysis")
def groupId(self):
return "vectoranalysis"
def __init__(self):
super().__init__()
def initAlgorithm(self, config=None):
self.addParameter(
QgsProcessingParameterFeatureSource(
self.INPUT,
self.tr("Line layer"),
[QgsProcessing.SourceType.TypeVectorLine],
)
)
self.addParameter(
QgsProcessingParameterFeatureSink(self.OUTPUT, self.tr("Climb layer"))
)
self.addOutput(
QgsProcessingOutputNumber(self.TOTALCLIMB, self.tr("Total climb"))
)
self.addOutput(
QgsProcessingOutputNumber(self.TOTALDESCENT, self.tr("Total descent"))
)
self.addOutput(
QgsProcessingOutputNumber(self.MINELEVATION, self.tr("Minimum elevation"))
)
self.addOutput(
QgsProcessingOutputNumber(self.MAXELEVATION, self.tr("Maximum elevation"))
)
def processAlgorithm(self, parameters, context, feedback):
source = self.parameterAsSource(parameters, self.INPUT, context)
fcount = source.featureCount()
source_fields = source.fields()
hasZ = QgsWkbTypes.hasZ(source.wkbType())
if not hasZ:
raise QgsProcessingException(
self.tr(
"The layer does not have Z values. If you have a DEM, use the Drape algorithm to extract Z values."
)
)
thefields = QgsFields()
climbindex = -1
descentindex = -1
minelevindex = -1
maxelevindex = -1
fieldnumber = 0
# Create new fields for climb and descent
thefields.append(QgsField(self.CLIMBATTRIBUTE, QMetaType.Type.Double))
thefields.append(QgsField(self.DESCENTATTRIBUTE, QMetaType.Type.Double))
thefields.append(QgsField(self.MINELEVATTRIBUTE, QMetaType.Type.Double))
thefields.append(QgsField(self.MAXELEVATTRIBUTE, QMetaType.Type.Double))
# combine all the vector fields
out_fields = QgsProcessingUtils.combineFields(thefields, source_fields)
layerwithz = source
(sink, dest_id) = self.parameterAsSink(
parameters,
self.OUTPUT,
context,
out_fields,
layerwithz.wkbType(),
source.sourceCrs(),
)
# get features from source (with z values)
features = layerwithz.getFeatures()
totalclimb = 0
totaldescent = 0
minelevation = float("Infinity")
maxelevation = float("-Infinity")
no_z_nodes = []
no_geometry = []
for current, feature in enumerate(features):
if feedback.isCanceled():
break
climb = 0
descent = 0
minelev = float("Infinity")
maxelev = float("-Infinity")
# In case of multigeometries we need to do the parts
parts = feature.geometry().constParts()
if not feature.hasGeometry():
no_geometry.append(self.tr("Feature: {0}").format(feature.id()))
for partnumber, part in enumerate(parts):
# Calculate the climb
first = True
zval = 0
for idx, v in enumerate(part.vertices()):
zval = v.z()
if math.isnan(zval):
no_z_nodes.append(
self.tr(
"Feature: {feature_id}, part: {part_id}, point: {point_id}"
).format(
feature_id=feature.id(),
part_id=partnumber,
point_id=idx,
)
)
continue
if first:
prevz = zval
minelev = zval
maxelev = zval
first = False
else:
diff = zval - prevz
if diff > 0:
climb += diff
else:
descent -= diff
minelev = min(minelev, zval)
maxelev = max(maxelev, zval)
prevz = zval
totalclimb += climb
totaldescent += descent
# Set the attribute values
# Append the attributes to the end of the existing ones
attrs = [climb, descent, minelev, maxelev, *feature.attributes()]
# Set the final attribute list
feature.setAttributes(attrs)
# Add a feature to the sink
sink.addFeature(feature, QgsFeatureSink.Flag.FastInsert)
minelevation = min(minelevation, minelev)
maxelevation = max(maxelevation, maxelev)
# Update the progress bar
if fcount > 0:
feedback.setProgress(int(100 * current / fcount))
feedback.pushInfo(
self.tr(
"The following features do not have geometry: {no_geometry_report}"
).format(no_geometry_report=(", ".join(no_geometry)))
)
feedback.pushInfo(
self.tr("The following points do not have Z values: {no_z_report}").format(
no_z_report=(", ".join(no_z_nodes))
)
)
sink.finalize()
# Return the results
return {
self.OUTPUT: dest_id,
self.TOTALCLIMB: totalclimb,
self.TOTALDESCENT: totaldescent,
self.MINELEVATION: minelevation,
self.MAXELEVATION: maxelevation,
}