Skip to content

Commit 797a9fd

Browse files
authored
Merge pull request #31 from JanCaha/version_2
complete plugin refactor
2 parents d222a09 + 4ddb18e commit 797a9fd

File tree

110 files changed

+3990
-2021
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

110 files changed

+3990
-2021
lines changed

.github/workflows/codestyle.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
- name: Set up Python
2020
uses: actions/setup-python@v5
2121
with:
22-
python-version: '3.10'
22+
python-version: '3.12'
2323
cache: 'pip'
2424

2525
- name: Install Python packages

.github/workflows/test_plugin.yaml

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
name: Tests for Plugin LoS Tools
22

3+
concurrency:
4+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
5+
cancel-in-progress: true
6+
37
on:
48
push:
59
paths:
@@ -13,8 +17,18 @@ jobs:
1317

1418
runs-on: ubuntu-24.04
1519

20+
strategy:
21+
matrix:
22+
qgis_source: [ubuntu, ubuntugis-nightly]
23+
1624
steps:
1725

26+
- id: skip_check
27+
uses: fkirc/skip-duplicate-actions@v5
28+
with:
29+
concurrent_skipping: 'same_content_newer'
30+
skip_after_successful_duplicate: 'true'
31+
1832
- name: Install
1933
run: |
2034
sudo apt-get install python3-pytest python3-pytest-cov python3-pytestqt
@@ -26,12 +40,11 @@ jobs:
2640
wget -O $KEYRING https://download.qgis.org/downloads/qgis-archive-keyring.gpg && \
2741
sudo touch /etc/apt/sources.list.d/qgis.sources && \
2842
echo 'Types: deb deb-src' | sudo tee -a /etc/apt/sources.list.d/qgis.sources && \
29-
echo 'URIs: https://qgis.org/ubuntugis' | sudo tee -a /etc/apt/sources.list.d/qgis.sources && \
43+
echo 'URIs: https://qgis.org/${{ matrix.qgis_source }}' | sudo tee -a /etc/apt/sources.list.d/qgis.sources && \
3044
echo 'Suites: '$(lsb_release -c -s) | sudo tee -a /etc/apt/sources.list.d/qgis.sources && \
3145
echo 'Architectures: '$(dpkg --print-architecture) | sudo tee -a /etc/apt/sources.list.d/qgis.sources && \
3246
echo 'Components: main' | sudo tee -a /etc/apt/sources.list.d/qgis.sources && \
3347
echo 'Signed-By: '$KEYRING | sudo tee -a /etc/apt/sources.list.d/qgis.sources && \
34-
LASTSUPPORTED=focal && \
3548
KEYRING=/usr/share/keyrings/ubuntugis-archive-keyring.gpg && \
3649
sudo gpg --no-default-keyring --keyring $KEYRING --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B827C12C2D425E227EDCA75089EBE08314DF160 && \
3750
sudo touch /etc/apt/sources.list.d/ubuntugis-unstable.sources && \
@@ -46,8 +59,8 @@ jobs:
4659
run: |
4760
sudo apt-get update && \
4861
sudo apt-get -y -q install --no-install-recommends wget software-properties-common build-essential ca-certificates python3-pip dialog apt-utils && \
49-
sudo apt -y -q install qgis qgis-dev qgis-plugin-grass
50-
62+
sudo apt -y -q install qgis qgis-dev
63+
5164
- name: QGIS Version
5265
run: qgis --version
5366

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ __pycache__
99
/tests/_data/*.gpkg-shm
1010
/tests/_data/*.aux.xml
1111
/website/docs
12+
/build

.vscode/settings.json

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,6 @@
1212
"/usr/share/qgis/python"
1313
],
1414
"svg.preview.background": "dark-transparent",
15-
"python.testing.pytestArgs": [
16-
"tests",
17-
"-rP",
18-
"-s",
19-
"--no-cov"
20-
],
2115
"python.testing.pytestEnabled": true,
2216
"[python]": {
2317
"editor.codeActionsOnSave": {
@@ -26,22 +20,8 @@
2620
"editor.defaultFormatter": "ms-python.black-formatter",
2721
"editor.formatOnSave": true,
2822
},
29-
"pylint.args": [
30-
"--disable= redefined-outer-name,no-name-in-module,missing-function-docstring,missing-class-docstring,missing-module-docstring,invalid-name,too-many-arguments,attribute-defined-outside-init",
31-
"--max-line-length=120"
32-
],
33-
"flake8.args": [
34-
"--max-line-length",
35-
"120"
36-
],
37-
"isort.args": [
38-
"--profile",
39-
"black",
40-
"--line-width",
41-
"120"
42-
],
43-
"black-formatter.args": [
44-
"--line-length",
45-
"120"
46-
],
23+
"python.analysis.diagnosticSeverityOverrides": {
24+
"reportAttributeAccessIssue": "none",
25+
"reportOptionalMemberAccess": "none"
26+
}
4727
}

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ QGIS plugin with focus on other aspects of visibility and its analyses is [Visib
1919

2020
The citation for the plugin should be:
2121

22-
```
22+
```text
2323
Jan Caha (2023). LoS Tools. QGIS Plugin version 1.0. https://jancaha.github.io/qgis_los_tools/
2424
```
2525

26-
```
26+
```tex
2727
@Manual{,
2828
title = {LoS Tools}. QGIS Plugin version 1.0,
2929
author = {Jan Caha},

los_tools/Makefile

Lines changed: 0 additions & 18 deletions
This file was deleted.

los_tools/classes/classes_los.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,15 @@ def __identify_horizons(self) -> None:
6464
self.horizon.append((self.visible[i] is True) and (self.visible[i + 1] is False))
6565

6666
def __parse_points(self, points: List[List[float]]) -> None:
67-
max_angle_temp = -180
67+
max_angle_temp = -180.0
6868

6969
first_point_x = points[0][0]
7070
first_point_y = points[0][1]
7171
first_point_z = points[0][2] + self.observer_offset
7272

73-
sampling_distance: float = None
73+
target_distance = 0.0
74+
sampling_distance = 1.0
75+
target_offset = 0.0
7476

7577
if self.is_global:
7678
target_distance = calculate_distance(first_point_x, first_point_y, self.target_x, self.target_y)
@@ -87,9 +89,11 @@ def __parse_points(self, points: List[List[float]]) -> None:
8789
point_z = self._curvature_corrections(point_z, distance, self.refraction_coefficient)
8890
target_offset = self._curvature_corrections(self.target_offset, distance, self.refraction_coefficient)
8991

92+
# first point
9093
if i == 0:
9194
self.points[i] = [point_x, point_y, 0, first_point_z, -90]
9295

96+
# target point global los
9397
elif self.is_global and math.fabs(target_distance - distance) < sampling_distance / 2:
9498
self.points[i] = [
9599
point_x,
@@ -101,6 +105,7 @@ def __parse_points(self, points: List[List[float]]) -> None:
101105

102106
self.target_index = i
103107

108+
# target point local los
104109
elif not self.is_global and not self.is_without_target and i == len(points) - 1:
105110
self.points[i] = [
106111
point_x,
@@ -110,6 +115,7 @@ def __parse_points(self, points: List[List[float]]) -> None:
110115
self._angle_vertical(distance, point_z + target_offset - first_point_z),
111116
]
112117

118+
# points
113119
else:
114120
self.points[i] = [
115121
point_x,
@@ -123,19 +129,17 @@ def __parse_points(self, points: List[List[float]]) -> None:
123129
self.previous_max_angle.append(max_angle_temp)
124130

125131
if i != 0:
126-
if max_angle_temp < self.points[i - 1][self.VERTICAL_ANGLE]:
127-
if self.is_global:
128-
if i != self.target_index:
129-
max_angle_temp = self.points[i - 1][self.VERTICAL_ANGLE]
132+
if max_angle_temp < self._angle_vertical(distance, point_z - first_point_z):
133+
if self.is_global and i == self.target_index:
134+
pass
130135
else:
131-
max_angle_temp = self.points[i - 1][self.VERTICAL_ANGLE]
136+
max_angle_temp = self._angle_vertical(distance, point_z - first_point_z)
132137

133138
# is visible is only valid if previous_max_angle is smaller then current angle
134139
if i == 0:
135140
self.visible.append(True)
136141
else:
137-
# [i] and [-1] actually points to the same point, no idea why I wrote this way
138-
self.visible.append(self.previous_max_angle[i] < self.points[i - 1][self.VERTICAL_ANGLE])
142+
self.visible.append(self.previous_max_angle[i] < self.points[i][self.VERTICAL_ANGLE])
139143

140144
def __str__(self):
141145
string = ""

los_tools/classes/list_raster.py

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import math
2-
from typing import List, Optional, Tuple
2+
import pathlib
3+
import typing
4+
from typing import Dict, List, Optional, Tuple
35

46
from qgis.core import (
57
QgsCoordinateReferenceSystem,
@@ -15,13 +17,18 @@
1517
QgsRectangle,
1618
qgsFloatNear,
1719
)
20+
from qgis.PyQt.QtCore import QFile, QIODevice
21+
from qgis.PyQt.QtXml import QDomDocument
1822

23+
from los_tools.constants.plugin import PluginConstants
1924
from los_tools.processing.tools.util_functions import bilinear_interpolated_value
2025

2126

2227
class ListOfRasters:
28+
2329
def __init__(self, rasters: List[QgsMapLayer]):
24-
self.rasters: List[QgsRasterLayer] = []
30+
31+
self._dict_rasters: Dict[str, QgsRasterLayer] = {}
2532

2633
if rasters:
2734
first_crs = rasters[0].crs()
@@ -32,10 +39,31 @@ def __init__(self, rasters: List[QgsMapLayer]):
3239
if not first_crs == raster.crs():
3340
raise ValueError("All CRS must be equal.")
3441

35-
self.rasters.append(raster)
42+
self._dict_rasters[raster.id()] = raster
3643

3744
self.order_by_pixel_size()
3845

46+
def __len__(self):
47+
return len(self._dict_rasters)
48+
49+
def __repr__(self):
50+
return f"ListOfRasters: [{", ".join([x.name() for x in self.rasters])}]"
51+
52+
def raster_to_use(self) -> str:
53+
return ", ".join([x.name() for x in self.rasters])
54+
55+
@property
56+
def rasters(self) -> List[QgsRasterLayer]:
57+
return list(self._dict_rasters.values())
58+
59+
@property
60+
def raster_ids(self) -> List[str]:
61+
return list(self._dict_rasters.keys())
62+
63+
def remove_raster(self, raster_id: str) -> None:
64+
if raster_id in self._dict_rasters:
65+
self._dict_rasters.pop(raster_id)
66+
3967
@staticmethod
4068
def validate_bands(rasters: List[QgsMapLayer]) -> Tuple[bool, str]:
4169
for raster in rasters:
@@ -139,11 +167,14 @@ def order_by_pixel_size(self) -> None:
139167
tuples = []
140168

141169
for raster in self.rasters:
142-
tuples.append((raster, raster.extent().width() / raster.width()))
170+
tuples.append((raster.id(), raster.extent().width() / raster.width(), raster))
143171

144172
sorted_by_cell_size = sorted(tuples, key=lambda tup: tup[1])
145173

146-
self.rasters = [x[0] for x in sorted_by_cell_size]
174+
self._dict_rasters = {}
175+
176+
for x in sorted_by_cell_size:
177+
self._dict_rasters[x[0]] = x[2]
147178

148179
@property
149180
def rasters_dp(self) -> List[QgsRasterDataProvider]:
@@ -201,3 +232,99 @@ def add_z_values(self, points: List[QgsPoint]) -> QgsLineString:
201232
points3d.append(QgsPoint(point.x(), point.y(), z))
202233

203234
return QgsLineString(points3d)
235+
236+
def save_to_file(self, file_path: str) -> typing.Tuple[bool, str]:
237+
"""Saves configuration to XML file. Result is a tuple with success status and message."""
238+
239+
path = pathlib.Path(file_path)
240+
if path.suffix.lower() != PluginConstants.rasters_xml_extension:
241+
file_path = path.with_suffix(PluginConstants.rasters_xml_extension).as_posix()
242+
243+
doc = QDomDocument()
244+
245+
root = doc.createElement("ListOfRasters")
246+
247+
doc.appendChild(root)
248+
249+
for raster in self.rasters:
250+
try:
251+
relative_path = pathlib.Path(raster.source()).relative_to(pathlib.Path(file_path).parent)
252+
path_type = "relative"
253+
except ValueError:
254+
path_type = "absolute"
255+
relative_path = raster.source()
256+
257+
raster_element = doc.createElement("raster")
258+
raster_element.setAttribute("dataProvider", raster.dataProvider().name())
259+
raster_element.setAttribute("name", raster.name())
260+
raster_element.setAttribute("path", relative_path)
261+
raster_element.setAttribute("pathType", path_type)
262+
raster_element.setAttribute("crs", raster.crs().authid())
263+
raster_element.setAttribute("cellsWidth", raster.width())
264+
raster_element.setAttribute("cellsHeight", raster.height())
265+
raster_element.setAttribute("extentWidth", raster.extent().width())
266+
raster_element.setAttribute("extentHeight", raster.extent().height())
267+
268+
root.appendChild(raster_element)
269+
270+
file = QFile(file_path)
271+
if file.open(QIODevice.OpenModeFlag.WriteOnly | QIODevice.OpenModeFlag.Text):
272+
bytes_written = file.write(doc.toByteArray())
273+
if bytes_written == -1:
274+
file.close()
275+
return False, f"Could not write to file `{file_path}`."
276+
file.close()
277+
278+
return True, f"Configuration saved to `{file_path}`."
279+
280+
def read_from_file(self, file_path: str) -> typing.Tuple[bool, str]:
281+
282+
self._dict_rasters = {}
283+
284+
file = QFile(file_path)
285+
if not file.open(QIODevice.OpenModeFlag.ReadOnly | QIODevice.OpenModeFlag.Text):
286+
return False, f"Could not open file `{file_path}`."
287+
288+
doc = QDomDocument()
289+
if not doc.setContent(file):
290+
file.close()
291+
return False, f"Could not read content of file `{file_path}`."
292+
file.close()
293+
294+
root = doc.documentElement()
295+
if root.tagName() != "ListOfRasters":
296+
return False, f"File `{file_path}` is not a valid {PluginConstants.rasters_xml_name} file."
297+
298+
items = root.elementsByTagName("raster")
299+
300+
load_messages = []
301+
for i in range(items.length()):
302+
item = items.item(i).toElement()
303+
304+
raster_path = item.attribute("path")
305+
if item.attribute("pathType") == "relative":
306+
raster_path = pathlib.Path(file_path).parent / raster_path
307+
else:
308+
raster_path = pathlib.Path(raster_path)
309+
310+
if not pathlib.Path(raster_path).exists():
311+
continue
312+
313+
raster = QgsRasterLayer(raster_path.as_posix(), item.attribute("name"), item.attribute("dataProvider"))
314+
if not raster.isValid():
315+
continue
316+
317+
if not (
318+
raster.crs().authid() == item.attribute("crs")
319+
and raster.width() == int(item.attribute("cellsWidth"))
320+
and raster.height() == int(item.attribute("cellsHeight"))
321+
and raster.extent().width() == float(item.attribute("extentWidth"))
322+
and raster.extent().height() == float(item.attribute("extentHeight"))
323+
):
324+
load_messages.append(f"Raster `{raster.name()}` does not fit with definition in the file.")
325+
326+
self._dict_rasters[raster.id()] = raster
327+
328+
self.order_by_pixel_size()
329+
330+
return True, ",".join(load_messages)

0 commit comments

Comments
 (0)