LandScript - Language Basics

1. Language Syntax

LandScript is made up of commands and functions that operate on numbers, text and spatial objects. The layout of script on the page is flexible and is designed to make reading script as easy as possible. Script can be spread over multiple lines with each instruction being terminated with a semicolon';'.

All non-blank lines in a script file will be interpreted by LandSerf's script engine except those that start with the hash symbol '#'. This symbol can be used to identify comment lines that help to explain the meaning of the script.

So, for example, the two scripts shown below are treated in exactly the same way even though their layout and comments differ.

1
2
3
4
# Simple script to display a message on screen.
version(1.0);

echo("Hello world");
1
2
3
4
5
6
version(1.0);
echo
# This is a comment line so it is ignored.
  ("Hello world");
# Layout is flexible, but each command line
# must be terminated with a semicolon.

LandScript is a case sensitive language, and by convention, all variables, functions and commands are given lower case letters (an exception is the intercapping of variable names described below).

It is possible that commands, functions or even syntax may change in future versions of LandScript. It is therefore good practice to include the version() command at the start of a script. This will ensure that the LandScript interpreter will always process the script in the way it was originally intended.

2. Variables

Variables can be used to store items of information. There are 3 types of information that can be stored - numbers, text and spatial objects. LandScript is not a strictly typed language, which means that you do not need to declare what type of information is to be stored in a variable, and the type of information it does store can change while a script is running.

Information is placed in variables using the assignment operator '=', where the code on the left of the = should be name of the variable to store the information, and the code on the right should be an expression that defines what is to be stored within it.

1
2
3
4
5
6
7
8
9
10
# Script to show multiple variable types.
version(1.0);

message = "Hello World";
echo(message);

message = 1+2+3;
echo(message);

myRaster = newraster(0,0,10,10,100,100);

In the example above, the variable message firstly stores a text string ("Hello World"). Text strings can be indicated with pairs of quotation marks (") surrounding the text. The next line in the example replaces the contents of the same variable with the result of evaluating the expression 1+2+3 (the number 6). Finally a separate variable myRaster is used to store an entire raster map created by the command newraster().

Variable names must start with alphabetic characters but can contain numbers (e.g. answer1, map2006 etc.). They cannot contain spaces or non-alphanumeric characters. By convention, variable names should be lowercase except when they consist of compound words, in which case intercapping (capitalising of the first letter of subsequent words) is encouraged. For example myRaster, forestMap.

Variables can also appear on the right hand side of the assignment operator, but only if they have been previously defined:

1
2
3
4
5
6
# The following two lines are valid
total = 0;
total = total+1;

# But the following would generate the error "'newTotal' not found".
newTotal = newTotal + 1;

The scope of a variable describes the parts of code in which its contents can be read or changed. Scope is limited to the block of code in which it is first assigned. A block (see section 3 below), is usually defined as any code enclosed between braces { and } including any nested sub-blocks within it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# var1 is first defined in the global block so is visible to the whole script.
var1 = "Global variable";

{
  # This is a sub-block so var2 is only visible within this block
  # Note also that variables defined in the 'parent' block(s) of
  # a sub-block are also visible.
  var2 = "Local variable";
  echo ("Var1 is " & var1 & " Var2 is " & var2);
  
  # This will change the contents of the global variable originally defined outside this block.
  var1 = "A new value";
}

 # The following line would now generate an error since var2 is not visible.
 echo ("Var1 is " & var1 & " Var2 is " & var2);

Variables that are defined in the outermost block (as var1 in the example above) are effectively global variables as their values can be read and changed in all parts of the script. The LandScript ScriptEditor identifies such global variables by colouring them mid-grey. It is generally good practice to make variables as local as possible within a script to avoid the possibility of accidentally changing variable contents and to limit naming conflicts.

3. Expressions

An expression is any collection of quantities (numbers, text or spatial objects) and operators that can be evaluated by the LandScript interpreter to produce a new quantity. The following are all examples of expressions:

1
2
3
4
5
6
numeric1 = 1 + (2/3) * 4;
numeric2 = sin(pi()/3);
numeric3 = numeric1 + 2*numeric2;
text1    = "First pair. " & "Second pair.";
spatial1 = newraster(5,5,15,15,100,100);
spatial1 = spatial1 + 5;
# Evaluated as 3.6666666666666665 (note rounding error in last digit)
# Evaluated as 0.8660254037844386
# Evaluated as 5.398717474235544
# Evaluated as the text 'First pair. Second pair.' (without quotes)
# Evaluated as a raster of 100x100 cells each containing 0
# Evaluated as a raster of 100x100 cells each containing 5 

4. Loops and Program Control

When LandScript code is interpreted, it is normally done line-by-line starting from the top of the script. However, it is possible to modify the flow of program control using loops, conditional blocks and the return command.

4.1 Loops

Loops are created using the command while followed by a condition in brackets and terminated by a semicolon. The condition can be any expression that can be evaluated to be true/non-zero or false/zero. The code to be repeated in the loop is then enclosed in braces following the while line. Because the looped code is defined in its own block, any variables defined for the first time inside the loop, are only visible within it.

1
2
3
4
5
6
7
8
9
version(1.0);

tableRow = 1;

while (tableRow <= 10);
{
  echo(tableRow & " x 7 = " & tableRow*7);
  tableRow = tableRow + 1;
}
1 x 7 = 7
2 x 7 = 14
3 x 7 = 21
4 x 7 = 28
5 x 7 = 35
6 x 7 = 42
7 x 7 = 49
8 x 7 = 56
9 x 7 = 63
10 x 7 = 70

The script above left generates times-table output shown to the right. You must take care to ensure that it is possible for the loop to terminate in a finite amount of time. In the example above this is ensured by adding 1 to the value of tableRow each time control loops round the code in braces.

Note that if you are used to programming in languages such as C++ or Java, the while line must always be terminated with a semicolon and the looped code itself must always be enclosed in braces even if it only contains a single line. Note also that there is no 'for' loop in LandScript. This can be represented instead with a while loop and a counter variable as shown in the times-table example above.

4.2 Conditional Blocks and the return statement

A conditional block is identified with the if keyword followed by a condition in brackets and terminated by a semicolon. It works in much the same way as a while loop, except that the code within braces is only run once if the condition is evaluated to be true or non-zero. As with any block of script, the scope of any variables defined for the first time within a conditional block is limited to that block.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Script to toss a coin.
version(1.0);

randomNum = rand();

if (randomNum < 0.5);
{
   echo("Heads");
}

if (randomNum >= 0.5);
{
   echo("Tails");
}

In the simple example above, two conditional blocks are used to simulate the outcome of a randomly tossed coin. Note that there is no 'else' construction in LandScript, so if mutually exclusive actions have to be modelled (as in the coin example above), you must explicitly state the mutually exclusive conditions.

It is possible exit a script early using the return keyword. When not used inside a script function (see section 5 below), calling return will quit the script, so it is most likely to be used inside a conditional block. For example,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Creates a raster containing the z scores of another raster.
version(1.0);

baseDir = "z:\data\testdata\";
surf =open(baseDir&"nyquist.srf");
zScores = new(surf);

mean = info(surf,"mean");
stdev = info(surf,"stdev");

if (stdev == 0);
{
  echo ("Raster has no variation, so cannot calculate z-scores.");
  return;
}

zScores = (surf-mean)/stdev;

colouredit(zScores,"diverging1");
save(zScores, baseDir&"zScores.srf");

The conditional block from lines 11-15 first checks to see if there is any variation in the attribute values contained in the raster. If there is no variation (ie the standard deviation is zero), it reports an error message and exits the script at line 14. If there is some variation, control passes to line 17 and beyond.

5. Script Functions

You can create your own functions (also known as methods or subroutines) in LandScript in order to group together the lines of code required to perform common tasks. User-defined functions should be indicated with the function keyword followed by the name of the function you wish to define and any parameters it requires. Following this line should be a block of code in braces that defines what the function actually does.

Functions can optionally return a value that can be used by the main body of the script in further processing. If the value is numeric, this can be combined with other built in functions to form more complex expressions.

1
2
3
4
5
6
7
8
9
10
11
12
13
# Script to show how a simple function works 
version(1.0); 
 
echo ("The sum of the numbers is "&sum(3,4)); 
 
# Function to add two numbers together. 
# Params: num1 First number to add. 
#         num2 Second number to add. 
# Return: Sum of the two given numbers. 
function sum(num1, num2) 
{ 
    return num1+num2; 
} 

Functions that do not return a value, but instead perform some specific task can also be useful, For example, the following displays the metadata associated with a raster map:

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
# Script to display the metadata associated with a raster. 
version(1.0); 
 
baseDir = "/data/testdata/"; 
 
newRaster = open(baseDir & "xslope.srf"); 
ans = displayInfo (newRaster); 
 
 
# Function to display raster metadata. 
# Params: rast Raster whose metadata is to be reported. 
function displayInfo(rast) 
{ 
  echo("Metadata for "&info(rast,"title")); 
 
  echo("Bounds:"); 
  echo("     "&info(rast,"N")); 
  echo(info(rast,"W")&"  "&info(rast,"E")); 
  echo("     "&info(rast,"S")); 
  echo("Minimum: "&info(rast,"min")&" Maximum: "&info(rast,"max")); 
 
  echo("Resolution - x: "&info(rast,"xRes")&" y: "&info(rast,"yRes")); 
  echo("Projection: "&info(rast,"projection")); 
  echo("Type: " & info(rast,"type")); 
  echo(info(rast,"notes")); 
} 

Functions can also recursively call themselves to provide very powerful processing constructs. In the example below, a simple recursive function calculates the factorial of any given number (the factorial of 6 is 6*5*4*3*2=720).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Script to calculate factorials using a recursive function. 
version(1.0); 
 
number = 6; 
answer = factorial(number); 
echo ("Factorial of " & number & " is " & answer); 
 
# Function that recursively calculates the given number's factorial. 
# Params: num Number to calculate 
# Return: The given number's factorial. 
function factorial(num) 
{ 
    if (num <= 1); 
    { 
        return 1; 
    } 
 
    if (num > 1); 
    { 
        ans = factorial(num-1); 
        return ans*num;  
    } 
} 

Note how line 20 inside the factorial() function recursively calls itself substituting num-1 for the original value of num. Care should always be given when writing recursive functions that they always have a 'get-out clause' enabling control to be returned from the function.

6. LandScript Programming Styles

6.1. LandScript as a Macro Language

Probably the simplest approach to using LandScript is as a way of repeating a sequence of actions that would otherwise be achieved using menu selection in LandSerf. This has the advantage of be reproducable and sharable between users, although it does not take advantage of some of the more sophisticated functionality of the language.

When used as a macro language, most of the LandScript code will be calls to the LandScript commands providing the values that would otherwise be entered via the LandSerf Graphical User Interface. So for example, to load a file, change its projection and colour table and save a copy of the edited object, the following commands could be issued:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Script to demonstrate simple macro processing.
version(1.0);

baseDir = "/data/testdata/";

# Open file.
rast = open(baseDir&"rast.srf");

# Add projection metadata and reproject.
edit(rast,"projection","LatLong");
edit(rast,"ellipsoid","WGS 84");
newRast = reproject(rast,"UTM","true",30,30);

# Provide new colour table
colouredit(newRast,"land3");

# Save the new file.
save(newRast,baseDir&"rastUTM.srf");

6.2. LandScript as a Map Algebra Calculator

Map algebra is an attempt to formalise the way we can combine and process spatial data. The concept comes from the work of Dana Tomlin (Tomlin 1983, 1990) who proposed a platform independent way of recording spatial processing. The concept is a simple one: represent spatial objects as algebraic terms and allow those terms to be manipulated using mathematical and logical operators.

For example, suppose we had two elevation models of the same area, one generated via remote sensing that represents the upper surface of a region (which we will call DSM), the other generated via the interpolation of contours (which we will call DGM). The difference in height between these two models would give us the height of all the features, such as trees and hedges, that sit on top of DGM and have been recorded in the DSM. In map algebra terms, this can simply be represented as

Surface features = DSM - DGM
Vegetation = DSM - DGM

All map algebra operations can be expressed in the form

newObject = f([Object1], [Object2], [Object3]...)

In other words new spatial objects are created as a function of existing objects. These objects may be raster or vector maps. Depending on what is used as input to a map algebra operation, two broad categories of function can be considered.

Local raster operations Local operations usually take input from at least two spatial objects. The output for any location is a function of the input objects at that same location. An example of a LandScript local operation applied to a pair of raster maps might be:
errorMap = sqrt((dem1-dem2)^2);
which creates a raster containing the root of the squared difference between two elevation models for each raster cell.
Local vector operations Applying local map algebra to vector maps works in a similar way except that the output map must first be created so that the vector objects that are to have new attributes may be defined. In other words, applying map algebra to vector objects has no effect on their geometry, but their attributes are a function of the input objects that share the same spatial location as the output objects. In LandScript, the centroid of each output object is used to define their locations.

Both raster and vector maps may be combined in a single local map algebra expression. So for example, the following expression effecitvely creates a rasterized version of combined vector maps summing their contents where the vector maps overlap.
raster = vect1 + vect2;
Focal operations Focal operations usually take input from several locations in a single spatial object. The output for any location is a function of the input object at points surrounding the output location. Such functions are often referred to as neighbourhood operations since they process the neighbourhood of location in order to generate output. LandScript can only apply focal operations to rasters and allows neighbouring raster cells to be identified using a focal modifier in square brackets containing row and column offsets. For example:
smoothedDEM = (dem[-1,-1] + dem[-1,0] + dem[1,0] +
               dem[-1,0]  + dem[0,0]  + dem[1,0] + 
               dem[1,-1]  + dem[1,0]  + dem[1,1])/9;
creates a raster where each cell is the average of an input raster's immediate neighbourhood.

More sophisticated use of focal modifiers can be made by combining them with conditional operations. In the example below, the nested ifselse commands in lines 22-26 copy all the non-null raster cells from one raster map to another as well as those that immediately border non-null cells. The result is a that islands are 'grown' by one cell. By using a local map algebra operation (line 30) to find the difference between the original and expanded rasters, an additional 'coastline' map containing only the new border cells can be created.

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
# Creates some fractal islands, then grows the coastlines by 1 pixel. 
version(1.0); 
 
baseDir = "/data/testdata/"; 
 
# Create empty raster. 
fracSurf = newraster(1000,1000,10,10,250,250,"Fractal"); 
 
# Fill raster with fractal surface. 
fractal(fracSurf,"2.01"); 
islands = new(fracSurf); 
 
# Use data range to get 'sea level'; 
maxi = info(fracSurf,"max"); 
mini = info(fracSurf,"min"); 
midPoint = mini + (maxi-mini)/2; 
 
# Flood all values below 'sea level' 
fracSurf = ifelse(fracSurf < midPoint, null(), fracSurf); 
 
# Grow coastline 
islands = ifelse(fracSurf != null(), fracSurf, 
             ifelse(fracSurf[0,-1] != null(), fracSurf[0,-1], 
                ifelse(fracSurf[0,1] != null(),  fracSurf[0,1], 
                   ifelse(fracSurf[-1,0] != null(), fracSurf[-1,0], 
                      ifelse(fracSurf[1,0] != null(), fracSurf[1,0],null()))))); 
 
# Find coastline by taking difference between original and grown islands. 
coastline = new(fracSurf); 
coastline = islands-fracSurf; 
 
# Save original and expanded islands. 
save(islands,baseDir & "islands.srf"); 
save(coastline,baseDir & "coastline.srf"); 

When creating a local map algebra expression, it is sometimes useful to be able to identify a complete spatial object rather than a particular cell value. This is especially so when including LandScript commands in an expression. For example, suppose we wished to add mean value of a raster to each cell in a raster. If we tried to do this with the following expression:

1
2
3
dem = open("myRaster.srf");

dem = dem + info(dem,"mean");

LandScript would generate the error: Expecting spatial object as parameter 1 in info but number provided. This is because second occurence of dem on the right hand side of the expression in line 3 is interpreted as being part of a local operator that would take each cell in turn and process its numeric value. What we actually intended was for the info() command to treat that particular instance of dem as a single raster rather than process each of its cells. To force this to happen, simply place an underscore character (_) after the name of the raster:

1
2
3
dem = open("myRaster.srf");

dem = dem + info(dem_,"mean");

In this particular example, it would have been more efficient to store the mean of the dem values once in its own variable rather than calculate the same mean for every cell in the raster:

1
2
3
dem = open("myRaster.srf");
mean = info(dem,"mean");
dem = dem + mean;

Keeping code efficient in map algebra expressions is important if you wish your scripts to execute in a reasonable length of time. But there are some circumstances when the use of the underscore character to stop local map algebra operations being performed is necessary. For example, calling a LandScript command whose output is to replace one of the input parameters in that expression:

1
raster = reproject(raster_,"UTM","true",50,50);

Without the underscore, LandScript would incorrectly interpret this expression as a local operator as it contains a spatial object variable (raster) on the left and right hand side of the equals symbol.

Finally, the special variables easting, northing, row and col can be used in map algebra expressions to substitute the location coordinates of each cell in a spatial object into the expression itself. So in the example below, a mathematical function based on the (x,y) coordinates of each cell (line 7) is used to create a new surface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Script to create a Nyquist sampling function. 
version(1.0); 
 
baseDir = "/data/testdata/"; 
 
nyquist = newraster(-100,-100,1,1,201,201); 
nyquist = 10* (sin(easting/10) / (easting/10)) * (sin(northing/10) /(northing/10)); 
 
# Add metadata. 
edit(nyquist,"title","Nyquist"); 
edit(nyquist,"notes", 
             "Nyquist sampling model based on the 2-d sinC function "& 
             "((sin PI x / PI x)(sin PI y / PI y) )");  
edit(nyquist,"type","elevation"); 
colouredit(nyquist,"land4"); 
 
# Save the new raster map. 
save(nyquist,baseDir&"nyquist.srf"); 

Nyquist sampling function
Nyquist sampling function produced by landscript.

6.3. LandScript as a Lower Level Programming Language

There may be some special cases where map algebra expressions do not provide sufficient control over operations to produce the desired output. In such circumstances, it is possible to use LandScript looping and conditional contructions with LandScript commands to process spatial objects in a low level fashion.

The disadvantage of this approach is that it can be considerably slower than using map algebra, and certainly much slower than using the LandSerf API to code a routine in Java or some other low level language.

Most lower level processing of a raster object will need to use two nested loops to select each raster cell in turn. The example below shows how this can be achieved in LandScript. Note that this particualar example simply does what would more readly be achieved with the single map algebra expression newRast = rast1 + rast2;

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
# Script to add the contents of two rasters.
version(1.0);

baseDir = "/Users/jwo/data/testData/";

# Create the two input rasters and one output raster. 
rast1 = newraster(0,0,1,1,100,100,"Fractal 1"); 
fractal(rast1,"2.01"); 

rast2 = newraster(0,0,1,1,100,100,"Fractal 2"); 
fractal(rast2,"2.01"); 

newRast = new (rast1);
edit(newRast,"title","Sum of rast1 and rast2");

# Store dimensions of the rasters
numRows = info(newRast,"numRows");
numCols = info(newRast,"numCols");

# Process each raster cell individually.
r=0;
while (r < numRows);
{
  c=0;
  while (c < numCols);
  {
    z1 = rvalueat(rast1,r,c);
    z2 = rvalueat(rast2,r,c);
    rvalueat(newRast,r,c,z1+z2);
    c = c+1;
  }
  r = r +1;
}

# Save new raster.
colouredit(newRast,"grey1");
save(newRast,baseDir&"newRast.srf");

The key to being able to extract individual raster values is the landsript command rvalueat() which is able to extract the raster value at a given row and column coordinate (lines 27 and 28 above) as well as set a new raster value for a given cell (line 29 above). By calling this command inside two nested loops that process each column in the rasters row by row, all the raster cells may be processed.

The code above is not particuarly useful in that is simply replicates the process more easily achieved with map algebra. However, for more sophisticated processing, such as recursive traversal of a raster, this approach can be useful:

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
# Script to calculate flow magnitude of a DEM.
version(1.0);

basedir = "/data/testdata/";

# Initialise flow magnitude and drainage basin rasters.
dem = open(baseDir&"newRast.srf");

aspect = surfparam(dem,"aspect",3);
flowMag = new(dem);
basins = new(dem);
basinID = 0;
edit(flowMag,"title","Flow magnitude");
edit(flowMag,"type","other");
edit(basins,"title","Drainage basins");
edit(basins,"type","other");

numRows = info(dem,"numRows");
numCols = info(dem,"numCols");
r=1;

while (r < numRows);
{
  echo("Processing row " & r);
  c=1;
  while (c < numCols);
  {
    basinID = r*numCols+c;
    localFlow = calcFlowMag(r,c);
    rvalueat(flowMag,r,c,localFlow);
    c = c +1;
  }
  r = r +1;
}

# Provide logarithmic colour scale for flow magnitude.
colouredit(flowMag,"exp1");

# And a random colour table for drainage basins.
colouredit(basins,"random");

# Save flow magnitude and drainage basins.
save(flowMag,baseDir&"demFlowMag.srf");
save(basins,baseDir&"demBasins.srf");

endTime = datetime();
echo ("Completed in "&(endTime-startTime)/10 &"seconds.");

# Recursive flow magnitude function.
function calcFlowMag(r,c)
{
  # Check we haven't been here before
  visitedCell = rvalueat(basins,r,c);
  if (visitedCell == basinID);
  {
    # We have already visited this cell during this pass.
    return 0;
  }
  
  flow = 1;

  # Log this cell as belonging to the drainage basin.
  rvalueat(basins,r,c,basinID);

  # Stop if we have reached the edge.
  if ((r==0) or (c == 0) or (r >= numRows-1) or (c >= numCols-1));
  {
    return flow;
  }

  # Look for neighbours that might flow into this cell.
  aspVal = rvalueat(aspect,r-1,c);
  
  if ((aspVal >135) and (aspVal <=215));
  {
     fl = calcFlowMag(r-1,c);
     flow = flow +fl;
  }

  aspVal = rvalueat(aspect,r+1,c);
  if ((aspVal >305) or ((aspVal <=45) and (aspVal != null())));
  {
     fl = calcFlowMag(r+1,c);
     flow = flow +fl;
  }

  aspVal = rvalueat(aspect,r,c-1);
  if ((aspVal >45) and (aspVal <= 135));
  {
     fl = calcFlowMag(r,c-1);
     flow = flow +fl;
  }

  aspVal = rvalueat(aspect,r,c+1);
  if ((aspVal >215) and (aspVal <= 305));
  {
     fl = calcFlowMag(r,c+1);
     flow = flow +fl;
  }

  return flow;
}

References

Brun, H.T. and Egenhofer, M.J. (1997) User interfaces for map algebra. Journal of the Urban and Regional Information Systems Association, 9(1), pp.44-54

Tomlin, C.D. (1983) A map algebra. Proceedings of the 1983 Harvard Computer Graphics Conference, Cambridge MA

Tomlin, C.D. (1990) Geographic Information Systems and Cartographic Modelling. Englewood Cliffs, HJ: Prentice-Hall