-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
373 additions
and
0 deletions.
There are no files selected for viewing
9 changes: 9 additions & 0 deletions
9
los_tools/processing/doc/tool_extract_horizon_lines_by_distances.help
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"ALG_DESC": "This tool extracts horizon lines from LoS without a target. For other types of LoS, this operation is not applicable. Maximal horizon till specified distance is created. So horizon lines for several distances can be created at once.", | ||
"ALG_CREATOR": "Jan Caha", | ||
"LoSLayer": "LoS layer to analyze.", | ||
"Distances": "Table of distance limits for which horizon lines should be extracted.", | ||
"CurvatureCorrections": "Should curvature and refraction corrections be applied?", | ||
"RefractionCoefficient": "Value of the refraction coefficient. Default value: 0.13.", | ||
"OutputLayer": "Output layer containing the extracted horizon lines." | ||
} |
238 changes: 238 additions & 0 deletions
238
los_tools/processing/horizons/tool_extract_horizon_lines_by_distances.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,238 @@ | ||
import typing | ||
|
||
from qgis.core import ( | ||
QgsFeature, | ||
QgsFeatureRequest, | ||
QgsField, | ||
QgsFields, | ||
QgsLineString, | ||
QgsPoint, | ||
QgsProcessing, | ||
QgsProcessingAlgorithm, | ||
QgsProcessingException, | ||
QgsProcessingFeedback, | ||
QgsProcessingParameterBoolean, | ||
QgsProcessingParameterFeatureSink, | ||
QgsProcessingParameterFeatureSource, | ||
QgsProcessingParameterMatrix, | ||
QgsProcessingParameterNumber, | ||
QgsProcessingUtils, | ||
QgsVectorLayer, | ||
QgsWkbTypes, | ||
) | ||
|
||
from los_tools.classes.classes_los import LoSWithoutTarget | ||
from los_tools.constants.field_names import FieldNames | ||
from los_tools.constants.names_constants import NamesConstants | ||
from los_tools.processing.tools.util_functions import get_los_type | ||
from los_tools.utils import COLUMN_TYPE, COLUMN_TYPE_STRING, get_doc_file | ||
|
||
|
||
class ExtractHorizonLinesByDistanceAlgorithm(QgsProcessingAlgorithm): | ||
LOS_LAYER = "LoSLayer" | ||
OUTPUT_LAYER = "OutputLayer" | ||
CURVATURE_CORRECTIONS = "CurvatureCorrections" | ||
REFRACTION_COEFFICIENT = "RefractionCoefficient" | ||
DISTANCES = "Distances" | ||
|
||
horizons_types = [NamesConstants.HORIZON_MAX_LOCAL, NamesConstants.HORIZON_GLOBAL] | ||
|
||
def initAlgorithm(self, config=None): | ||
self.addParameter( | ||
QgsProcessingParameterFeatureSource(self.LOS_LAYER, "LoS layer", [QgsProcessing.TypeVectorLine]) | ||
) | ||
|
||
self.addParameter( | ||
QgsProcessingParameterMatrix( | ||
self.DISTANCES, | ||
"Distance limits for horizon lines", | ||
numberRows=1, | ||
headers=["Distance"], | ||
) | ||
) | ||
|
||
self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT_LAYER, "Output layer")) | ||
|
||
self.addParameter( | ||
QgsProcessingParameterBoolean( | ||
self.CURVATURE_CORRECTIONS, | ||
"Use curvature corrections?", | ||
defaultValue=True, | ||
) | ||
) | ||
|
||
self.addParameter( | ||
QgsProcessingParameterNumber( | ||
self.REFRACTION_COEFFICIENT, | ||
"Refraction coefficient value", | ||
type=QgsProcessingParameterNumber.Double, | ||
defaultValue=0.13, | ||
) | ||
) | ||
|
||
def checkParameterValues(self, parameters, context): | ||
los_layer = self.parameterAsVectorLayer(parameters, self.LOS_LAYER, context) | ||
|
||
field_names = los_layer.fields().names() | ||
|
||
if FieldNames.LOS_TYPE not in field_names: | ||
msg = ( | ||
"Fields specific for LoS not found in current layer ({0}). " | ||
"Cannot extract horizon lines from this layer.".format(FieldNames.LOS_TYPE) | ||
) | ||
|
||
return False, msg | ||
|
||
los_type = get_los_type(los_layer, field_names) | ||
|
||
if los_type != NamesConstants.LOS_NO_TARGET: | ||
msg = "LoS must be of type `{0}` to extract horizon lines but type `{1}` found.".format( | ||
NamesConstants.LOS_NO_TARGET, los_type | ||
) | ||
|
||
return False, msg | ||
|
||
distances = self.parameterAsMatrix(parameters, self.DISTANCES, context) | ||
|
||
if len(distances) < 1: | ||
msg = f"Length of distances must be at least 1. It is {len(distances)}." | ||
|
||
return False, msg | ||
|
||
return super().checkParameterValues(parameters, context) | ||
|
||
def processAlgorithm(self, parameters, context, feedback: QgsProcessingFeedback): | ||
los_layer: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.LOS_LAYER, context) | ||
|
||
if los_layer is None: | ||
raise QgsProcessingException(self.invalidSourceError(parameters, self.LOS_LAYER)) | ||
|
||
distances_matrix = self.parameterAsMatrix(parameters, self.DISTANCES, context) | ||
|
||
distances: typing.List[float] = [] | ||
|
||
for distance in distances_matrix: | ||
try: | ||
distances.append(float(distance)) | ||
except ValueError: | ||
raise QgsProcessingException(f"Cannot convert value `{distance}` to float number.") | ||
|
||
curvature_corrections = self.parameterAsBool(parameters, self.CURVATURE_CORRECTIONS, context) | ||
ref_coeff = self.parameterAsDouble(parameters, self.REFRACTION_COEFFICIENT, context) | ||
|
||
fields = QgsFields() | ||
fields.append(QgsField(FieldNames.HORIZON_DISTANCE, COLUMN_TYPE.Double)) | ||
fields.append(QgsField(FieldNames.ID_OBSERVER, COLUMN_TYPE.Int)) | ||
fields.append(QgsField(FieldNames.OBSERVER_X, COLUMN_TYPE.Double)) | ||
fields.append(QgsField(FieldNames.OBSERVER_Y, COLUMN_TYPE.Double)) | ||
|
||
horizon_distance_field_index = fields.indexFromName(FieldNames.HORIZON_DISTANCE) | ||
id_observer_field_index = fields.indexFromName(FieldNames.ID_OBSERVER) | ||
observer_x_field_index = fields.indexFromName(FieldNames.OBSERVER_X) | ||
observer_y_field_index = fields.indexFromName(FieldNames.OBSERVER_Y) | ||
|
||
sink, dest_id = self.parameterAsSink( | ||
parameters, | ||
self.OUTPUT_LAYER, | ||
context, | ||
fields, | ||
QgsWkbTypes.LineStringZM, | ||
los_layer.sourceCrs(), | ||
) | ||
|
||
if sink is None: | ||
raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT_LAYER)) | ||
|
||
id_values = list(los_layer.uniqueValues(los_layer.fields().indexFromName(FieldNames.ID_OBSERVER))) | ||
|
||
total = 100 / (los_layer.featureCount()) if los_layer.featureCount() else 0 | ||
|
||
i = 0 | ||
|
||
for id_value in id_values: | ||
request = QgsFeatureRequest() | ||
request.setFilterExpression("{} = '{}'".format(FieldNames.ID_OBSERVER, id_value)) | ||
order_by_clause = QgsFeatureRequest.OrderByClause(FieldNames.AZIMUTH, ascending=True) | ||
request.setOrderBy(QgsFeatureRequest.OrderBy([order_by_clause])) | ||
|
||
features = los_layer.getFeatures(request) | ||
|
||
line_points: typing.Dict[float, typing.List[QgsPoint]] = {} | ||
values: typing.Dict[float, typing.List[float]] = {} | ||
|
||
for distance in distances: | ||
line_points[distance] = [] | ||
values[distance] = [] | ||
|
||
for los_feature in features: | ||
if feedback.isCanceled(): | ||
break | ||
|
||
full_los = LoSWithoutTarget( | ||
los_feature.geometry(), | ||
observer_offset=los_feature.attribute(FieldNames.OBSERVER_OFFSET), | ||
use_curvature_corrections=curvature_corrections, | ||
refraction_coefficient=ref_coeff, | ||
) | ||
|
||
for distance in distances: | ||
los_limited = LoSWithoutTarget.from_another(full_los, distance_limit=distance) | ||
|
||
line_points[distance].append(los_limited.get_global_horizon()) | ||
values[distance].append(los_limited.get_global_horizon_angle()) | ||
|
||
i += 1 | ||
|
||
feedback.setProgress(int(i * total)) | ||
|
||
for distance in distances: | ||
|
||
points = line_points[distance] | ||
m_values = values[distance] | ||
|
||
if 1 < len(points): | ||
line = QgsLineString(points) | ||
line.addMValue() | ||
|
||
for i in range(0, line.numPoints()): | ||
line.setMAt(i, m_values[i]) | ||
|
||
f = QgsFeature(fields) | ||
f.setGeometry(line) | ||
f.setAttribute(id_observer_field_index, id_value) | ||
f.setAttribute( | ||
observer_x_field_index, | ||
los_feature.attribute(FieldNames.OBSERVER_X), | ||
) | ||
f.setAttribute( | ||
observer_y_field_index, | ||
los_feature.attribute(FieldNames.OBSERVER_Y), | ||
) | ||
f.setAttribute(horizon_distance_field_index, distance) | ||
|
||
sink.addFeature(f) | ||
|
||
return {self.OUTPUT_LAYER: dest_id} | ||
|
||
def name(self): | ||
return "extracthorizonlinesbydistace" | ||
|
||
def displayName(self): | ||
return "Extract Horizon Lines By Distance" | ||
|
||
def group(self): | ||
return "Horizons" | ||
|
||
def groupId(self): | ||
return "horizons" | ||
|
||
def createInstance(self): | ||
return ExtractHorizonLinesByDistanceAlgorithm() | ||
|
||
def helpUrl(self): | ||
# TODO FIXME | ||
return "https://jancaha.github.io/qgis_los_tools/tools/Horizons/tool_extract_horizon_lines/" | ||
|
||
# def shortHelpString(self): | ||
# # TODO FIXME | ||
# return QgsProcessingUtils.formatHelpMapAsHtml(get_doc_file(__file__), self) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
92 changes: 92 additions & 0 deletions
92
tests/processing/horizons/tool_extract_horizon_lines_by_distances.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import typing | ||
|
||
import pytest | ||
from qgis.core import Qgis, QgsVectorLayer | ||
|
||
from los_tools.constants.field_names import FieldNames | ||
from los_tools.processing.horizons.tool_extract_horizon_lines_by_distances import ExtractHorizonLinesByDistanceAlgorithm | ||
from tests.custom_assertions import ( | ||
assert_algorithm, | ||
assert_check_parameter_values, | ||
assert_field_names_exist, | ||
assert_layer, | ||
assert_parameter, | ||
assert_run, | ||
) | ||
from tests.utils import result_filename | ||
|
||
|
||
def test_parameters() -> None: | ||
alg = ExtractHorizonLinesByDistanceAlgorithm() | ||
alg.initAlgorithm() | ||
|
||
assert_parameter(alg.parameterDefinition("LoSLayer"), parameter_type="source") | ||
assert_parameter(alg.parameterDefinition("OutputLayer"), parameter_type="sink") | ||
assert_parameter(alg.parameterDefinition("CurvatureCorrections"), parameter_type="boolean", default_value=True) | ||
assert_parameter(alg.parameterDefinition("RefractionCoefficient"), parameter_type="number", default_value=0.13) | ||
assert_parameter(alg.parameterDefinition("Distances"), parameter_type="matrix") | ||
|
||
|
||
def test_alg_settings() -> None: | ||
alg = ExtractHorizonLinesByDistanceAlgorithm() | ||
alg.initAlgorithm() | ||
|
||
assert_algorithm(alg) | ||
|
||
|
||
def test_wrong_params( | ||
los_no_target_wrong: QgsVectorLayer, los_local: QgsVectorLayer, los_global: QgsVectorLayer | ||
) -> None: | ||
alg = ExtractHorizonLinesByDistanceAlgorithm() | ||
alg.initAlgorithm() | ||
|
||
# use layer that is not corrently constructed LoS layer | ||
params = {"LoSLayer": los_no_target_wrong} | ||
|
||
with pytest.raises(AssertionError, match="Fields specific for LoS not found in current layer"): | ||
assert_check_parameter_values(alg, params) | ||
|
||
params = {"LoSLayer": los_local, "HorizonType": 0} | ||
|
||
with pytest.raises( | ||
AssertionError, match="LoS must be of type `without target` to extract horizon lines but type `local` found" | ||
): | ||
assert_check_parameter_values(alg, params) | ||
|
||
params = {"LoSLayer": los_global, "HorizonType": 0} | ||
|
||
with pytest.raises( | ||
AssertionError, match="LoS must be of type `without target` to extract horizon lines but type `global` found" | ||
): | ||
assert_check_parameter_values(alg, params) | ||
|
||
|
||
def test_run_alg(los_no_target: QgsVectorLayer) -> None: | ||
alg = ExtractHorizonLinesByDistanceAlgorithm() | ||
alg.initAlgorithm() | ||
|
||
output_path = result_filename("horizon_lines.gpkg") | ||
|
||
distances = [10, 30, 50, 100, 200] | ||
params = { | ||
"LoSLayer": los_no_target, | ||
"Distances": distances, | ||
"OutputLayer": output_path, | ||
"CurvatureCorrections": True, | ||
"RefractionCoefficient": 0.13, | ||
} | ||
|
||
assert_run(alg, params) | ||
|
||
horizon_lines_layer = QgsVectorLayer(output_path) | ||
|
||
assert_layer(horizon_lines_layer, geom_type=Qgis.WkbType.LineStringZM, crs=los_no_target.sourceCrs()) | ||
|
||
assert_field_names_exist( | ||
[FieldNames.HORIZON_DISTANCE, FieldNames.ID_OBSERVER, FieldNames.OBSERVER_X, FieldNames.OBSERVER_Y], | ||
horizon_lines_layer, | ||
) | ||
|
||
unique_ids = list(los_no_target.uniqueValues(los_no_target.fields().lookupField(FieldNames.ID_OBSERVER))) | ||
|
||
assert horizon_lines_layer.featureCount() == len(unique_ids) * len(distances) |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions
32
website/pages/tools/Horizons/tool_extract_horizon_lines_by_distances.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# Extract Horizon Lines | ||
|
||
Tool that extracts horizon lines from LoS without target (created using tool [Create no target LoS](../LoS Creation/tool_create_notarget_los.md)). For other types of LoS this operation does not make sense. | ||
|
||
The horizon lines are extracted for a set of distances from the observer. The distances are provided in the table. The horizon line is extracted for each observer and distance separately, only horizons that are closer to the observer than the given distance are consider for each horizon line. | ||
|
||
## Parameters | ||
|
||
| Label | Name | Type | Description | | ||
| --------------------------------- | ----------------------- | ----------------------------------------- | --------------------------------------------------------------------- | | ||
| LoS layer | `LoSLayer` | [vector: line] | LoS layer to analyse. | | ||
| Distance limits for horizon lines | `Distances` | [matrix] | Table of distance limits for which horizon lines should be extracted. | | ||
| Output layer | `OutputLayer` | [vector: line] | Output layer horizon lines. | | ||
| Use curvature corrections? | `CurvatureCorrections` | [boolean]<br/><br/>Default: `True` | Should the curvarture and refraction corrections be used? | | ||
| Refraction coefficient value | `RefractionCoefficient` | [number] <br/><br/> Default: <br/> `0.13` | Value of refraction coefficient. | | ||
|
||
## Outputs | ||
|
||
| Label | Name | Type | Description | | ||
| ------------ | ------------- | -------------- | --------------------------- | | ||
| Output layer | `OutputLayer` | [vector: line] | Output layer horizon lines. | | ||
|
||
### Fields in the output layer | ||
|
||
* __horizon_distance__ - double - maximal distance of the horizon line from observer | ||
* __id_observer__ - integer - value from expected field (`id_observer`) in `LoSLayer` | ||
* __observer_x__ - double - X coordinate of observer point, to be used later in analyses | ||
* __observer_y__ - double - Y coordinate of observer point, to be used later in analyses | ||
|
||
## Tool screenshot | ||
|
||
 |