2  Geographic data in Julia

using Pkg
Pkg.status()
Status `~/work/geocompjl/geocompjl/Project.toml`
  [c9ce4bd3] ArchGDAL v0.10.4
  [13f3f980] CairoMakie v0.12.11
  [a93c6f00] DataFrames v1.6.1
  [0703355e] DimensionalData v0.28.1 `https://github.com/rafaqz/DimensionalData.jl#main`
  [62cb38b5] GeoDataFrames v0.3.9
  [68eda718] GeoFormatTypes v0.4.2
  [cf35fbd7] GeoInterface v1.3.6
  [61d90e0f] GeoJSON v0.8.1
  [db073c08] GeoMakie v0.7.4
  [3251bfac] GeometryOps v0.1.12
  [a90b1aa1] LibGEOS v0.9.2
  [c94c279d] Proj v1.7.1
  [a3a2b9e3] Rasters v0.11.8 `https://github.com/asinghvi17/Rasters.jl#as/makie_and_cf`
  [2913bbd2] StatsBase v0.34.3
  [10745b16] Statistics v1.10.0
"output"

2.1 Introduction

using GeoDataFrames
df = GeoDataFrames.read("data/world.gpkg")
177ร—11 DataFrame
171 rows omitted
Row geom iso_a2 name_long continent region_un subregion type area_km2 pop lifeExp gdpPercap
IGeometrโ€ฆ String? String String String String String Float64 Float64? Float64? Float64?
1 Geometry: wkbMultiPolygon FJ Fiji Oceania Oceania Melanesia Sovereign country 19290.0 885806.0 69.96 8222.25
2 Geometry: wkbMultiPolygon TZ Tanzania Africa Africa Eastern Africa Sovereign country 9.32746e5 5.22349e7 64.163 2402.1
3 Geometry: wkbMultiPolygon EH Western Sahara Africa Africa Northern Africa Indeterminate 96270.6 missing missing missing
โ‹ฎ โ‹ฎ โ‹ฎ โ‹ฎ โ‹ฎ โ‹ฎ โ‹ฎ โ‹ฎ โ‹ฎ โ‹ฎ โ‹ฎ โ‹ฎ
175 Geometry: wkbMultiPolygon XK Kosovo Europe Europe Southern Europe Sovereign country 11230.3 1.8218e6 71.0976 8698.29
176 Geometry: wkbMultiPolygon TT Trinidad and Tobago North America Americas Caribbean Sovereign country 7737.81 1.35449e6 70.426 31181.8
177 Geometry: wkbMultiPolygon SS South Sudan Africa Africa Eastern Africa Sovereign country 6.24909e5 1.1531e7 55.817 1935.88
using CairoMakie
using GeoMakie

f, a, p = poly(df.geom)

2.1.1 Raster from scratch

In this section, we are going to demonstrate the creation of rasters from scratch. We will construct two small rasters, elev and grain, which we will use in examples later in the book. Unlike creating a vector layer (see ?sec-vector-layer-from-scratch), creating a raster from scratch is rarely needed in practice because aligning a raster with the proper spatial extent is challenging to do programmatically (โ€œgeoreferencingโ€ tools in GIS software are a better fit for the job). Nevertheless, the examples will be helpful to become more familiar with the Rasters.jl data structures.

using Rasters
import GeoFormatTypes as GFT

Conceptually, a raster is an array combined with georeferencing information, whereas the latter comprises:

  • A transformation matrix, containing the origin and resolution, thus linking pixel indices with coordinates in a particular coordinate system
  • A CRS definition, specifying the association of that coordinate system with the surface of the earth (optional)

Therefore, to create a raster, we first need to have an array with the values, and then supplement it with the georeferencing information. Letโ€™s create the arrays elev and grain. The elev array is a \(6 \times 6\) array with sequential values from 1 to 36. It can be created as follows using base Julia functions.

elev = reshape(UInt8(1):UInt8(36), (6, 6))
6ร—6 reshape(::UnitRange{UInt8}, 6, 6) with eltype UInt8:
 0x01  0x07  0x0d  0x13  0x19  0x1f
 0x02  0x08  0x0e  0x14  0x1a  0x20
 0x03  0x09  0x0f  0x15  0x1b  0x21
 0x04  0x0a  0x10  0x16  0x1c  0x22
 0x05  0x0b  0x11  0x17  0x1d  0x23
 0x06  0x0c  0x12  0x18  0x1e  0x24

The grain array represents a categorical raster with values 0, 1, 2, corresponding to categories โ€œclayโ€, โ€œsiltโ€, โ€œsandโ€, respectively. We will create it from a specific arrangement of pixel values, using reshape.

v = UInt8[
  1, 0, 1, 2, 2, 2, 
  0, 2, 0, 0, 2, 1, 
  0, 2, 2, 0, 0, 2, 
  0, 0, 1, 1, 1, 1, 
  1, 1, 1, 2, 1, 1, 
  2, 1, 2, 2, 0, 2
]
grain = reshape(v, (6, 6))
6ร—6 Matrix{UInt8}:
 0x01  0x00  0x00  0x00  0x01  0x02
 0x00  0x02  0x02  0x00  0x01  0x01
 0x01  0x00  0x02  0x01  0x01  0x02
 0x02  0x00  0x00  0x01  0x02  0x02
 0x02  0x02  0x00  0x01  0x01  0x00
 0x02  0x01  0x02  0x01  0x01  0x02

Note that in both cases, we are using the uint8 (unsigned integer in 8 bits, i.e., 0-255) data type, which is sufficient to represent all possible values of the given rasters (see ?tbl-numpy-data-types). This is the recommended approach for a minimal memory footprint.

What is missing now is the georeferencing information (see ?sec-using-rasterio). In this case, since the rasters are arbitrary, we also set up arbitrary dimension lookups for the x and y axes, where:

  • The origin (\(x_{min}\), \(y_{max}\)) is at -1.5,1.5
  • The raster resolution (\(delta_{x}\), \(delta_{y}\)) is 0.5,-0.5

We can add this information using rasterio.transform.from_origin, and specifying west, north, xsize, and ysize parameters. The resulting transformation matrix object is hereby named new_transform.

new_x = X(range(-1.5, step=0.5, length=6))
new_y = Y(range(1.0, step=-0.5, length=6))
Y 1.0:-0.5:-1.5

We can now construct a Raster object, from the elev array and the dimensions new_x and new_y:

elev_raster = Raster(elev, (new_x, new_y))
โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”‚ 6ร—6 Raster{UInt8,2} โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ dims โ”
  โ†“ X Sampled{Float64} -1.5:0.5:1.0 ForwardOrdered Regular Points,
  โ†’ Y Sampled{Float64} 1.0:-0.5:-1.5 ReverseOrdered Regular Points
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ raster โ”ค
  extent: Extent(X = (-1.5, 1.0), Y = (-1.5, 1.0))
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
  โ†“ โ†’     1.0     0.5     0.0    -0.5    -1.0    -1.5
 -1.5  0x01    0x07    0x0d    0x13    0x19    0x1f
 -1.0  0x02    0x08    0x0e    0x14    0x1a    0x20
 -0.5  0x03    0x09    0x0f    0x15    0x1b    0x21
  0.0  0x04    0x0a    0x10    0x16    0x1c    0x22
  0.5  0x05    0x0b    0x11    0x17    0x1d    0x23
  1.0  0x06    0x0c    0x12    0x18    0x1e    0x24

The raster can now be plotted in its coordinate system, passing the array elev along with the transformation matrix new_transform to rasterio.plot.show (Figure 2.1). The raster can now be plotted in its coordinate system, passing the array elev along with the transformation matrix new_transform to rasterio.plot.show (Figure 2.1).

plot(elev_raster)
Figure 2.1: Plot of the elev raster, a minimal example of a continuous raster, created from scratch

The grain raster can be plotted the same way, as we are going to use the same transformation matrix for it as well (Figure 2.2).

plot(Raster(grain, (new_x, new_y)))
Figure 2.2: Plot of the grain raster, a minimal example of a categorical raster, created from scratch

At this point, we have two rasters, each composed of an array and related dimension lookups. We can work with the raster using Rasters.jl by:

  • Keeping in mind that any other layer we use in the analysis is in the same CRS

Finally, to export the raster for permanent storage, along with the spatial metadata, we need to go through the following steps:

  1. Create a raster file (where we set the lookups and the CRS, among other settings)
  2. Write the array with raster values into the connection
  3. Close the connection

Donโ€™t worry if the code below is unclear; the concepts related to writing raster data to file will be explained in ?sec-data-output-raster. For now, for completeness, and also to use these rasters in subsequent chapters without having to re-create them from scratch, we just provide the code for exporting the elev and grain rasters into the output directory. In the case of elev, we do it as follows with the Rasters.write functions and methods of the Rasters.jl package.

write("output/elev.tif", Rasters.setcrs(elev_raster, GFT.EPSG(4326)); force = true)
"output/elev.tif"

Note that the CRS we (arbitrarily) set for the elev raster is WGS84, defined using crs=4326 according to the EPSG code.

Exporting the grain raster is done in the same way, with the only differences being the file name and the array we write into the connection.

write("output/grain.tif", Raster(grain, (new_x, new_y); crs = GFT.EPSG(4326)); force = true)
"output/grain.tif"

As a result, the files elev.tif and grain.tif are written into the output directory. We are going to use these small raster files later on in the examples (for example, ?sec-raster-subsetting).

Note that the transform matrices and dimensions of elev and grain are identical. This means that the rasters are overlapping, and can be combined into one two-band raster, processed in raster algebra operations (?sec-map-algebra), etc.