/
pgGetRast.R
284 lines (247 loc) · 10.6 KB
/
pgGetRast.R
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
## pgGetRast
##' Load raster from PostGIS database into R.
##'
##' Retrieve rasters from a PostGIS table into a \code{terra SpatRaster} object
##'
##'
##' Since version 1.5.0, this function retrieve SpatRaster objects from
##' \code{terra} package by default. The argument \code{returnclass} can be
##' used to return \code{raster} objects instead.
##'
##' The argument \code{bands} can take as argument:
##'
##' * The index of the desirable band (e.g. bands = 2 will fetch the second band
##' of the raster).
##'
##' * More than one index for several bands (e.g. bands = c(2,4) will return a
##' \code{SpatRaster} with two bands).
##'
##' * All bands in the raster (bands = TRUE).
##'
##'
##' @param conn A connection object to a PostgreSQL database
##' @param name A character string specifying a PostgreSQL schema and
##' table/view name holding the geometry (e.g., \code{name =
##' c("schema","table")})
##' @param rast Name of the column in \code{name} holding the raster object.
##' Defaults to "rast".
##' @param clauses character, optional SQL to append to modify select
##' query from table. Must begin with 'WHERE'.
##' @param bands Index number(s) for the band(s) to retrieve (defaults to 1).
##' The special case (\code{bands = TRUE}) returns all bands in the raster. See
##' also 'Details'
##' @param boundary \code{sf} object, \code{SpatVector} object, or numeric. If a
##' spatial object is provided, its bounding box will be used to select
##' the part of the raster to import. Alternatively, a numeric vector
##' (\code{c([top], [bottom], [right], [left])}) indicating the
##' projection-specific limits with which to clip the raster. If not value
##' is provided, the default \code{boundary = NULL} will return the
##' full raster.
##' @param returnclass 'terra' by default; or 'raster' for \code{raster} objects.
##' @param progress whether to show a progress bar (TRUE by default). The progress
##' bar mark the progress of reading bands from the database.
##'
##' @author David Bucklin \email{david.bucklin@@gmail.com} and Adrián Cidre
##' González \email{adrian.cidre@@gmail.com}
##' @importFrom terra rast ext crs crop
##' @importFrom sf st_crs st_bbox
##' @importFrom raster raster stack
##' @export
##' @return \code{SpatRaster}; \code{raster}; or \code{RasterStack} object
##' @examples
##' \dontrun{
##' pgGetRast(conn, c("schema", "tablename"))
##' pgGetRast(conn, c("schema", "DEM"), boundary = c(55,
##' 50, 17, 12))
##' }
pgGetRast <- function(conn, name, rast = "rast", bands = 1,
boundary = NULL, clauses = NULL,
returnclass = "terra", progress = TRUE) {
## Message
message("Since version 1.5 this function outputs SpatRaster objects by default. Use returnclass = 'raster' to return raster objects.")
## Check connection and PostGIS extension
dbConnCheck(conn)
if (!suppressMessages(pgPostGIS(conn))) {
stop("PostGIS is not enabled on this database.")
}
## Check and prepare the schema.name
name1 <- dbTableNameFix(conn, name)
nameque <- paste(name1, collapse = ".")
namechar <- gsub("'", "''", paste(gsub('^"|"$', '', name1), collapse = "."))
## Raster query name
rastque <- dbQuoteIdentifier(conn, rast)
## Fix user clauses
clauses2 <- sub("^where", "AND", clauses, ignore.case = TRUE)
## Check table exists and return error if it does not exist
tmp.query <- paste0("SELECT r_raster_column AS geo FROM raster_columns\n WHERE (r_table_schema||'.'||r_table_name) = '",
namechar, "';")
tab.list <- dbGetQuery(conn, tmp.query)$geo
if (is.null(tab.list)) {
stop(paste0("Table '", namechar, "' is not listed in raster_columns."))
} else if (!rast %in% tab.list) {
stop(paste0("Table '", namechar, "' raster column '", rast,
"' not found. Available raster columns: ", paste(tab.list,
collapse = ", ")))
}
## Check bands
tmp.query <- paste0("SELECT st_numbands(", rastque, ") FROM ", nameque, " WHERE ", rastque, " IS NOT NULL LIMIT 1;")
nbs <- 1:dbGetQuery(conn, tmp.query)[1,1]
if (isTRUE(bands)) {
bands <- nbs
} else if (!all(bands %in% nbs)) {
stop(paste0("Selected band(s) do not exist in PostGIS raster: choose band numbers between ", min(nbs), " and ", max(nbs), "."))
}
## Retrieve the SRID
tmp.query <- paste0("SELECT DISTINCT(ST_SRID(", rastque, ")) FROM ", nameque, " WHERE ", rastque, " IS NOT NULL;")
srid <- dbGetQuery(conn, tmp.query)
## Check if the SRID is unique, otherwise throw an error
if (nrow(srid) > 1) {
stop("Multiple SRIDs in the raster")
} else if (nrow(srid) < 1) {
stop("Database table is empty.")
}
## Retrieve the proj4string
p4s <- NA
# tmp.query <- paste0("SELECT proj4text AS p4s FROM spatial_ref_sys WHERE srid = ",
# srid$st_srid, ";")
tmp.query.sr <- paste0("SELECT r_proj4 AS p4s FROM ", nameque, ";")
try(db.proj4 <- dbGetQuery(conn, tmp.query.sr)$p4s, silent = TRUE)
# if db.proj4 doesnt exist (error because raster was not loaded using rpostgis)
if (!exists("db.proj4")) {
tmp.query.sr <- paste0("SELECT proj4text AS p4s FROM spatial_ref_sys WHERE srid = ",
srid$st_srid, ";")
db.proj4 <- dbGetQuery(conn, tmp.query.sr)$p4s
}
if (!is.null(db.proj4)) {
try(p4s <- terra::crs(db.proj4[1]), silent = TRUE)
}
if (is.na(p4s)) {
warning("Table SRID not found. Projection will be undefined (NA)")
}
## Check alignment of raster
tmp.query <- paste0("SELECT ST_SameAlignment(", rastque, ") FROM ", nameque, ";")
# needs postgis version 2.1+, so just try
al <- FALSE
try(al <- dbGetQuery(conn, tmp.query)[1,1])
if (!al) {
# get alignment from upper left pixel of all raster tiles
tmp.query <- paste0("SELECT min(ST_UpperLeftX(", rastque, ")) ux, max(ST_UpperLeftY(", rastque, ")) uy FROM ", nameque, ";")
aligner <- dbGetQuery(conn, tmp.query)
aq <- c("ST_SnapToGrid(", paste0(aligner[1,1],","), paste0(aligner[1,2],"),"))
} else {
aq <- NULL
}
## Get band function
get_band <- function(band) {
## Get raster information (bbox, rows, cols)
info <- dbGetQuery(conn, paste0("select
st_xmax(st_envelope(rast)) as xmax,
st_xmin(st_envelope(rast)) as xmin,
st_ymax(st_envelope(rast)) as ymax,
st_ymin(st_envelope(rast)) as ymin,
st_width(rast) as cols,
st_height(rast) as rows
from
(select st_union(",aq[1],rastque,",",aq[2],aq[3],band,") rast from ",nameque," ", clauses,") as a;"))
## Retrieve values of the cells
vals <- dbGetQuery(conn, paste0("select
unnest(st_dumpvalues(rast, 1)) as vals
from
(select st_union(",aq[1],rastque,",",aq[2],aq[3],band,") rast from ",nameque," ", clauses,") as a;"))$vals
rout <- terra::rast(nrows = info$rows, ncols = info$cols, xmin = info$xmin,
xmax = info$xmax, ymin = info$ymin, ymax = info$ymax,
crs = p4s, vals = vals)
return(rout)
}
## Get band with boundary function
get_band_boundary <- function(band) {
## Get info
info <- dbGetQuery(conn, paste0("select
st_xmax(st_envelope(rast)) as xmx,
st_xmin(st_envelope(rast)) as xmn,
st_ymax(st_envelope(rast)) as ymx,
st_ymin(st_envelope(rast)) as ymn,
st_width(rast) as cols,
st_height(rast) as rows
from
(select st_union(",aq[1],rastque,",",aq[2],aq[3],band,") rast from ",nameque, "\n
WHERE ST_Intersects(",
rastque, ",ST_SetSRID(ST_GeomFromText('POLYGON((", boundary[4],
" ", boundary[1], ",", boundary[4], " ", boundary[2],
",\n ", boundary[3], " ", boundary[2], ",", boundary[3],
" ", boundary[1], ",", boundary[4], " ", boundary[1],
"))'),", srid, "))", clauses2,") as a;"))
if (is.na(info$cols) & is.na(info$rows)) {
stop("No data found within geographic subset defined by 'boundary'.")
}
vals <- dbGetQuery(conn,paste0("select
unnest(st_dumpvalues(rast, 1)) as vals
from
(select st_union(",aq[1],rastque,",",aq[2],aq[3],band,") rast from ",nameque, "\n
WHERE ST_Intersects(",
rastque, ",ST_SetSRID(ST_GeomFromText('POLYGON((", boundary[4],
" ", boundary[1], ",", boundary[4], " ", boundary[2],
",\n ", boundary[3], " ", boundary[2], ",", boundary[3],
" ", boundary[1], ",", boundary[4], " ", boundary[1],
"))'),", srid, "))", clauses2,") as a;"))$vals
rout <- terra::rast(nrows = info$rows, ncols = info$cols,
xmin = info$xmn, xmax = info$xmx, ymin = info$ymn, ymax = info$ymx,
crs = p4s, vals = vals)
return(rout)
}
## Get raster
if (is.null(boundary)) {
## Get bands
if (progress) {
rout <- purrr::map(bands, get_band, .progress = "Reading bands")
} else {
rout <- purrr::map(bands, get_band)
}
rb <- terra::rast(rout)
## Else: when boundary is provided
} else {
## Bbox of terra and sf objects
if (inherits(boundary, "sf")) {
boundary <- sf::st_bbox(boundary)
boundary <- c(boundary[4], boundary[2], boundary[3], boundary[1])
} else if (inherits(boundary, "SpatVector")) {
boundary <- c(terra::ext(boundary)[4], terra::ext(boundary)[3],
terra::ext(boundary)[2], terra::ext(boundary)[1])
}
## Extent to clip the Rast
extclip <- terra::ext(boundary[4], boundary[3], boundary[2], boundary[1])
## Get bands
rout <- purrr::map(bands, get_band_boundary, .progress = "Reading bands")
rb <- terra::rast(rout)
}
## Set layer names
if ("band_names" %in% dbTableInfo(conn,name)$column_name) {
try({
ct <- 1
for (b in bands) {
lnm <- dbGetQuery(conn, paste0("SELECT DISTINCT band_names[",b,
"][1] as nm FROM ",nameque," ", clauses,";"))
names(rb)[ct] <- lnm$nm
ct <- ct + 1
}
})
}
# precise cropping
if (!is.null(boundary)) {
rb_final <- terra::crop(rb, extclip)
} else {
rb_final <- rb
}
# Output terra or raster
if (returnclass == "terra") {
return(rb_final)
} else if (returnclass == "raster") {
if (terra::nlyr(rb_final) == 1) {
return(raster::raster(rb_final))
} else {
return(raster::stack(rb_final))
}
} else {
stop("returnclass must be one of 'terra' or 'raster'")
}
}