-
Notifications
You must be signed in to change notification settings - Fork 2
/
forestRoadNetwork_algorithm.py
1003 lines (844 loc) · 47.3 KB
/
forestRoadNetwork_algorithm.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
"""
/***************************************************************************
ForestRoads
A QGIS plugin
Create a network of forest roads based on zones to access, roads to connect
them to, and a cost matrix.
The code of the plugin is based on the "LeastCostPath" plugin available on
https://github.com/Gooong/LeastCostPath. We thank their team for the template.
Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
-------------------
begin : 10-07-2019
copyright : (C) 2019 by Clement Hardy
email : clem.hardy@outlook.fr
***************************************************************************/
/***************************************************************************
* *
* 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. *
* *
***************************************************************************/
This script describes the algorithm used to make the forest road network.
"""
__author__ = 'clem.hardy@outlook.fr'
__date__ = 'Currently in work'
__copyright__ = '(C) 2019 by Clement Hardy'
# We load every function necessary from the QIS packages.
import random
from .kdtree import KDTree
import numpy as np
from PyQt5.QtCore import QCoreApplication, QVariant
from PyQt5.QtGui import QIcon
from qgis.core import (
QgsFeature,
QgsGeometry,
QgsPoint,
QgsPointXY,
QgsField,
QgsFields,
QgsWkbTypes,
QgsProcessing,
QgsFeatureSink,
QgsProcessingException,
QgsProcessingAlgorithm,
QgsProcessingParameterFeatureSource,
QgsProcessingParameterFeatureSink,
QgsProcessingParameterRasterLayer,
QgsProcessingParameterField,
QgsProcessingParameterBand,
QgsProcessingParameterBoolean,
QgsProcessingParameterNumber,
QgsProcessingParameterEnum
)
# We import the algorithm used for processing a road.
from .dijkstra_algorithm import dijkstra
# We import mathematical functions needed for the algorithm.
from math import floor, sqrt
# The algorithm class heritates from the algorithm class of QGIS.
# There, it can register different parameter during initialization
# that can be put into variables using "
class ForestRoadNetworkAlgorithm(QgsProcessingAlgorithm):
"""
Class that described the algorithm, that will be launched
via the provider, itself launched via initialization of
the plugin.
The algorithm takes 4 entries :
- A cost raster
- The raster band to use for the cost
- The layer with the polygons of zones to access
- The layer with the roads (lines) that they can be connected to
by the generated roads
"""
# Constants used to refer to parameters and outputs. They will be
# used when calling the algorithm from another algorithm, or when
# calling from the QGIS console.
INPUT_COST_RASTER = 'INPUT_COST_RASTER'
INPUT_RASTER_BAND = 'INPUT_RASTER_BAND'
INPUT_POLYGONS_TO_ACCESS = 'INPUT_POLYGONS_TO_ACCESS'
INPUT_ROADS_TO_CONNECT_TO = 'INPUT_ROADS_TO_CONNECT_TO'
SKIDDING_DISTANCE = 'SKIDDING_DISTANCE'
METHOD_OF_GENERATION = 'METHOD_OF_GENERATION'
HEURISTIC_IN_POLYGONS = 'HEURISTIC_IN_POLYGONS'
ANGLES_CONSIDERED = 'ANGLES_CONSIDERED'
PUNISHER_45DEGREES = 'PUNISHER_45DEGREES'
PUNISHER_90DEGREES = 'PUNISHER_90DEGREES'
PUNISHER_135DEGREES = 'PUNISHER_135DEGREES'
OUTPUT = 'OUTPUT'
def initAlgorithm(self, config):
"""
Here we define the inputs and output of the algorithm, along
with some other properties. Theses will be asked to the user.
"""
self.addParameter(
QgsProcessingParameterRasterLayer(
self.INPUT_COST_RASTER,
self.tr('Cost raster layer'),
)
)
self.addParameter(
QgsProcessingParameterBand(
self.INPUT_RASTER_BAND,
self.tr('Cost raster band'),
0,
self.INPUT_COST_RASTER,
)
)
self.addParameter(
QgsProcessingParameterFeatureSource(
self.INPUT_POLYGONS_TO_ACCESS,
self.tr('Polygons to access via the generated roads'),
[QgsProcessing.TypeVectorPolygon]
)
)
self.addParameter(
QgsProcessingParameterFeatureSource(
self.INPUT_ROADS_TO_CONNECT_TO,
self.tr('Roads to connect the polygons to access to'),
[QgsProcessing.TypeVectorLine]
)
)
self.addParameter(
QgsProcessingParameterNumber(
self.SKIDDING_DISTANCE,
self.tr('Skidding distance (in CRS units)'),
type=QgsProcessingParameterNumber.Double,
defaultValue=100,
optional=False,
minValue=0
)
)
self.addParameter(
QgsProcessingParameterEnum(
self.METHOD_OF_GENERATION,
self.tr('Method of generation of the road network'),
['Random', 'Closest first', 'Farthest first'],
defaultValue=1
)
)
self.addParameter(
QgsProcessingParameterField(
self.HEURISTIC_IN_POLYGONS,
self.tr('Attribute field of the harvest polygons that indicates the order in which they must be reached'),
parentLayerParameterName=self.INPUT_POLYGONS_TO_ACCESS,
type=QgsProcessingParameterField.Numeric,
optional=True
)
)
self.addParameter(
QgsProcessingParameterEnum(
self.ANGLES_CONSIDERED,
self.tr('Should the angles of the roads be considered in the cost ?'),
['Yes', 'No'],
defaultValue=1
)
)
self.addParameter(
QgsProcessingParameterNumber(
self.PUNISHER_45DEGREES,
self.tr('Punishing multiplier for a 45 degrees turning angle'),
type=QgsProcessingParameterNumber.Double,
defaultValue=1.25,
optional=True,
minValue=1
)
)
self.addParameter(
QgsProcessingParameterNumber(
self.PUNISHER_90DEGREES,
self.tr('Punishing multiplier for a 90 degrees turning angle'),
type=QgsProcessingParameterNumber.Double,
defaultValue=2,
optional=True,
minValue=1
)
)
self.addParameter(
QgsProcessingParameterNumber(
self.PUNISHER_135DEGREES,
self.tr('Punishing multiplier for a 135 degrees turning angle'),
type=QgsProcessingParameterNumber.Double,
defaultValue=5,
optional=True,
minValue=1
)
)
self.addParameter(
QgsProcessingParameterFeatureSink(
self.OUTPUT,
self.tr('Output of the algorithm')
)
)
def processAlgorithm(self, parameters, context, feedback):
"""
Here is where the processing itself takes place.
"""
cost_raster = self.parameterAsRasterLayer(
parameters,
self.INPUT_COST_RASTER,
context
)
cost_raster_band = self.parameterAsInt(
parameters,
self.INPUT_RASTER_BAND,
context
)
polygons_to_connect = self.parameterAsVectorLayer(
parameters,
self.INPUT_POLYGONS_TO_ACCESS,
context
)
current_roads = self.parameterAsVectorLayer(
parameters,
self.INPUT_ROADS_TO_CONNECT_TO,
context
)
skidding_distance = self.parameterAsInt(
parameters,
self.SKIDDING_DISTANCE,
context
)
method_of_generation = self.parameterAsString(
parameters,
self.METHOD_OF_GENERATION,
context
)
if self.parameterAsString(
parameters,
self.HEURISTIC_IN_POLYGONS,
context
) is not None:
heuristic_in_polygons_index = polygons_to_connect.fields().lookupField(
self.parameterAsString(
parameters,
self.HEURISTIC_IN_POLYGONS,
context
))
else:
heuristic_in_polygons_index = None
if self.parameterAsString(
parameters,
self.ANGLES_CONSIDERED,
context
) == '0' :
angles_considered = True
else:
angles_considered = False
punisherAngleDictionnary = dict()
punisherAngleDictionnary[45] = self.parameterAsDouble(
parameters,
self.PUNISHER_45DEGREES,
context
)
punisherAngleDictionnary[90] = self.parameterAsDouble(
parameters,
self.PUNISHER_90DEGREES,
context
)
punisherAngleDictionnary[135] = self.parameterAsDouble(
parameters,
self.PUNISHER_135DEGREES,
context
)
# If source was not found, throw an exception to indicate that the algorithm
# encountered a fatal error. The exception text can be any string, but in this
# case we use the pre-built invalidSourceError method to return a standard
# helper text for when a source cannot be evaluated
if cost_raster is None:
raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT_COST_RASTER))
if cost_raster_band is None:
raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT_RASTER_BAND))
if polygons_to_connect is None:
raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT_START_LAYER))
if current_roads is None:
raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT_START_LAYER))
if skidding_distance is None:
raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT_START_LAYER))
if method_of_generation is None:
raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT_START_LAYER))
# We try to see if there are divergence between the CRSs of the inputs
if cost_raster.crs() != polygons_to_connect.sourceCrs() \
or polygons_to_connect.sourceCrs() != current_roads.sourceCrs():
raise QgsProcessingException(self.tr("ERROR: The input layers have different CRSs."))
# We check if the cost raster in indeed numeric
if cost_raster.rasterType() not in [cost_raster.Multiband, cost_raster.GrayOrUndefined]:
raise QgsProcessingException(self.tr("ERROR: The input cost raster is not numeric."))
# We initialize the "sink", an object that will make use able to create an output.
# First, we create the fields for the attributes of our lines as outputs.
# They will only have one field :
sink_fields = MinCostPathHelper.create_fields()
# We indicate that our output will be a line, stored in WBK format.
output_geometry_type = QgsWkbTypes.LineString
# Finally, we create the field object and register the destination ID of it.
# This sink will be based on the OUTPUT parameter we registered during initialization,
# will have the fields and the geometry type we just created, and the same CRS as the cost raster.
(sink, dest_id) = self.parameterAsSink(
parameters,
self.OUTPUT,
context,
fields=sink_fields,
geometryType=output_geometry_type,
crs=cost_raster.crs(),
)
# If sink was not created, throw an exception to indicate that the algorithm
# encountered a fatal error. The exception text can be any string, but in this
# case we use the pre-built invalidSinkError method to return a standard
# helper text for when a sink cannot be evaluated
if sink is None:
raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT))
# We check if the raster has been read correctly
feedback.pushInfo(self.tr("The extent of the raster is : " + cost_raster.dataProvider().extent().asWktCoordinates()))
# We put the data of the raster into a variable that we will send to the algorithm.
block = MinCostPathHelper.get_all_block(cost_raster, cost_raster_band)
# We transform the raster data into a matrix and check if the matrix contains negative values
# CAREFUL : The matrix is created in a raster coordinate systems; rows (y axis) start at the top
# and go to the bottom. This implies a transformation when getting back the values from a cartesian
# system (rows go from bottom to top)
matrix, contains_negative = MinCostPathHelper.block2matrix(block)
# We display a feedback on the loading of the raster, or we display an error if needed
if block.height() == 0 or block.width() == 0:
raise QgsProcessingException(self.tr("ERROR: The raster couldn't be read properly (0 rows or 0 columns). "
"This is often due to the raster being too big. "
"Try to lower the resolution of your raster, and/or limit it to the"
"extent of your data."))
feedback.pushInfo(self.tr("The size of the cost raster is: %d * %d pixels") % (block.height(), block.width()))
# If there are negative values in the raster, we make an issue.
if contains_negative:
raise QgsProcessingException(self.tr("ERROR: Cost raster contains negative value."))
feedback.pushInfo("Scanning the polygons to reach...")
# First of all : We transform the starting polygons into cells on the raster (coordinates
# in rows and colons).
polygons_to_reach_features = list(polygons_to_connect.getFeatures())
# feedback.pushInfo(str(len(start_features)))
# We make a set of nodes to reach.
set_of_nodes_to_reach, heuristicDictionnary = MinCostPathHelper.features_to_row_cols(polygons_to_reach_features,
heuristic_in_polygons_index,
cost_raster)
# If there are no nodes to reach (e.g. all polygons are out of the raster)
if len(set_of_nodes_to_reach) == 0:
raise QgsProcessingException(self.tr("ERROR: There is no polygon to reach in this raster. Check if some"
"polygons are inside the raster."))
feedback.pushInfo("Polygons scanned !")
feedback.pushInfo("Scanning the existing roads...")
# We do another set concerning the nodes that contains roads to connect to
roads_to_connect_to_features = list(current_roads.getFeatures())
# feedback.pushInfo(str(len(end_features)))
set_of_nodes_to_connect_to, uselessHeuristicDictionary = MinCostPathHelper.features_to_row_cols(roads_to_connect_to_features,
None,
cost_raster)
# If there is no nodes to connect to, throw an exception
if len(set_of_nodes_to_connect_to) == 0:
raise QgsProcessingException(self.tr("ERROR: There is no road to connect to in this raster. Check if some"
"roads are inside the raster."))
# If some overlap, raise another exception :
if set_of_nodes_to_reach in set_of_nodes_to_connect_to:
raise QgsProcessingException(self.tr("ERROR: Some polygons to reach are overlapping with roads "
"to connect to given this resolution."))
feedback.pushInfo("Roads scanned !")
# Before we start, we need to order the nodes with the chosen heuristic.
# We create a list that we are going to order.
list_of_nodes_to_reach = list(set_of_nodes_to_reach)
# If the method of generation asks for a random order, we shuffle the list randomly and it's over.
if method_of_generation == '0':
feedback.pushInfo("Randomizing order of cells to visit...")
random.shuffle(list_of_nodes_to_reach)
# If not, we create a list that will contain the minimal distance between the given node and the nodes to
# connect to.
else:
list_of_nodes_to_reach_with_order = list()
feedback.pushInfo("Computing distances between polygons and roads...(This can take some time !)")
feedbackProgress = 0
# To quickly calculate the distance from the existing roads to each node in our polygons, we will use
# the k-d tree function from Scipy.
# For that, we need to make an Numpy array containing our road nodes
pointsToReach = list()
for node in set_of_nodes_to_connect_to:
nodeToPoint = MinCostPathHelper._row_col_to_point(node, cost_raster)
pointsToReach.append(nodeToPoint)
numpyArrayOfRoadPoints = np.array(pointsToReach)
spatialKDTREEForDistanceSearch = KDTree(numpyArrayOfRoadPoints, leafsize=20)
# Then, we search and find the distance from current roads for each point.
for node in list_of_nodes_to_reach:
# feedback.pushInfo("For node " + str(node) + "...")
# Obsolete with the use of the k-d tree.
# minimalDistance = MinCostPathHelper.minimum_distance_to_a_node(node,
# set_of_nodes_to_connect_to,
# cost_raster)
# feedback.pushInfo("Brute force gives minimal distance : " + str(minimalDistance))
# We now make a query to the tree to find the closest node of the tree to the given node.
nodeAsPoint = MinCostPathHelper._row_col_to_point(node, cost_raster)
minimalDistance = spatialKDTREEForDistanceSearch.query(nodeAsPoint)[0]
# feedback.pushInfo("K-D Tree gives minimal distance : " + str(minimalDistance))
# We add the distance to a tuple, so that we can classify the node afterward.
list_of_nodes_to_reach_with_order.append((minimalDistance, node))
feedbackProgress += 1
feedback.setProgress(100 * (feedbackProgress / len(list_of_nodes_to_reach)))
if feedback.isCanceled():
raise QgsProcessingException(self.tr("ERROR: Operation was cancelled."))
feedback.pushInfo("Computing distances is done !")
# We then sort according to this distance, in increasing or decreasing order.
if method_of_generation == '1':
# We sort the list by the heuristic inside the polygons, AND the main heuristic.
list_of_nodes_to_reach_with_order = sorted(list_of_nodes_to_reach_with_order,
key=lambda tuple: (heuristicDictionnary[tuple[1]],
tuple[0]))
# list_of_nodes_to_reach_with_order.sort()
feedback.pushInfo("Ordering towards closest cells to visit...")
else:
feedback.pushInfo("Ordering towards farthest cells to visit...")
# Here, we take the opposite of the distance so that it respects the "smallest first" sorting, but
# in the opposite way to select the farthest first.
list_of_nodes_to_reach_with_order = sorted(list_of_nodes_to_reach_with_order,
key=lambda tuple: (heuristicDictionnary[tuple[1]],
-tuple[0]))
# list_of_nodes_to_reach_with_order.sort(reverse=True)
feedback.pushInfo("Ordering is done !")
# We put the result in the list of nodes to reach back again, removing the distance.
list_of_nodes_to_reach = [i[1] for i in list_of_nodes_to_reach_with_order]
# Now, time to launch the algorithm properly !
feedback.pushInfo(self.tr("Generating the road network...(This can take some time !)"))
# First, we have to initialize some things.
feedbackProgress = 0
errorMessages = 0
listOfResults = list()
pointsToReach = set()
for node in set_of_nodes_to_connect_to:
nodeToPoint = MinCostPathHelper._row_col_to_point(node, cost_raster)
pointsToReach.add(nodeToPoint)
# This is the road matrix. It's use to quickly know if there are roads at two given coordinates.
roadMatrix = np.zeros( (cost_raster.height(), cost_raster.width()) )
# A "1" means that there is a road at the given coordinate.
for node in set_of_nodes_to_connect_to:
roadMatrix[node[0]][node[1]] = 1
# Now, we have to initialize the circle neighborhood.
skiddingDistanceCircleNeighborhood = MinCostPathHelper.createRelativeCircleNeighborhood(skidding_distance,
cost_raster)
feedback.pushInfo(self.tr("Size of the skidding neighborhood : " + str(len(skiddingDistanceCircleNeighborhood))))
for nodeToReach in list_of_nodes_to_reach:
feedbackProgress += 1
# If the node to reach is inside a no-value pixel, no need to look at it.
if matrix[(len(matrix)-1)-nodeToReach[0]][nodeToReach[1]] is not None:
# First, we check the distance between the node and the nodes to connect to,
# to see if it's not at a skidding distance of it.
# Obsolete with the k-d tree.
# minimalDistanceToNodesToConnect = MinCostPathHelper.minimum_distance_to_a_node(nodeToReach,
# set_of_nodes_to_connect_to,
# cost_raster)
# To quickly calculate the distance from the existing roads to each node in our polygons, we will use
# the k-d tree function from SciPy.
# Now obsolete with the circle neighborhood method.
# For that, we need to make an Numpy array containing our road nodes
# spatialKDTREEForDistanceSearch = KDTree(np.array(list(pointsToReach)), leafsize=20)
# nodeAsPoint = MinCostPathHelper._row_col_to_point(nodeToReach, cost_raster)
# minimalDistanceToNodesToConnect = spatialKDTREEForDistanceSearch.query(nodeAsPoint)[0]
# If it's superior, we create a road to this node
# if minimalDistanceToNodesToConnect > skidding_distance:
# New method : using a relative neighborhood.
foundRoadInSkiddingDistance = MinCostPathHelper.checkRelativeCircleNeighborhoodForRoads(skiddingDistanceCircleNeighborhood,
nodeToReach,
roadMatrix)
if not foundRoadInSkiddingDistance :
start_row_col = nodeToReach
end_row_cols = list(set_of_nodes_to_connect_to)
min_cost_path, costs, selected_end = dijkstra(start_row_col, end_row_cols, matrix,
angles_considered, punisherAngleDictionnary, feedback)
# If there was a problem, we indicate if it's because the search was cancelled by the user
# or if there was no end point that could be reached.
if min_cost_path is None:
if feedback.isCanceled():
raise QgsProcessingException(self.tr("ERROR: Search canceled."))
else:
errorMessages += 1
# If there wasn't a problem, we save the results
else:
# When the road is done by the Dijkstra algorithm, we put the path and the cost
# in the list of results
listOfResults.append((min_cost_path, costs[-1]))
# We also add the nodes of the created path to the set of nodes that can be reached now
set_of_nodes_to_connect_to.update(min_cost_path)
for node in min_cost_path:
nodeToPoint = MinCostPathHelper._row_col_to_point(node, cost_raster)
pointsToReach.add(nodeToPoint)
roadMatrix[node[0]][node[1]] = 1
feedback.setProgress(100 * (feedbackProgress / len(list_of_nodes_to_reach)))
# When the loop is done..
feedback.setProgress(100)
feedback.pushInfo(self.tr("Network created ! Saving network..."))
# For every path we create, we save it as a line and put it into the sink !
ID = 1
for (path, cost) in listOfResults:
# feedback.pushInfo("Cost of feature saved : " + str(cost))
# Time to save the path as a vector.
# We take the starting and ending points as pointXY
start_point = MinCostPathHelper._row_col_to_point(path[0], cost_raster)
end_point = MinCostPathHelper._row_col_to_point(path[-1], cost_raster)
# We make a list of Qgs.pointXY from the nodes in our pathlist
path_points = MinCostPathHelper.create_points_from_path(cost_raster, path, start_point, end_point)
# With the total cost which is the last item in our accumulated cost list,
# we create the PolyLine that will be returned as a vector.
path_feature = MinCostPathHelper.create_path_feature_from_points(path_points, cost, ID, sink_fields)
# Into the sink that serves as our output, we put the PolyLines from the list of lines we created
# one by one
sink.addFeature(path_feature, QgsFeatureSink.FastInsert)
ID += 1
# We display the error messages if there was some
if errorMessages > 0:
feedback.pushInfo("WARNING : During the pathfinding, there was " + str(errorMessages) + " cases were a road could not"
" be constructed to a certain point to reach in a harvested polygon. When this happens, it's often due to"
" the cost raster containing No Data pixels that the algorithm cannot cross. Please, check"
" your cost raster and and change the No Data pixels if needed.")
# When all is done, we return our output that is linked to the sink.
return {self.OUTPUT: dest_id}
# Here are different functions used by QGIS to name and define the algorithm
# to the user.
def name(self):
"""
Returns the algorithm name, used for identifying the algorithm. This
string should be fixed for the algorithm, and must not be localised.
The name should be unique within each provider. Names should contain
lowercase alphanumeric characters only and no spaces or other
formatting characters.
"""
return 'Forest Road Network Creation'
def displayName(self):
"""
Returns the translated algorithm name, which should be used for any
user-visible display of the algorithm name.
"""
return self.tr(self.name())
def group(self):
"""
Returns the name of the group this algorithm belongs to. This string
should be localised.
"""
return self.tr(self.groupId())
def groupId(self):
"""
Returns the unique ID of the group this algorithm belongs to. This
string should be fixed for the algorithm, and must not be localised.
The group id should be unique within each provider. Group id should
contain lowercase alphanumeric characters only and no spaces or other
formatting characters.
"""
# Not enough algorithms in this plugin for the need to make groups of algorithms
return ''
# Function used for translation. Called every time something needs to be
# Translated
def tr(self, string):
return QCoreApplication.translate('Processing', string)
def createInstance(self):
return ForestRoadNetworkAlgorithm()
def helpUrl(self):
return 'https://github.com/Klemet/ForestRoadNetworkPluginForQGIS'
def shortHelpString(self):
return self.tr("""
This algorithm creates a forest road network based on areas to access (polygons) and current roads to connect them to (lines).
**Parameters:**
Please ensure all the input layers have the same CRS.
- Cost raster layer: Numeric raster layer that represents the cost of each spatial unit. It should not contains negative value. Pixel with `NoData` value represent it is unreachable.
- Cost raster band: The input band of the cost raster.
- Polygons to access via the generated roads: Layer that contains the polygons to access.
- Roads to connect the polygons to access to: Layer that contains the roads to connect the polygons to.
- Skidding distance. Maximum distance that a cell can be to not need a road going up to it.
- Method of generation : a parameter indicating what type of heuristic is used to generate the network. Random cell order, farther cells from current roads first, closer cells from curent roads first.
- Attribute containing an heuristic : An attribute field of the polygons that contains an heuristic that describe in which order the algorithm should reach them. The lower the value, the higher the priority; this way, the heuristic can be a date or a time. It is combined with the heuristic chosen before by the user to determine the order in which pixels are accessed a single polygon.
- Punishment of angles : The algorithm can "punish" the use of steep angle between a pixel and another by increasing the cost when it is used to allow for smoother lines. Remember that this does not affect the angle of connection between the roads, but only the zigzags in each road.
""")
def shortDescription(self):
return self.tr('Generate a network of roads to connect forest area to an existing road network.')
# Path to the icon of the algorithm
def svgIconPath(self):
return '.icon.png'
def tags(self):
return ['least', 'cost', 'path', 'distance', 'raster', 'analysis', 'road', 'network', 'forest', 'A*', 'dijkstra']
# Methods to help the algorithm; all static, do not need to initialize an object of this class.
class MinCostPathHelper:
# Function to transform a given row/column into a QGIS point with a x,y
# coordinates based on the resolution of the raster layer we're considering
# (calculated with its extent and number of cells)
# CAREFUL : Coordinate system is cartesian, in opposition to the
# matrix containing the data of the raster (see further down)
@staticmethod
def _row_col_to_point(row_col, raster_layer):
xres = raster_layer.rasterUnitsPerPixelX()
yres = raster_layer.rasterUnitsPerPixelY()
extent = raster_layer.dataProvider().extent()
x = (row_col[1] + 0.5) * xres + extent.xMinimum()
# There is a dissonance about how I see y axis of the raster
# and how the dijstra algorithm sees it. This is where the transformation is made.
y = (row_col[0] + 0.5) * yres + extent.yMinimum()
return QgsPointXY(x, y)
# Method to determine where a given polygon is in the raster
@staticmethod
def _polygon_to_row_col(polygon, raster_layer):
# We initialize the object to return : a set of nodes, corresponding a tuple of a row
# and a column in the raster
listOfNodesInPolygons = set()
# We get the extent of the raster
xres = raster_layer.rasterUnitsPerPixelX()
yres = raster_layer.rasterUnitsPerPixelY()
extentRaster = raster_layer.dataProvider().extent()
maxRasterCols = round((extentRaster.xMaximum() - extentRaster.xMinimum()) / xres)
maxRasterRows = round((extentRaster.yMaximum() - extentRaster.yMinimum()) / yres)
# We get the extent of the polygon
extentPolygon = QgsGeometry.fromPolygonXY(polygon).boundingBox()
# We traduce this extent into limits of columns and rows on the raster
rowMin = floor((extentPolygon.yMinimum() - extentRaster.yMinimum()) / yres)
rowMax = floor((extentPolygon.yMaximum() - extentRaster.yMinimum()) / yres)
colMin = floor((extentPolygon.xMinimum() - extentRaster.xMinimum()) / xres)
colMax = floor((extentPolygon.xMaximum() - extentRaster.xMinimum()) / xres)
# If one of these values is not in the range of the raster, then we
# restrict it to column and rows that are inside of it.
if rowMin < 0: rowMin = 0
if rowMax > maxRasterRows: rowMax = maxRasterRows
if colMin < 0: colMin = 0
if colMax > maxRasterCols: colMax = maxRasterCols
# If the polygon is out of range, then we return an empty set
if rowMin > maxRasterRows or colMin > maxRasterCols or rowMax < 0 or colMax < 0:
return set()
# If not, we treat the selected range of the raster to avoid looking
# at all of the raster.
# For each column of our range,
for col in range(colMin, colMax):
# For each row of our range,
for row in range(rowMin, rowMax):
# We make a centroid of the row/column of the raster
centroid = MinCostPathHelper._row_col_to_point((row, col), raster_layer)
# We check if the centroid is in the polygon
isInPolygon = QgsGeometry.fromPolygonXY(polygon).contains(centroid)
# If it is, we add the centroid to a set with the form of a node (tuple (row, column))
if isInPolygon:
listOfNodesInPolygons.add((row, col))
# if not, we continue the loops
# When the loops are done, we return the set of nodes that have been taken into account
return listOfNodesInPolygons
# Method to determine where a given line is in the raster. Similar to previous.
@staticmethod
def _line_to_row_col(line, raster_layer):
# We initialize the object to return : a set of nodes, corresponding a tuple of a row
# and a column in the raster
listOfNodesInPolygons = set()
# We get the extent of the raster
xres = raster_layer.rasterUnitsPerPixelX()
yres = raster_layer.rasterUnitsPerPixelY()
extentRaster = raster_layer.dataProvider().extent()
maxRasterCols = round((extentRaster.xMaximum() - extentRaster.xMinimum()) / xres)
maxRasterRows = round((extentRaster.yMaximum() - extentRaster.yMinimum()) / yres)
# We get the extent of the line
extentLine = QgsGeometry.fromPolylineXY(line).boundingBox()
# We traduce this extent into limits of colomns and rows on the raster
rowMin = floor((extentLine.yMinimum() - extentRaster.yMinimum()) / yres)
rowMax = floor((extentLine.yMaximum() - extentRaster.yMinimum()) / yres)
colMin = floor((extentLine.xMinimum() - extentRaster.xMinimum()) / xres)
colMax = floor((extentLine.xMaximum() - extentRaster.xMinimum()) / xres)
# If one of these values is not in the range of the raster, then we
# restrict it to column and rows that are inside of it.
if rowMin < 0: rowMin = 0
if rowMax > maxRasterRows: rowMax = maxRasterRows
if colMin < 0: colMin = 0
if colMax > maxRasterCols: colMax = maxRasterCols
# If the polygon is out of range, then we return an empty set
if rowMin > maxRasterRows or colMin > maxRasterCols or rowMax < 0 or colMax < 0:
return set()
# If not, we treat the select range of the raster.
# For each column of our range,
for col in range(colMin, colMax):
# For each row of our range,
for row in range(rowMin, rowMax):
# We make square polygon around the cell
centroid = MinCostPathHelper._row_col_to_point((row, col), raster_layer)
halfACellX = 0.5 * raster_layer.rasterUnitsPerPixelX()
halfACellY = 0.5 * raster_layer.rasterUnitsPerPixelY()
square = QgsGeometry.fromPolygonXY([[QgsPointXY(centroid.x() - halfACellX, centroid.y() - halfACellY),
QgsPointXY(centroid.x() + halfACellX, centroid.y() - halfACellY),
QgsPointXY(centroid.x() + halfACellX, centroid.y() + halfACellY),
QgsPointXY(centroid.x() - halfACellX, centroid.y() + halfACellY)]])
# We check if the centroid is in the polygon
intersectWithSquare = square.intersects(QgsGeometry.fromPolylineXY(line))
# If it is, we add the centroid to a set with the form of a node (tuple (row, column))
if intersectWithSquare:
listOfNodesInPolygons.add((row, col))
# if not, we continue the loops
# When the loops are done, we return the set of nodes that have been taken into account
return listOfNodesInPolygons
# Function to return a list of Qgs.pointXY. Each point is made based on the center of the node
# that we get from the path list.
# At the end, we put the precise coordinates of the starting/ending nodes that were given by
# the user at the start.
@staticmethod
def create_points_from_path(cost_raster, min_cost_path, start_point, end_point):
path_points = list(
map(lambda row_col: MinCostPathHelper._row_col_to_point(row_col, cost_raster), min_cost_path))
path_points[0].setX(start_point.x())
path_points[0].setY(start_point.y())
path_points[-1].setX(end_point.x())
path_points[-1].setY(end_point.y())
return path_points
@staticmethod
def create_fields():
# Create an ID field to know in which order the roads have been constructed
id_field = QgsField("Construction order", QVariant.Int, "integer", 10, 3)
# Create the field of "total cost" by indicating name, type, typeName,
# lenght and precision (decimals in that case)
cost_field = QgsField("Total cost", QVariant.Double, "double", 15, 3)
# Then, we create a container of multiple fields
fields = QgsFields()
# We add the fields to the container
fields.append(id_field)
fields.append(cost_field)
# We return the container with our fields.
return fields
# Function to create a polyline with the list of qgs.pointXY
@staticmethod
def create_path_feature_from_points(path_points, total_cost, ID, fields):
# We create the geometry of the polyline
polyline = QgsGeometry.fromPolylineXY(path_points)
# We retrieve the fields and add them to the feature
feature = QgsFeature(fields)
cost_index = feature.fieldNameIndex("total cost")
feature.setAttribute(cost_index, total_cost) # cost
id_index = feature.fieldNameIndex("Construction order")
feature.setAttribute(id_index, ID) # id
# We add the geometry to the feature
feature.setGeometry(polyline)
return feature
# Method to transform given features into a set of
# nodes (row + column) on the raster.
# Features have to be lines or polygons.
# Also return a dictionary containing the heuristic read in the polygon or line
# for use in ordering the nodes to reach.
@staticmethod
def features_to_row_cols(given_features, heuristic_index, raster_layer):
row_cols = set()
heuristic_dictionary = dict()
# extent = raster_layer.dataProvider().extent()
# if extent.isNull() or extent.isEmpty:
# return list(col_rows)
for given_feature in given_features:
if given_feature.hasGeometry():
given_feature_geom = given_feature.geometry()
if heuristic_index is not None:
attributes = given_feature.attributes()
heuristic = attributes[heuristic_index]
else:
heuristic = 0
# Case of multipolygons
if given_feature_geom.wkbType() == QgsWkbTypes.MultiPolygon:
multi_polygon = given_feature_geom.asMultiPolygon()
for polygon in multi_polygon:
row_cols_for_this_polygon = MinCostPathHelper._polygon_to_row_col(polygon, raster_layer)
row_cols.update(row_cols_for_this_polygon)
for row_col in row_cols_for_this_polygon:
heuristic_dictionary[row_col] = heuristic
# Case of polygons
elif given_feature_geom.wkbType() == QgsWkbTypes.Polygon:
polygon = given_feature_geom.asPolygon()
row_cols_for_this_polygon = MinCostPathHelper._polygon_to_row_col(polygon, raster_layer)
row_cols.update(row_cols_for_this_polygon)
for row_col in row_cols_for_this_polygon:
heuristic_dictionary[row_col] = heuristic
# Case of multi lines
elif given_feature_geom.wkbType() == QgsWkbTypes.MultiLineString:
multi_line = given_feature_geom.asMultiPolyline()
for line in multi_line:
row_cols_for_this_line = MinCostPathHelper._line_to_row_col(line, raster_layer)
row_cols.update(row_cols_for_this_line)
for row_col in row_cols_for_this_line:
heuristic_dictionary[row_col] = heuristic
# Case of lines
elif given_feature_geom.wkbType() == QgsWkbTypes.LineString:
line = given_feature_geom.asPolyline()
row_cols_for_this_line = MinCostPathHelper._line_to_row_col(line, raster_layer)
row_cols.update(row_cols_for_this_line)
for row_col in row_cols_for_this_line:
heuristic_dictionary[row_col] = heuristic
return row_cols, heuristic_dictionary
# Function that get the data block from a entire raster for a given band
@staticmethod
def get_all_block(raster_layer, band_num):
provider = raster_layer.dataProvider()
extent = provider.extent()
xres = raster_layer.rasterUnitsPerPixelX()
yres = raster_layer.rasterUnitsPerPixelY()
width = floor((extent.xMaximum() - extent.xMinimum()) / xres)
height = floor((extent.yMaximum() - extent.yMinimum()) / yres)
return provider.block(band_num, extent, width, height)
# Function that transforms
@staticmethod
def block2matrix(block):
contains_negative = False
matrix = [[None if block.isNoData(i, j) else block.value(i, j) for j in range(block.width())]
for i in range(block.height())]
for l in matrix:
for v in l:
if v is not None:
if v < 0:
contains_negative = True
return matrix, contains_negative
# This function return the minimum distance between a given node, and the nodes in a set or list of nodes.
@staticmethod
def minimum_distance_to_a_node(node, listOrSetOfNodes, raster_layer):
listOfNodes = list(listOrSetOfNodes)
minimumDistance = float("inf")
pointGeometryOfNode = QgsGeometry.fromPointXY(MinCostPathHelper._row_col_to_point(node, raster_layer))
for otherNode in listOfNodes:
distance = pointGeometryOfNode.distance(QgsGeometry.fromPointXY(MinCostPathHelper._row_col_to_point(otherNode, raster_layer)))
if distance < minimumDistance:
minimumDistance = distance
return minimumDistance
@staticmethod
def createRelativeCircleNeighborhood(skiddingDistance, raster_layer):
""""This method initialize a relative circle neighborhood based on the size of the pixels and on the
skidding distance inputted by the user, in order to check rapidly if an existing road is at skidding distance
from a given node."""
xres = raster_layer.rasterUnitsPerPixelX()
yres = raster_layer.rasterUnitsPerPixelY()
widthOfNeighborhood = floor(skiddingDistance / xres) + 1
heightOfNeighborhood = floor(skiddingDistance / yres) + 1
relativeCircleNeighborhood = set()
for col in range(-widthOfNeighborhood, widthOfNeighborhood+1):
for row in range(-heightOfNeighborhood, heightOfNeighborhood+1):
# If the euclidian distance between a relative coordinate and the "origin" cell is superior to the
# skidding distance, this cell won't be part of the neighborhood.
if sqrt((row*yres)**2 + (col*xres)**2) <= skiddingDistance:
relativeCircleNeighborhood.add((row, col))
return relativeCircleNeighborhood
@staticmethod
def checkRelativeCircleNeighborhoodForRoads(relativeCircleNeighborhood, nodeAsRowCol, roadMatrix):
""""This function uses the relative circle neighborhood to check if a road is at skidding distance from a
node."""
rowOfNode = nodeAsRowCol[0]
colOfNode = nodeAsRowCol[1]
foundARoad = False
for relativeNeighbour in relativeCircleNeighborhood:
relativeRow = rowOfNode + relativeNeighbour[0]
relativeColumn = colOfNode + relativeNeighbour[1]
# We check if the relative row index is correct; same for the column.
if relativeRow > 0 and relativeRow < len(roadMatrix):
if relativeColumn > 0 and relativeColumn < len(roadMatrix[0]):
if roadMatrix[relativeRow][relativeColumn] == 1:
foundARoad = True
break
return foundARoad