10

I have GPS coordinates which identify aid projects in Africa. Now, I want to re-locate these projects, such that each project is re-located by 100 km in a random direction from its original location.

(Extra: I have country layers; can I force QGIS to re-locate only within the original country?)

I tried the Virtual Layer & Geometry Generator, both didn't work so far. Any leads?

I use QGIS 3.14.15.

Babel
  • 71,072
  • 14
  • 78
  • 208
Franziska
  • 119
  • 4

1 Answers1

21

This answer has been updated several times to get a solution that gradually adds complexity to the expression. For the final solution, you should go directely to the very bottom (step 5). The history of updating the orignal answer however adds up to a step by step guide of how this solution works, starting with a very easy way. It gives some insight how more complex QGIS expressions work and what can be achieved with it. Each step of complexity is separated by a horizontal rule and named Step 1, Step 2 ... to Step 5 so that you can jump easily to these steps. Step 4 is a detailed explanation about how all the different parts of the expressions come together.


Step 1

You can do this by using the following expression (see below for the extra to re-locate inside of countries):

project ( $geometry , 100000 , radians ( rand ( 0,360) ) ) 

If you need it for visualization purpose only, add a symbol layer, define it as geoemtry generator and paste the expression above. Change 100000 if the units of your CRS is not in meters. Be aware that every time you pan, zoom or perform another activity, the layer is updated and the random points will be on a new location. If you also want a random distance of say between 50 and 100 km, replace 100000 with rand ( 50000, 100000).

If you want the actual geometry, use Menu processing / toolbox / geoemtry by expression to generate a new layer, use the same expression.

Result: red dots: original; blue dots: re-located points.

enter image description here

There is a possibility to create x and y coordinates for the random points in your original points layer without creating a new geometry. To do that, use the field calculator and create a new field, named let's say x_coordinate and y_coordinate. Use the expression x(geometry) and y(geometry) to create a separate field for each that contains the coordinates of the randomly re-placed points whereas geometry is the expression to create the re-placed points (in the screenshot below I used the more sophisticated expression from below). You will get two new fields with values of re-placed x-/y- coordinates. They will be stored parmanently and will not change/update any more (you could use virtual fields to have changing content).

qgis expression editor: create x and y coordinates

With that, you could use this fields to visualize your points in the re-placed position, using a symbol layer with the expression:

make_point(
   "x_coordinate" ,  
   "y_coordiante" 
)

This expression generates the white, re-placed, points in the following sreenshot. If you want, you can remove the original representation of the original, red, points to keep only the re-placed points:

qgis expression editor: geometry generator make_point


Step 2

A refined version to re-locate the points only inside the country. Same solution, but instead of the above expression, use this one here. Be aware to change 'countries' to the name of the layer that contains your country polygons. Both layers as well as the project should be in the same CRS, best use a projected CRS (with a geographic CRS, distance measurements do not make sense).

Again, this layer should have a field 'fid' that contains a unique integer (an not, as stated in an earlier version, any unique field value like country name). You can replace 'fid' in the following expression with any fieldname of your countries layer that contains an integer that is unique:

with_variable ( 
   'boundary',
    boundary ( 
      intersection (
         buffer ( $geometry, 100000),
         geometry(
            get_feature( 
               'countries',
                'fid',
                to_int ( array_to_string (
                   overlay_within (
                      'countries', 
                      fid)
                 ))
            )
          )
       )
   ), 
   line_interpolate_point (
      @boundary,
      rand (
         0,
         length(@boundary)
      )
   )
)

Step 3

And a further perfection of the expression: in the version above, if the original points are near the border (< 100 km), the re-placed points are sometimes located on the border itself and thus less than 100 km away from the orginal point. The above expression creates a circle around the original point and does an intersection with the current country. It than takes the outer line of this intersection and places a random point on this outer boundary - and this boundary includes sometimes segments of the country-borders if the original point is within a distance of 100 km.

To prevent that, I changed the expression accordingly, adding a difference operation: from the boundary just described, it clips (extracts) all country borders. On the remaining lines (only those parts of the original buffer-circle inside the current country of the point remain), the new points are positioned randomly. (see at the bottom of this answer for a revised version of the expression, as not all border segments are clipped completely):

with_variable ( 
   'boundary',
difference(
    boundary ( 
      intersection (
         buffer ( $geometry, 100000),
         geometry(
            get_feature( 
               'countries',
                'fid',
                to_int ( array_to_string (
                   overlay_within (
                      'countries', 
                      fid)
                 ))
            )
          )
       )
  ), 
  boundary ( 
     aggregate ( 
        'countries', 
        'collect',
        $geometry)
  )),

line_interpolate_point ( @boundary, rand ( 0, length(@boundary) ) ) )


Step 4

Following a description with screenshots to better understand how the expression works:

  1. create a buffer of 100 km around the points: buffer ( $geometry, 100000) (polygon)

qgis expression editor: create buffer from points

  1. get the polygon shapes of the countries from the other layer:

expression:

geometry(
   get_feature(
         'countries',
      'fid',
      to_int ( 
         array_to_string (
            overlay_within (
           'countries', 
                   fid)
         )
      )
   )
)

(polygon)

qgis expression editor: feature overlay polygon geometry

  1. Intersection of 1 and 2: only those parts of the buffers (1) that lay within the current country (2)

intersection ( a, b), whereas a is the expression from 1, b the expression from 2

qgis expression editor: intersect

  1. create the outer boundary of 3 (part of the buffer that is inside the country): boundary (c), whereas c is the expression from 3 (line)

qgis expression editor: boundary intersection to create line

  1. get the country borders as line from the polygon boundaries of the countries layer:

expression:

boundary ( 
       aggregate ( 
          'countries', 
          'collect',
          $geometry)
    )

(line) qgis expression editor: boundary aggregate

  1. clip (extract) the country borders (5) from 4 (outer boundary of buffer inside country): difference(d, e), whereas d is the expression from 4 and e is the expression from 5

qgis expression editor: difference, clip country border

  1. Now, set a random point somewhere on the line from no. 6 with the expression line_interpolate_point(geometry,distance) whereas
  • geometry is the expression from 6 (thus: the 100-km circle except the parts laying outside of the country and without the country borders)
  • distance on this line is create randomly with the expression rand(min,max) with min=0 and max= whole length of this geometry line (from no. 6) - so the point will be allocated ranomly somewhere between the start and the end of the line.
  1. The last (or very first part) of the expression is with_variable(name,value,expression): this sets a variable and within this function we have all the elements we created from 1 to 7 together.
  • A: The variable has the name (1st argument) 'boundary' (to be referenced in C, see below) - the name is arbitrary and could be anyting else.

  • B: it's value (2nd argument) is the whole expression created in no. 6 (with the element from 1 to 5), thus the cirle line without country borders and

  • C: the expression (3rd argument) is the the line_interpolate_point expression from no. 7 that takes the input-geometry (the value from B) as variable @boundary, the name defined above (see A) (the @ character in front of the name of the variable is the QGIS convention to address a variable).

The result looks like this - blue: original point with id-label, white: re-located point with id-label:

enter image description here


Step 5

And a (hopfully very last) improvement: as I realized only now, in some cases points still may lay on the border, less than 100 km away from the original point. The border-segments that are crossed by the circle are not clipped, see: qgis expression editor line segments

This is the background why I asked this question:Getting the segment of a line, crossed by another with QGIS expression

But there is an easier solution that I include here for completeness, to have a "final" answer: in step 4, part 5 (get the country borders), instead of using the country borders, I create a buffer of let's say 1 meter around the border, so this part of the expression look slightly different:

buffer (
   boundary ( 
      aggregate ( 
         'countries', 
         'collect',
          $geometry)
       ) ,
    1 
)

Be aware that now, it takes quite some time to render the map image because QGIS has to calculate the buffer around the borders. This last solution is better to use with the geometry by expression tool to create once and for all new geometries, not for symbol layers that start to render each time you pan or zoom.

Increasing the number 1 in the expression, you have the control of how far away the random points should be from the border - setting it to 20000 [meters, in case you use a projected CRS with meters as units] means that re-located points should have at least a distance of 20 km away from the border, while still being 100 km away from the original point.

qgis expression editor: buffer boundary segments

So here is the final expression the generate these random points:

with_variable ( 
   'boundary',
difference(
    boundary ( 
      intersection (
         buffer ( $geometry, 100000),
         geometry(
            get_feature( 
               'countries',
                'fid',
                to_int ( array_to_string (
                   overlay_within (
                      'countries', 
                      fid)
                 ))
            )
          )
       )
  ), 
  buffer (boundary ( 
     aggregate ( 
        'countries', 
        'collect',
        $geometry)
  ),1)),

line_interpolate_point ( @boundary, rand ( 0, length(@boundary) ) ) )

That's all Folks!


Babel
  • 71,072
  • 14
  • 78
  • 208
  • Thanks! The first version worked - is there any way I can export the "new" coordinates as a csv assigned to the data of the original point? I couldn't make it work with the second option - it won't allow the geometry expression. I pasted project_location_id ( $geometry , 100000 , radians ( rand ( 0,360) ) ) - got it wrong? – Franziska Nov 27 '20 at 16:19
  • For the second option, geometry by expression: did you chose point as geometry type? See: https://i.stack.imgur.com/mR4bo.png – Babel Nov 27 '20 at 16:27
  • Unfortunately, you can't export the "new" coordinates in the first version - the points are not real geometries, but just "styles". If want real geometries, you must stick to the second option, than you can export this layer as csv (rightclick layer / export). The same is possible with the virtual layers solution presented by @Taras. – Babel Nov 27 '20 at 16:32
  • Yes! I choose "project_location_id" (which identifies my projects uniquely) from the drop down menu, but as soon as I add the $geometry command, it turns red and produces an error. Did I misunderstand what "project" would stand for? (In the original file, I have latitude and longitude in the same row as project_location_id.) – Franziska Nov 27 '20 at 16:46
  • 1
    project in the expression is a function that projects (re-places) a point to another location - so don't change this part of the expression. Just replace the numbers (the distance, in principle) if it does not suit. You only have to select the right layer above, in the drop-down menu - the same layer you used for the first version. See my screenshot: everything except the input layer (africa2) and the distance (100000) should look exactely the same: https://i.stack.imgur.com/mR4bo.png – Babel Nov 27 '20 at 16:53
  • Thank you, very helpful! Works now. – Franziska Nov 27 '20 at 16:59
  • 1
    Great! Than it's time to try the other solution I just added - it displaces the points only within your country-layer – Babel Nov 27 '20 at 17:52
  • 1
    Amazing explanation! I managed and now want to export the file with the new coordinates. I right-click on Layer and then Export > Save Features as and select Geometry > As XY, however, it only exports the old coordinates (old data file, two columns added with the same latitude/longitude data as original point). – Franziska Nov 28 '20 at 13:05
  • Figured it out! Same thing as yesterday - when pasted in geometry generator, it is for visual purposes. But when adding the code in "Geometry by expression", I can export it as described above (As YX). – Franziska Nov 28 '20 at 13:37
  • You're right! Glad that it works. There is a possibility to create x and y coordinates for the random points in your original points layer without creating a new geometry. I added that to the solution, search for "possibility to create x" as the answer meanwhile got quite long... ;-) – Babel Nov 28 '20 at 14:31
  • If the solution worked for you, consider accepting this answer: click the check mark next to the question on the left side: [link]http://i.stack.imgur.com/fpe2V.png
    See [tour]
    – Babel Nov 28 '20 at 14:47
  • what an answer, I am honestly amazed =) – Taras Nov 30 '20 at 09:55
  • 1
    Thanks, @Taras - I'm learning from the best. The honor for not less great answers belongs to you ! There are so many very substantial answers out here by you and I learned a lot from them (and will continue to do so, hopefully). – Babel Nov 30 '20 at 10:05
  • This is quite an elaborate use of the expression, though the cost of recalculation worries me as all the expressions are re-evaluation on redraw and the aggregate operations can be quite demanding. A simpler option would be to make a model to build a small buffer ring within the country, generate a point in the ring and join it to the buffer to get the attributes. – Al rl Jan 28 '21 at 00:38
  • You're completely right, updating the canvas gets very slow with this solution - so maybe better use this expression with geometry by expression to create the points once and for all, avoiding permanent redrawing. The performance issue of this solution was one of the reasons to ask this question: https://gis.stackexchange.com/questions/382520/performance-limits-of-qgis-expressions – Babel Jan 28 '21 at 11:02