Tuesday 19 December 2017

python - Creating sector lights in QGIS?


I'm using QGIS 2.18. I need to create sector lights for navigational purposes on a map.


I have the light sector data as fields giving becon_id, start degree, end degree and color in a shapefile with over 500 buoys, beacons and lighthouses that need to be shown on the map. For each beacon, there can be many rows, each describing one light sector (for instance a white sector)


The end result should look about like below: The light sectors in the right colors, with the color marked as the character in the color field (R G W), and dotted lines from 100m to 1000m from the buoy/beacon/lighthouse.



This should most likely be created as a ruled-based symbol, but needs some python I guess?


enter image description here


Here below is an example of the shapefile data for a lighthouse (unfortunately not the above one) which has a green sector between 114 and 154 degrees, a white sector between 154 and 168 degrees, a red sector between 168 and 237 degrees, a green sector between 237 and 314 degrees, a white sector between 314 and 320 degrees, a red sector between 320 and 337 degrees (for some reason, 0 is not north but south):


shapefile table example



Answer



EDIT I edited the answer for managing particular situations (due to specific angle values) and for not displaying the dotted lines when a round angle is defined.




I propose a solution by only recurring to rule-based symbology and labeling.


Before starting, I want to underline that I will focus the attention on the explanation of the minimal things to do for reproducing the desired result: this means that some other minor parameters (like sizes, widths and so on) should be easily adjusted by you for better fitting your needs.


Furthermore, this solution only works if you assume that 0 degree is North instead of South (if 0 is South, instead, it would be enough summing a 180 value every time that appears a '90' in formulas which deal with angles, e.g. cos(radians(90)) would become cos(radians(180 + 90))). I preferred doing this only for the sake of giving a more general solution.






We will render the points with a Single symbol and by recurring to one Simple Marker and three Geometry generator symbol layers:


enter image description here


In the further explanation, I will follow the same order of the symbols in the image above.


1) Simple Marker


I chose a default symbol of a black star (this is the easier part of this tutorial), having a size of 3 mm and a width of 0.4 mm.


2) Geometry Generator No. 1


Add a new symbol layer and select the Geometry generator type:


enter image description here



Insert this expression in the Expression field:


CASE
WHEN abs( "ALKUKULMA" - "LOPPUKULMA") < 360
THEN
make_line(
$geometry,
make_point(
$x + 1000*cos(radians(90 - "ALKUKULMA")),
$y + 1000*sin(radians(90 - "ALKUKULMA"))
)

)
END

We have just defined the first line which points towards the point where the light sector starts from. This line is 1000 m long and it's created only when the opening angle of the sector light is not a round angle (this happens for avoiding that the line would break a whole circle).


3) Geometry Generator No. 2


Same as above but, in this step, you need to use this expression:


CASE
WHEN abs( "ALKUKULMA" - "LOPPUKULMA") < 360
THEN
make_line(

$geometry,
make_point(
$x + 1000*cos(radians(90 - "LOPPUKULMA")),
$y + 1000*sin(radians(90 - "LOPPUKULMA"))
)
)
END

We have just defined the first line which points towards the point where the light sector ends at. This line is 1000 m long and it's created only when the opening angle of the sector light is not a round angle (this happens for avoiding that the line would break a whole circle).


4) Geometry Generator No. 3



Insert this expression in the Expression field:


CASE

WHEN abs("ALKUKULMA" - "LOPPUKULMA") <= 180 AND "ALKUKULMA" >= "LOPPUKULMA"
THEN
difference(
boundary(
buffer(
$geometry, 900)
),

make_polygon(
geom_from_wkt(
geom_to_wkt(
make_line(
$geometry,
make_point($x + 2000*cos(radians(90 - "ALKUKULMA" )), $y + 2000*sin(radians((90 - "ALKUKULMA" )))),
make_point($x + 2000*cos(radians(90 - ("LOPPUKULMA" + "ALKUKULMA")/2 )), $y + 2000*sin(radians((90 - ("LOPPUKULMA" + "ALKUKULMA")/2 )))),
make_point($x + 2000*cos(radians(90 - "LOPPUKULMA")), $y + 2000*sin(radians((90 - "LOPPUKULMA")))),
$geometry)
)

)
)
)

WHEN abs("ALKUKULMA" - "LOPPUKULMA") <= 180 AND "ALKUKULMA" <= "LOPPUKULMA"
THEN
intersection(
boundary(
buffer(
$geometry, 900)

),
make_polygon(
geom_from_wkt(
geom_to_wkt(
make_line(
$geometry,
make_point($x + 2000*cos(radians(90 - "ALKUKULMA" )), $y + 2000*sin(radians((90 - "ALKUKULMA" )))),
make_point($x + 2000*cos(radians(90 - ("LOPPUKULMA" + "ALKUKULMA")/2 )), $y + 2000*sin(radians((90 - ("LOPPUKULMA" + "ALKUKULMA")/2 )))),
make_point($x + 2000*cos(radians(90 - "LOPPUKULMA")), $y + 2000*sin(radians((90 - "LOPPUKULMA")))),
$geometry)

)
)
)
)

WHEN abs("ALKUKULMA" - "LOPPUKULMA") > 180 AND "ALKUKULMA" >= "LOPPUKULMA"
THEN
intersection(
boundary(
buffer(

$geometry, 900)
),
make_polygon(
geom_from_wkt(
geom_to_wkt(
make_line(
$geometry,
make_point($x + 2000*cos(radians(90 - "ALKUKULMA" )), $y + 2000*sin(radians((90 - "ALKUKULMA" )))),
make_point($x - 2000*cos(radians(90 - ("LOPPUKULMA" + "ALKUKULMA")/2 )), $y - 2000*sin(radians((90 - ("LOPPUKULMA" + "ALKUKULMA")/2 )))),
make_point($x + 2000*cos(radians(90 - "LOPPUKULMA")), $y + 2000*sin(radians((90 - "LOPPUKULMA")))),

$geometry)
)
)
)
)

WHEN abs("ALKUKULMA" - "LOPPUKULMA") > 180 AND "ALKUKULMA" <= "LOPPUKULMA"
THEN
difference(
boundary(

buffer(
$geometry, 900)
),
make_polygon(
geom_from_wkt(
geom_to_wkt(
make_line(
$geometry,
make_point($x + 2000*cos(radians(90 - "ALKUKULMA" )), $y + 2000*sin(radians((90 - "ALKUKULMA" )))),
make_point($x - 2000*cos(radians(90 - ("LOPPUKULMA" + "ALKUKULMA")/2 )), $y - 2000*sin(radians((90 - ("LOPPUKULMA" + "ALKUKULMA")/2 )))),

make_point($x + 2000*cos(radians(90 - "LOPPUKULMA")), $y + 2000*sin(radians((90 - "LOPPUKULMA")))),
$geometry)
)
)
)
)


END


We have just defined the arc between the starting and ending points of the light sector (please note that 2000 is an arbitrary value because I'm trying to create a polygon to intersect with the boundary of the circle having a radius of 900 m).


Furthermore, we need to set the color which is stored in the "VARIS" field. For doing this, we need to specify it with a custom expression. Follow the arrow in the image below:


enter image description here


and then type this expression after having clicked on the Edit... button:


CASE
WHEN "VARIS" = 'vi' THEN color_rgb(51,160,44)
WHEN "VARIS" = 'v' THEN color_rgb(255,255,255)
WHEN "VARIS" = 'p' THEN color_rgb(227,26,28)
END


Please note that, for this symbol layer, I created two lines: the upper line defines the color to use (in fact I set the custom expression for this one), while the lower one is useful for defining a black border (it will have a width which is bigger than the one of the upper line). Remember also to set Flat as Cap style for both lines for avoiding any color overlapping.





1) Setting the labels


Go to Layer Properties > Labels and, as usual, follow the red arrows:


enter image description here


and then type this expression:


CASE
WHEN "VARIS" = 'vi' THEN 'G'
WHEN "VARIS" = 'v' THEN 'W'

WHEN "VARIS" = 'p' THEN 'R'
END

We have just defined the color rule using the value stored in the "VARIS" field.


2) Setting the placement for labels


Select the Placement option in the Labels Menu and select Offset from point.


Then, with reference to the image below:


enter image description here


follow the red arrow and type this expression:


CASE

WHEN "ALKUKULMA" > "LOPPUKULMA"
THEN
concat(
-1000*cos(radians(90 - ("ALKUKULMA" + "LOPPUKULMA")/2)),
',',
1000*sin(radians(90 - ("ALKUKULMA" + "LOPPUKULMA")/2))
)
WHEN "ALKUKULMA" <= "LOPPUKULMA"
THEN
concat(

1000*cos(radians(90 - ("ALKUKULMA" + "LOPPUKULMA")/2)),
',',
-1000*sin(radians(90 - ("ALKUKULMA" + "LOPPUKULMA")/2))
)
END

Then, follow the green arrow and type this expression:


CASE
WHEN "ALKUKULMA" >= "LOPPUKULMA"
THEN

180-(("ALKUKULMA" + "LOPPUKULMA")/2)
WHEN "ALKUKULMA" < "LOPPUKULMA"
THEN
- (("ALKUKULMA" + "LOPPUKULMA")/2)
END


If you correctly performed the previous tasks, you should be able to get this result:


enter image description here


Bonus



Since the minor parameters were too many for being completely covered within this answer, I have attached the style here: you may open this code with any text editor and save it as a QGIS Layer Style file (i.e. with a .qml extension).


The above style was created using QGIS 2.18.4 (it must have the same name of the shapefile you are using).


No comments:

Post a Comment

arcpy - Changing output name when exporting data driven pages to JPG?

Is there a way to save the output JPG, changing the output file name to the page name, instead of page number? I mean changing the script fo...