R/utils-shiny.R

Defines functions getInfolocsColumns isRaster isVector findGeoType getLayers setTimeInput updateNumericTimeInput getTrajDefaults getFullTraj getBurstGeom getAnimalsDf getBurstsDF getStepWindow createShinyBurstsView createShinyStepsView

Documented in createShinyBurstsView createShinyStepsView findGeoType getAnimalsDf getBurstGeom getBurstsDF getFullTraj getInfolocsColumns getLayers getStepWindow getTrajDefaults isRaster isVector setTimeInput updateNumericTimeInput

#' Create a materialized view of the steps of a pgtraj for the shiny app
#' 
#' Steps are projected to EPSG:4326 thus there is no need for coordinate transformation
#' for leaflet.
#'
#' @param conn DBI::DBIConnection
#' @param schema String. Schema name.
#' @param pgtraj String. Pgtraj name.
#'
#' @return nothing
#'
#' @author Balázs Dukai \email{balazs.dukai@@gmail.com}
#' @keywords internal
createShinyStepsView <- function(conn, schema, pgtraj) {
    
    schema_q <- dbQuoteIdentifier(conn, schema)
    pgtraj_s <- dbQuoteString(conn, pgtraj)
    view <-
        dbQuoteIdentifier(conn, paste0("step_geometry_shiny_", pgtraj))
    
    infolocs_table <- paste0("infolocs_", pgtraj)
    info_cols <- getInfolocsColumns(conn, schema, pgtraj, df=TRUE)
    
    # if there is an infolocs table
    if (nrow(info_cols) > 0) {
        cols <- paste(paste(paste0(
            "i.",
            dbQuoteIdentifier(conn, info_cols$column_name)
        ),
        collapse = ", "),
        ",")
        join <-
            paste0("JOIN ",schema_q,".", infolocs_table, " i ON p.step_id = i.step_id")
    } else {
        cols <- NULL
        join <- NULL
    }
    
    # Stop in case the relocations are not projected, because Leaflet cannot plot them
    sql_query <-
        paste0("SELECT proj4string FROM ",schema_q,".pgtraj WHERE pgtraj_name = ",
               pgtraj_s,
               ";")
    srid <- dbGetQuery(conn, sql_query)$proj4string
    if (is.na(srid)) {
        stop("Cannot plot unprojected geometries (0 SRID). Not creating MATERIALIZED VIEW.")
    }
    
    sql_query <- paste0("
                        CREATE MATERIALIZED VIEW IF NOT EXISTS ",schema_q,".", view, " AS
                        SELECT
                        p.step_id,
                        st_transform(st_makeline(r1.geom, r2.geom), 4326)::geometry(LineString,4326) AS step_geom,
                        r1.relocation_time AS date,
                        p.dx,
                        p.dy,
                        p.dist,
                        p.dt,
                        p.abs_angle,
                        p.rel_angle,
                        ",cols,"
                        p.animal_name,
                        p.burst AS burst_name,
                        p.pgtraj AS pgtraj_name
                        FROM ",schema_q,".parameters_",pgtraj," p
                        JOIN ",schema_q,".step s ON p.step_id = s.id
                        JOIN ",schema_q,".relocation r1 ON s.relocation_id_1 = r1.id
                        JOIN ",schema_q,".relocation r2 ON s.relocation_id_2 = r2.id
                        ",join,"
                        WHERE st_makeline(r1.geom, r2.geom) NOTNULL;
                        
                        CREATE
                        INDEX IF NOT EXISTS step_geometry_shiny_", pgtraj, "_date_idx ON
                        ",schema_q,".", view, "
                        USING btree(date);
                        
                        CREATE
                        INDEX IF NOT EXISTS step_geometry_shiny_", pgtraj, "_step_geom_idx ON
                        ",schema_q,".", view, "
                        USING gist(step_geom);")
    
    create_sql_query <- gsub(pattern = '\\s', replacement = " ",
                             x = sql_query)
    
    tryCatch({
        dbExecute(conn, create_sql_query)
        message(paste0("MATERIALIZED VIEW step_geometry_shiny_",pgtraj,
                       " created in schema '",
                       schema, "'."))
        
        dbVacuum(conn, name = c(schema, paste0("step_geometry_shiny_",pgtraj)),
                 analyze = TRUE)
    }, warning = function(x) {
        message(x)
        message(". Cannot CREATE MATERIALIZED VIEW")
        stop("Returning from function")
        
    }, error = function(x) {
        message(x)
        message(". Cannot CREATE MATERIALIZED VIEW")
        stop("Returning from function")
    })
}


#' Create a materialized view of all bursts for the shiny app
#'
#' It is expected that *all* pgtrajes are projected in the schema in order to
#' run. Bursts are projected to EPSG:4326 thus there is no need for coordinate transformation
#' for leaflet.
#'
#' @param conn DBI::DBIConnection
#' @param schema String. Schema name.
#'
#' @return nothing
#'
#' @author Balázs Dukai \email{balazs.dukai@@gmail.com}
#' @keywords internal
createShinyBurstsView <- function(conn, schema) {
    
    schema_q <- dbQuoteIdentifier(conn, schema)
    view <- dbQuoteIdentifier(conn, "all_burst_summary_shiny")
    
    # Stop in case the relocations are not projected, because Leaflet cannot plot them
    sql_query <- paste0("SELECT proj4string FROM ",schema_q,".pgtraj LIMIT 1;")
    srid <- dbGetQuery(conn, sql_query)$proj4string
    if (is.na(srid)) {
        stop("Cannot plot unprojected geometries (0 SRID). Not creating MATERIALIZED VIEW.")
    }
    
    sql_query <- paste0("
                        CREATE
                        MATERIALIZED VIEW IF NOT EXISTS ",schema_q,".", view, " AS SELECT
                        p.id AS pgtraj_id,
                        p.pgtraj_name,
                        ab.animal_name,
                        ab.burst_name,
                        COUNT( r.id ) AS num_relocations,
                        COUNT( r.id )- COUNT( r.geom ) AS num_na,
                        MIN( r.relocation_time ) AS date_begin,
                        MAX( r.relocation_time ) AS date_end,
                        st_transform(
                        st_makeline(r.geom),
                        4326
                        )::geometry(
                        LineString,
                        4326
                        ) AS burst_geom
                        FROM
                        ",schema_q,".pgtraj p,
                        ",schema_q,".animal_burst ab,
                        ",schema_q,".relocation r,
                        ",schema_q,".s_b_rel sb,
                        ",schema_q,".step s
                        WHERE
                        p.id = ab.pgtraj_id
                        AND ab.id = sb.animal_burst_id
                        AND sb.step_id = s.id
                        AND s.relocation_id_1 = r.id
                        GROUP BY
                        p.id,
                        p.pgtraj_name,
                        ab.id,
                        ab.animal_name,
                        ab.burst_name
                        ORDER BY
                        p.id,
                        ab.id;
                        
                        CREATE
                        INDEX IF NOT EXISTS all_burst_summary_shiny_burst_name_idx ON
                        ",schema_q,".all_burst_summary_shiny USING btree(burst_name);")
    
    create_sql_query <- gsub(pattern = '\\s', replacement = " ",
                             x = sql_query)
    
    tryCatch({
        dbExecute(conn, create_sql_query)
        message(paste0("MATERIALIZED VIEW all_burst_summary_shiny created in schema '",
                       schema, "'."))
        dbVacuum(conn, name = c(schema, "all_burst_summary_shiny"), analyze = TRUE)
    }, warning = function(x) {
        message(x)
        message(" . Cannot CREATE MATERIALIZED VIEW")
        stop("Returning from function")
        
    }, error = function(x) {
        message(x)
        message(". Cannot CREATE MATERIALIZED VIEW")
        stop("Returning from function")
    })
    
}


#' Get steps within a temporal window
#'
#' @param conn DBI::DBIConnection
#' @param schema String. Schema name.
#' @param view String. View name.
#' @param time String of the start time of the time window. Including time zone.
#' @param interval lubridate::lubridate::period object of the time window
#' @param step_mode Boolean. Detailed step info (TRUE) or aggregate
#' @param info_cols Character vector of the infolocs columns of the pgtraj.
#' @param tstamp_start POSIXct with timestamp. First time stamp in view.
#' @param tstamp_last POSIXct with timestamp. Last time stamp in view.
#'
#' @return A simple feature object of the steps. NULL when out of range.
#' 
#' @author Balázs Dukai \email{balazs.dukai@@gmail.com}
#' @keywords internal
getStepWindow <- function(conn, schema, view, time, interval, step_mode,
                          info_cols, tstamp_start, tstamp_last){
    stopifnot(lubridate::is.period(interval))
    i <- lubridate::period_to_seconds(interval)
    t <- dbQuoteString(conn, format(time, usetz = TRUE))
    t_interval <- dbQuoteString(conn, paste(i, "seconds"))
    schema_q <- dbQuoteIdentifier(conn, schema)
    view_q <- dbQuoteIdentifier(conn, view)
    
    # if((time < tstamp_start | time > tstamp_last) |
    #    (i < 1)) {
    #     message("time window out of range")
    #     return(NULL)
    # }
    
    if(step_mode){
        sql_query <- paste0("
                            SELECT
                            step_id,
                            step_geom,
                            date,
                            dx,
                            dy,
                            dist,
                            dt,
                            abs_angle,
                            rel_angle,
                            ",info_cols,"
                            animal_name,
                            burst_name,
                            pgtraj_name
                            FROM ", schema_q, ".", view_q, " a
                            WHERE a.date >= ",t,"::timestamptz
                            AND a.date < (",t,"::timestamptz + ",
                            t_interval, "::INTERVAL)
                            AND a.step_geom IS NOT NULL;")
    } else {
        sql_query <- paste0("
                            SELECT
                            st_makeline(step_geom)::geometry(
                            linestring,
                            4326
                            ) AS step_geom,
                            burst_name,
                            animal_name
                            FROM
                            ", schema_q, ".", view_q, "
                            WHERE
                            date >= ",t,"::timestamptz
                            AND date < (",t,"::timestamptz + ",
                            t_interval, "::INTERVAL)
                            GROUP BY
                            burst_name, animal_name;")
    }
    withCallingHandlers(
        s <- sf::st_read_db(conn, query = sql_query, geom_column = "step_geom"),
        warning = function(w) {
            warning(paste("Didn't find any steps at", time, "+", interval))
        }
    )
    
    return(s)
    }

#' Get distinct burst names in a step_geometry view
#'
#' @param conn DBI::DBIConnection
#' @param schema String. Schema name.
#' @param view String. View name.
#'
#' @return data frame with column 'burst_name'
#' 
#' @author Balázs Dukai \email{balazs.dukai@@gmail.com}
#' @keywords internal
getBurstsDF <- function(conn, schema, view){
    schema_q <- dbQuoteIdentifier(conn, schema)
    view_q <- dbQuoteIdentifier(conn, view)
    sql_query <- paste0("
                        SELECT
                        DISTINCT burst_name
                        FROM
                        ",schema_q,".", view_q,";")
    return(dbGetQuery(conn, sql_query))
}

#' Get distinct animal names in step_geometry view
#'
#' @param conn DBI::DBIConnection
#' @param schema String. Schema name.
#' @param view String. View name.
#'
#' @return data frame with column 'animal_name'
#'
#' @author Balázs Dukai \email{balazs.dukai@@gmail.com}
#' @keywords internal
getAnimalsDf <- function(conn, schema, view){
    schema_q <- dbQuoteIdentifier(conn, schema)
    view_q <- dbQuoteIdentifier(conn, view)
    sql_query <- paste0("
                        SELECT
                        DISTINCT animal_name
                        FROM
                        ",schema_q,".", view_q,";")
    return(dbGetQuery(conn, sql_query))
}

#' Get geometry of bursts as linestring
#'
#' @param conn DBI::DBIConnection
#' @param schema String. Schema name.
#' @param view String. View name.
#' @param burst_name String. Accepts a character vector of variable length
#'
#' @return a single LINESTRING per burst
#'
#' @author Balázs Dukai \email{balazs.dukai@@gmail.com}
#' @keywords internal
getBurstGeom <- function(conn, schema, view, burst_name){
    
    if (is.null(burst_name) | length(burst_name) == 0){
        return()
    } else if (length(burst_name) == 1) {
        burst_sql <- dbQuoteString(conn, burst_name)
    } else if (length(burst_name) > 1) {
        sql_array <- paste(burst_name, collapse = "','")
        burst_sql <- paste0("ANY(ARRAY['",sql_array,"'])")
    }
    
    schema_q <- dbQuoteIdentifier(conn, schema)
    view_q <- dbQuoteIdentifier(conn, view)
    
    sql_query <- paste0("
                        SELECT *
                        FROM ", schema_q, ".all_burst_summary_shiny
                        WHERE burst_name = ", burst_sql, ";")
    
    return(sf::st_read_db(conn, query=sql_query, geom_column = "burst_geom"))
}

#' Get the complete trajectory of an animal as a single linestring
#'
#' @param conn DBI::DBIConnection
#' @param schema String. Schema name.
#' @param view String. View name.
#'
#' @return a single LINESTRING per animal
#'
#' @author Balázs Dukai \email{balazs.dukai@@gmail.com}
#' @keywords internal
getFullTraj <- function(conn, schema, view){
    sql_query <- paste0("
                        SELECT
                        st_makeline(step_geom)::geometry(linestring, 4326) AS traj_geom,
                        animal_name
                        FROM ", schema, ".", view, "
                        GROUP BY animal_name;")
    return(sf::st_read_db(conn, query=sql_query, geom_column = "traj_geom"))
}

#' Get default time parameters for steps
#'
#' @param conn DBI::DBIConnection
#' @param schema String. Schema name.
#' @param view String. View name.
#' @param pgtraj String. Pgtraj name
#'
#' @return data frame with columns: tstamp_start (epoch), tstamp_last (epoch), increment, tzone
#' 
#' @author Balázs Dukai \email{balazs.dukai@@gmail.com}
#' @keywords internal
getTrajDefaults <- function(conn, schema, view, pgtraj){
    schema_q <- dbQuoteIdentifier(conn, schema)
    view_q <- dbQuoteIdentifier(conn, view)
    sql_query <- paste0("
                        SELECT time_zone
                        FROM ", schema, ".pgtraj
                        WHERE pgtraj_name = ", dbQuoteString(conn, pgtraj),
                        ";")
    tzone <- dbGetQuery(conn, sql_query)
    
    # default increment is the median step duration
    sql_query <- paste0("
                        SELECT
                        MIN( DATE ) AS tstamp_start,
                        MAX( DATE ) AS tstamp_last,
                        PERCENTILE_CONT( 0.5 ) WITHIN GROUP(
                        ORDER BY
                        dt
                        ) AS increment
                        FROM ",schema_q,".", view_q,";")
    
    time_params <- dbGetQuery(conn, sql_query)
    
    return(cbind(time_params, tzone))
}

#' Convert the value of input$interval/increment to the unit selected in input$*_unit
#'
#' @param session Shiny session
#' @param inputUnit String. One of years, months, days, hours, minutes seconds
#' @param inputId String. Id of the input slot.
#' @param reactiveTime A lubridate::lubridate::period object stored in a Reactive Value
#'
#' @return nothing
#'
#' @author Balázs Dukai \email{balazs.dukai@@gmail.com}
#' @keywords internal
updateNumericTimeInput <-
    function(session, inputUnit, inputId, reactiveTime) {
        if (inputUnit == "years") {
            shiny::updateNumericInput(session, inputId,
                                      value = reactiveTime@year)
        } else if (inputUnit == "months") {
            shiny::updateNumericInput(session, inputId,
                                      value = reactiveTime@month)
        } else if (inputUnit == "days") {
            shiny::updateNumericInput(session, inputId,
                                      value = reactiveTime@day)
        } else if (inputUnit == "hours") {
            shiny::updateNumericInput(session, inputId,
                                      value = reactiveTime@hour)
        } else if (inputUnit == "minutes") {
            shiny::updateNumericInput(session, inputId,
                                      value = reactiveTime@minute)
        } else if (inputUnit == "seconds") {
            shiny::updateNumericInput(session, inputId,
                                      value = reactiveTime@.Data)
        }
    }

#' Set a lubridate::lubridate::period value from input$interval/increment
#'
#' @param inputUnit String.
#' @param inputTime lubridate::lubridate::period
#' @param reactiveTime Reactive value to set
#'
#' @return nothing
#'
#' @author Balázs Dukai \email{balazs.dukai@@gmail.com}
#' @keywords internal
setTimeInput <- function(inputUnit, inputTime, reactiveTime) {
    if (inputUnit == "years") {
        reactiveTime <- lubridate::period(num = inputTime,
                                          units = "years")
    } else if (inputUnit == "months") {
        reactiveTime <- lubridate::period(num = inputTime,
                                          units = "months")
    } else if (inputUnit == "days") {
        reactiveTime <- lubridate::period(num = inputTime,
                                          units = "days")
    } else if (inputUnit == "hours") {
        reactiveTime <- lubridate::period(num = inputTime,
                                          units = "hours")
    } else if (inputUnit == "minutes") {
        reactiveTime <- lubridate::period(num = inputTime,
                                          units = "minutes")
    } else if (inputUnit == "seconds") {
        reactiveTime <- lubridate::period(num = inputTime,
                                          units = "seconds")
    }
    
    return(reactiveTime)
}

#' Get base layers from database
#'
#' Not implemented for rasters. Transforms coordinates to EPSG:4326. 
#'
#' @param conn DBI::DBIConnection
#' @param layers List. List of character vectors for each layer to include as a
#' base layer.
#'
#' @return list of simple features as \code{list(name=sf object, name2=sf object)}
#' @importFrom magrittr "%>%"
#'
#' @author Balázs Dukai \email{balazs.dukai@@gmail.com}
#' @examples
#' \dontrun{
#' layers <- list(c("schema1", "tableA"), c("schema2", "tableB"))
#' }
#' @keywords internal
getLayers <- function(conn, layers) {
    if(!is.list(layers)){
        stop("layers_vector must be a list")
    }
    geo_type <- findGeoType(conn, layers)
    base <- list()
    if (length(geo_type$vect) > 0) {
        for (l in seq_along(geo_type$vect)) {
            relation <-  geo_type$vect[[l]]
            # project to EPSG:4326 for simpler handling
            data <- sf::st_read_db(conn, table = relation) %>%
                sf::st_transform(4326)
            # check geometry type
            geom_type <- unique(sf::st_geometry_type(data))
            if(length(geom_type) > 1) {
                stop(paste("The layer", relation,
                           "contains geometries of type",
                           paste(geom_type, collapse = " and "),
                           ". Please cast the geometries into a single type."))
            } else if (grepl("multipoint", geom_type, ignore.case = TRUE)) {
                stop("Leaflet 1.1.0 doesn't support MULTIPOINT geometries. Please cast to POINT.")
            }
            # add layer name
            # attr(data, "name") <- t[2]
            base[relation[2]] <- list(data)
        }
    } else if (length(geo_type$rast) > 0) {
        for (l in seq_along(geo_type$rast)) {
            relation <- geo_type$rast[[l]]
            # data <- pgGetRast(conn, relation)
            # base[relation[2]] <- list(data)
            warning("raster layers not implemented yet")
        }
    } else {
        stop("Something went wrong in getLayers.")
    }
    return(base)
}

#' Figures out whether the provided database relation contains vector or raster data.
#'
#' Not implemented for rasters.
#'
#' @param conn DBI::DBIConnection
#' @param layers List. List of character vectors for each layer to include as a 
#' base layer. 
#'
#' @return List of lists of database relations as \code{list(vect = list(), rast = list())}.
#' @author Balázs Dukai \email{balazs.dukai@@gmail.com}
#'
#' @examples
#' \dontrun{
#' layers <- list(c("example_data", "county_subdiv"), c("public", "florida_dem"))
#' geo_type <- findGeoType(conn, layers)
#' geo_type$vect[[1]]
#' }
#' @keywords internal
findGeoType <- function(conn, layers) {
    stopifnot(is.list(layers))
    testthat::expect_true((length(layers) >= 1))
    # geo_type <- data.frame(name = character(), type = character(),
    #                        schema = character(), table = character(),
    #                        stringsAsFactors = FALSE)
    geo_type <- list(vect = list(), rast = list())
    for(i in seq_along(layers)) {
        layer <- layers[[i]]
        v <- isVector(conn, layer)
        r <- isRaster(conn, layer)
        if (v) {
            geo_type$vect <- append(geo_type$vect, layers[i])
        } else if (r) {
            geo_type$rast <- append(geo_type$rast, layers[i])
        } else {
            warning(paste("Couldn't find the table", paste(layer, collapse = ".")
                          , "in the database."))
        }
    }
    
    return(geo_type)
}

#' Does a table contain vector data?
#'
#' @param conn DBI::DBIConnection
#' @param layer String. As c(schema, table)
#'
#' @return Boolean
#'
#' @author Balázs Dukai \email{balazs.dukai@@gmail.com}
#' @keywords internal
isVector <- function(conn, layer) {
    sql_query <- paste0("SELECT *
                        FROM public.geometry_columns
                        WHERE f_table_schema = ",dbQuoteString(conn, layer[1]),"
                        AND f_table_name = ",dbQuoteString(conn, layer[2]),
                        ";")
    v <- suppressWarnings(dbGetQuery(conn, sql_query))
    if(nrow(v) > 0) {
        return(TRUE)
    } else {
        return(FALSE)
    }
}


#' Does a table contain raster data?
#'
#' @param conn DBI::DBIConnection
#' @param layer String. As c(schema, table)
#'
#' @return Boolean
#'
#' @author Balázs Dukai \email{balazs.dukai@@gmail.com}
#' @keywords internal
isRaster <- function(conn, layer) {
    sql_query <- paste0("SELECT *
                        FROM public.raster_columns
                        WHERE r_table_schema = ",dbQuoteString(conn, layer[1]),"
                        AND r_table_name = ",dbQuoteString(conn, layer[2]),
                        ";")
    r <- suppressWarnings(dbGetQuery(conn, sql_query))
    if(nrow(r) > 0) {
        return(TRUE)
    } else {
        return(FALSE)
    }
}


#' Get all columns in the infolocs table but the step_id
#' 
#' Gets all the columns names in the infoloc table of the pgtraj and parses
#' them for inserting into an SQL query, e.g.: "col1 ,col2, col2 ,"
#'
#' @param conn DBI::DBIConnection
#' @param schema String. Schema name.
#' @param pgtraj String. Pgtraj name.
#' @param df Boolean. Return a data frame or a string?
#'
#' @return character vector or NULL if there are no infolocs
#' 
#' @author Balázs Dukai \email{balazs.dukai@@gmail.com}
#' @keywords internal
getInfolocsColumns <- function(conn, schema, pgtraj, df=FALSE){
    schema_s <- dbQuoteString(conn, schema)
    table_s <- dbQuoteString(conn, paste0("infolocs_", pgtraj))
    
    sql_query <- paste0("
                        SELECT column_name
                        FROM information_schema.columns
                        WHERE table_schema = ",schema_s,"
                        AND table_name = ",table_s,"
                        AND column_name != 'step_id';")
    ic <- dbGetQuery(conn, sql_query)
    
    if(df) {
        return(ic)
    } else {
        if(nrow(ic) > 0) {
            info_cols <- paste(paste(ic$column_name, collapse = ", "), ",")
        } else {
            info_cols <- NULL
        }
        return(info_cols)
    }
}
mablab/rpostgisLT documentation built on May 10, 2020, 9:35 a.m.