R/maps.R

Defines functions get_vector_tiles query_tiles check_upload_status upload_tiles

Documented in check_upload_status get_vector_tiles query_tiles upload_tiles

#' Upload dataset to your Mapbox account
#'
#' @param input An `sf` object, or the path to the dataset to upload as a
#'   character string.
#' @param username Your Mapbox username
#' @param access_token Your Mapbox access token; must have secret scope
#' @param tileset_id The ID of the tileset in your Mapbox account
#' @param tileset_name The name of the tileset in your Mapbox account
#' @param keep_geojson Whether or not to keep the temporary GeoJSON used to
#'   generate the tiles (if the input is an `sf` object)
#' @param multipart Whether or not to upload to the temporary AWS staging bucket
#'   as a multipart object; defaults to `FALSE`.
#'
#' @examples \dontrun{
#'
#' # Example: create a tileset of median age for all United States Census tracts
#' # Requires setting a Mapbox secret access token as an environment variable
#'
#' library(mapboxapi)
#' library(tidycensus)
#' options(tigris_use_cache = TRUE)
#'
#' median_age <- get_acs(
#'   geography = "tract",
#'   variables = "B01002_001",
#'   state = c(state.abb, "DC"),
#'   geometry = TRUE
#' )
#'
#' upload_tiles(
#'   input = median_age,
#'   username = "kwalkertcu", # Your username goes here
#'   tileset_id = "median_age",
#'   tileset_name = "us_median_age_2014_to_2018"
#' )
#' }
#'
#' @export
upload_tiles <- function(input,
                         username,
                         access_token = NULL,
                         tileset_id = NULL,
                         tileset_name = NULL,
                         keep_geojson = FALSE,
                         multipart = FALSE) {
  access_token <- get_mb_access_token(access_token, secret_required = TRUE)

  # Allow input to be an `sf` object
  if (any(grepl("^sf", class(input)))) {
    input <- sf::st_transform(input, 4326)

    if (is.null(tileset_name)) {
      layer_name <- stringi::stri_rand_strings(1, 6)
    } else {
      layer_name <- tileset_name
    }

    if (keep_geojson) {
      outfile <- paste0(layer_name, ".geojson")

      path <- file.path(dir, outfile)

      sf::st_write(input, path, quiet = TRUE)
    } else {
      tmp <- tempdir()

      tempfile <- paste0(layer_name, ".geojson")

      path <- file.path(tmp, tempfile)

      sf::st_write(input, path, quiet = TRUE)
    }
  } else {
    path <- input
  }

  # If tileset_id is NULL, use the basename of the input
  if (is.null(tileset_id)) {
    tileset_id <- gsub("\\.[^.]*$", "", basename(path))
  }

  # Get AWS credentials
  base1 <- sprintf(
    "https://api.mapbox.com/uploads/v1/%s/credentials",
    username
  )

  req <- httr::POST(base1, query = list(access_token = access_token))

  credentials <- httr::content(req, as = "text") %>%
    jsonlite::fromJSON()

  # Use these credentials to transfer to the staging bucket
  aws.s3::put_object(
    file = path,
    object = credentials$key,
    bucket = credentials$bucket,
    region = "us-east-1",
    key = credentials$accessKeyId,
    secret = credentials$secretAccessKey,
    session_token = credentials$sessionToken,
    check_region = FALSE,
    multipart = multipart
  )

  # Once done, generate the upload
  url <- sprintf(
    "http://%s.s3.amazonaws.com/%s",
    credentials$bucket,
    credentials$key
  )

  if (is.null(tileset_name)) {
    upload <- sprintf(
      '{"url": "%s", "tileset": "%s.%s"}',
      url, username, tileset_id
    )
  } else {
    upload <- sprintf(
      '{"url": "%s", "tileset": "%s.%s", "name": "%s"}',
      url, username, tileset_id, tileset_name
    )
  }

  base2 <- sprintf(
    "https://api.mapbox.com/uploads/v1/%s",
    username
  )

  request <- httr::POST(base2,
    httr::add_headers(
      "Content-Type" = "application/json",
      "Cache-Control" = "no-cache"
    ),
    body = upload,
    query = list(access_token = access_token)
  )

  response <- request %>%
    httr::content(as = "text") %>%
    jsonlite::fromJSON()

  if (request$status_code != "201") {
    stop(sprintf("Upload failed: your error message is %s", response),
      call. = FALSE
    )
  }

  message(sprintf("Your upload ID is %s", response$id))
}


#' Check the status of a Mapbox upload
#'
#' @param upload_id The upload ID
#' @param username Your account's username
#' @param access_token Your Mapbox access token
#'
#' @export
check_upload_status <- function(upload_id,
                                username,
                                access_token = NULL) {
  access_token <-
    get_mb_access_token(
      access_token,
      default = "MAPBOX_SECRET_TOKEN",
      secret_required = TRUE
      )

  status <- httr::GET(
    sprintf(
      "https://api.mapbox.com/uploads/v1/%s/%s",
      username, upload_id
    ),
    query = list(access_token = access_token)
  )

  status %>%
    httr::content(as = "text") %>%
    jsonlite::fromJSON()
}


#' Get information about features in a tileset using the Tilequery API
#'
#' @param location The location for which you'd like to query tiles, expressed
#'   as either a length-2 vector of longitude and latitude or an address you'd
#'   like to geocode.
#' @param tileset_id The tileset ID to query.
#' @param radius The radius around the point (in meters) for which you'd like to
#'   query features. For point-in-polygon queries (e.g. "what county is my point
#'   located in?") the default of 0 should be used.
#' @param limit How many features to return (defaults to 5). Can be an integer
#'   between 1 and 50.
#' @param dedupe Whether or not to return duplicate features as identified by
#'   their IDs. The default, TRUE, will de-duplicate your dataset.
#' @param geometry The feature geometry type to query - can be `"point"`,
#'   `"linestring"`, or `"polygon"`. If left blank, all geometry types
#'   will be queried.
#' @param layers A vector of layer IDs you'd like to query (recommended); if
#'   left blank will query all layers, with the limitation that at most 50
#'   features can be returned.
#' @param access_token A Mapbox access token, which can be set with
#'   [mb_access_token()].
#'
#' @return An R list containing the API response, which includes information
#'   about the requested features. Parse the list to extract desired elements.
#' @seealso
#'   <https://docs.mapbox.com/help/tutorials/find-elevations-with-tilequery-api/>
#'
#'
#' @examples \dontrun{
#'
#' library(mapboxapi)
#'
#' elevation <- query_tiles(
#'   location = "Breckenridge, Colorado",
#'   tileset_id = "mapbox.mapbox-terrain-v2",
#'   layer = "contour",
#'   limit = 50
#' )
#'
#' max(elevation$features$properties$ele)
#' }
#'
#' @export
query_tiles <- function(location,
                        tileset_id,
                        radius = 0,
                        limit = 5,
                        dedupe = TRUE,
                        geometry = NULL,
                        layers = NULL,
                        access_token = NULL) {
  access_token <- get_mb_access_token(access_token)

  # If location is an address, geocode it
  if (length(location) == 1) {
    coords <- mb_geocode(location, access_token = access_token)
  } else if (length(location) == 2) {
    coords <- location
  } else {
    stop("The specified location must either be a
         coordinate pair or a valid address",
      call. = FALSE
    )
  }

  # Format the requested layers if included
  if (!is.null(layers)) {
    layers <- paste0(layers, collapse = ",")
  }

  # Format the dedupe param
  if (dedupe) {
    dedupe <- "true"
  } else {
    dedupe <- "false"
  }

  # Construct the JSON for the request
  base <- sprintf(
    "https://api.mapbox.com/v4/%s/tilequery/%s,%s.json",
    tileset_id, coords[1], coords[2]
  )

  # Build the request
  query <- httr::GET(base, query = list(
    radius = radius,
    limit = limit,
    dedupe = dedupe,
    geometry = geometry,
    layers = layers,
    access_token = access_token
  ))

  content <- httr::content(query, as = "text")

  if (query$status_code != 200) {
    pull <- jsonlite::fromJSON(content)
    stop(pull$message, call. = FALSE)
  }

  result <- jsonlite::fromJSON(content)

  return(result)
}


#' Retrieve vector tiles from a given Mapbox tileset
#'
#' @param tileset_id The name of the tileset ID; names can be retrieved from
#'   your Mapbox account
#' @param location The location for which you'd like to retrieve tiles. If the
#'   input is an `sf` object, the function will return data for all tiles that
#'   intersect the object's bounding box. If the input is a coordinate pair or
#'   an address, data will be returned for the specific tile that contains the
#'   input.
#' @param zoom The zoom level of the request; larger zoom levels will return
#'   more detail but will take longer to process.
#' @param access_token A Mapbox access token; which can be set with
#'   [mb_access_token()].
#'
#' @return A list of `sf` objects representing the different layer types found
#'   in the requested vector tiles.
#'
#' @examples \dontrun{
#'
#' library(mapboxapi)
#' library(ggplot2)
#'
#' vector_extract <- get_vector_tiles(
#'   tileset_id = "mapbox.mapbox-streets-v8",
#'   location = c(-73.99405, 40.72033),
#'   zoom = 15
#' )
#'
#' ggplot(vector_extract$building$polygons) +
#'   geom_sf() +
#'   theme_void()
#' }
#'
#' @export
get_vector_tiles <- function(tileset_id,
                             location,
                             zoom,
                             access_token = NULL) {
  access_token <- get_mb_access_token(access_token)

  # If location is an `sf` object, get the bbox and the tiles that intersect it
  if (any(grepl("^sf", class(location)))) {
    bbox <- location %>%
      sf::st_transform(4326) %>%
      sf::st_bbox(.)

    tile_grid <- slippymath::bbox_to_tile_grid(bbox = bbox, zoom = zoom)

    tile_ids <- tile_grid$tiles

    message(sprintf("Requesting data for %s map tiles. To speed up your query
                    choose a smaller extent or zoom level.", nrow(tile_ids)))
    sf_list <- purrr::map2(tile_ids$x, tile_ids$y, ~ {
      # Build the request to Mapbox
      url <- sprintf(
        "https://api.mapbox.com/v4/%s/%s/%s/%s.mvt",
        tileset_id, zoom, .x, .y
      )


      request <- httr::GET(url, query = list(access_token = access_token))


      # if (request$status_code != 200) {
      #   content <- httr::content(request, as = "text")
      #   stop(print(content$message), call. = FALSE)
      # }

      # Only try to read the data if there is data available
      if (request$status_code == 200) {
        sf_output <- protolite::read_mvt_sf(request$url)
        return(sf_output)
      }
    })

    # Now, combine the internal list elements by name
    # First, find the tile with the most elements and get the names
    max_ix <- purrr::map(sf_list, ~ length(.x)) %>% which.max()

    # Next, grab the names of that list element
    layer_names <- names(sf_list[[max_ix]])

    names(layer_names) <- layer_names

    # Now, iterate over the layer names, then the lists, keeping what you need
    # and combining
    master_list <- purrr::map(layer_names, function(name) {
      # print(name) # Leave here for de-bugging
      layer_list <- purrr::map(sf_list, ~ {
        if (name %in% names(.x)) {
          layer <- .x[[name]]
          # If layer comes through as mixed geometry, we need to parse it into
          # multiple list elements and return that
          if (sf::st_geometry_type(layer, by_geometry = FALSE) == "GEOMETRY") {
            sub_geoms <- list()
            mixed_geoms <- sf::st_geometry_type(layer, by_geometry = TRUE)
            if ("POINT" %in% mixed_geoms || "MULTIPOINT" %in% mixed_geoms) {
              point_ix <- mixed_geoms %in% c("POINT", "MULTIPOINT")
              sub_geoms$points <- layer[point_ix, ] %>%
                suppressWarnings(sf::st_cast("MULTIPOINT"))
            }
            if ("LINESTRING" %in% mixed_geoms || "MULTILINESTRING" %in% mixed_geoms) {
              line_ix <- mixed_geoms %in% c("LINESTRING", "MULTILINESTRING")
              sub_geoms$lines <- layer[line_ix, ] %>%
                sf::st_cast("MULTILINESTRING")
            }
            if ("POLYGON" %in% mixed_geoms || "MULTIPOLYGON" %in% mixed_geoms) {
              polygon_ix <- mixed_geoms %in% c("POLYGON", "MULTIPOLYGON")
              sub_geoms$polygons <- layer[polygon_ix, ] %>%
                sf::st_cast("MULTIPOLYGON")
            }

            return(sub_geoms)
          } else {
            if ("POLYGON" %in% sf::st_geometry_type(layer)) {
              # Remove malformed polygons
              layer$layer_area <- sf::st_area(layer) %>% as.numeric()
              layer <- dplyr::filter(layer, layer_area > 0)
              layer <- sf::st_cast(layer, "MULTIPOLYGON")
              layer <- dplyr::select(layer, -layer_area)
            } else if ("LINESTRING" %in% sf::st_geometry_type(layer)) {
              layer <- sf::st_cast(layer, "MULTILINESTRING")
            } else if ("POINT" %in% sf::st_geometry_type(layer)) {
              layer <- sf::st_cast(layer, "MULTIPOINT")
            }
            return(layer)
          }
        }
      }) %>%
        purrr::compact()
    })

    # Within the master list, we need to separate out by geometry type
    # We'll have a mix of points, lines, and polygons for some
    parsed_list <- purrr::map(layer_names, function(name) {
      # ID the specific segment by layer name then smooth out any mixed geoms
      segment <- master_list[[name]] %>%
        rlang::flatten_if(predicate = function(x) inherits(x, "list"))
      # identify how many geometries are in the list
      geoms <- purrr::map_int(segment, ~ {
        sf::st_geometry_type(.x, by_geometry = FALSE)
      })

      # If there is only one geometry, return the combined segment across the
      # tiles
      # Otherwise, we should give back points, lines, and polygons objects as
      # appropriate
      if (length(unique(geoms)) == 1) {
        return(dplyr::bind_rows(segment))
      } else if (length(unique(geoms)) > 1) {
        output <- list()
        if (2L %in% geoms || 1L %in% geoms || 5L %in% geoms) {
          point_ix <- geoms %in% c(1L, 2L, 5L)
          output$points <- segment[point_ix] %>%
            dplyr::bind_rows() %>%
            sf::st_cast("MULTIPOINT")
        }
        if (6L %in% geoms) {
          line_ix <- geoms == 6L
          output$lines <- segment[line_ix] %>% dplyr::bind_rows()
        }
        if (7L %in% geoms) {
          polygon_ix <- geoms == 7L
          output$polygons <- segment[polygon_ix] %>% dplyr::bind_rows()
        }
        return(output)
      }
    })

    return(parsed_list)
  }

  # If location is a length-2 numeric vector of longitude/latitude, get the
  # specific tile IDs
  # If location is an address/text description, geocode it
  if (is.vector(location)) {
    if (inherits(location, "numeric")) {
      if (length(location) != 2) {
        stop("Location must be a length-2 vector of format c(lon, lat) if
             supplying coordinates as input.")
      }
    } else if (inherits(location, "character")) {
      location <- mb_geocode(location)
    }

    tile_id <-
      slippymath::lonlat_to_tilenum(
        location[1], location[2], zoom = zoom
        )


    # Build the request to Mapbox
    url <- sprintf(
      "https://api.mapbox.com/v4/%s/%s/%s/%s.mvt",
      tileset_id, zoom, tile_id$x, tile_id$y
    )


    request <- httr::GET(url, query = list(access_token = access_token))


    if (request$status_code != 200) {
      content <- httr::content(request, as = "text")
      stop(print(content$message), call. = FALSE)
    }

    sf_list <- protolite::read_mvt_sf(request$url)

    # Iterate through the elements and parse into sub-elements when geometry is
    # mixed
    layer_names <- names(sf_list)

    names(layer_names) <- layer_names

    output_list <- purrr::map(layer_names, function(name) {
      layer <- sf_list[[name]]
      if (sf::st_geometry_type(layer, by_geometry = FALSE) == "GEOMETRY") {
        sub_geoms <- list()
        mixed_geoms <- sf::st_geometry_type(layer, by_geometry = TRUE)
        if ("POINT" %in% mixed_geoms || "MULTIPOINT" %in% mixed_geoms) {
          point_ix <- mixed_geoms %in% c("POINT", "MULTIPOINT")
          sub_geoms$points <- layer[point_ix, ] %>%
            suppressWarnings(sf::st_cast("MULTIPOINT"))
        }
        if ("LINESTRING" %in% mixed_geoms || "MULTILINESTRING" %in% mixed_geoms) {
          line_ix <- mixed_geoms %in% c("LINESTRING", "MULTILINESTRING")
          sub_geoms$lines <- layer[line_ix, ] %>%
            sf::st_cast("MULTILINESTRING")
        }
        if ("POLYGON" %in% mixed_geoms || "MULTIPOLYGON" %in% mixed_geoms) {
          polygon_ix <- mixed_geoms %in% c("POLYGON", "MULTIPOLYGON")
          sub_geoms$polygons <- layer[polygon_ix, ] %>%
            sf::st_cast("MULTIPOLYGON")
        }

        return(sub_geoms)
      } else {
        if ("POLYGON" %in% sf::st_geometry_type(layer)) {
          # Remove malformed polygons
          layer$layer_area <- sf::st_area(layer) %>% as.numeric()
          layer <- dplyr::filter(layer, layer_area > 0)
          layer <- sf::st_cast(layer, "MULTIPOLYGON")
          layer <- dplyr::select(layer, -layer_area)
        } else if ("LINESTRING" %in% sf::st_geometry_type(layer)) {
          layer <- sf::st_cast(layer, "MULTILINESTRING")
        } else if ("POINT" %in% sf::st_geometry_type(layer)) {
          layer <- sf::st_cast(layer, "MULTIPOINT")
        }
        return(layer)
      }
    })

    return(output_list)
  }
}


#' Return a static Mapbox map from a specified style
#'
#' This function uses the [Mapbox Static Maps
#' API](https://www.mapbox.com/static-maps) to return a pointer to an
#' `"magick-image"` class image or a [httr::response] object from the static map
#' image URL.
#'
#' @inheritParams get_static_tiles
#' @param buffer_dist The distance to buffer around an input `sf` object for
#'   determining static map, specified in units. If location is a POINT object
#'   of 2 rows or less and `buffer_dist` is 0 or `NULL`, a 1 unit buffer is
#'   applied to try to ensure the creation of a valid bounding box for the map
#'   area.
#' @param units Units of `buffer_dist`; defaults to "m" (meters). If buffer_dist
#'   is a units class object, the units argument is ignored.
#' @param style_id A style ID (required if style_url is `NULL`).
#' @param username A Mapbox username (required if `style_url = NULL`).
#' @param style_url A Mapbox style url; defaults to `NULL`.
#' @param overlay_sf The overlay `sf` object (optional). The function will
#'   convert the `sf` object to GeoJSON then plot over the basemap style.
#'   Spatial data that are too large will trigger an error, and should be added
#'   to the style in Mapbox Studio instead.
#' @param overlay_style A named list of vectors specifying how to style the sf
#'   overlay. Possible names are "stroke", "stroke-width" (or "stroke_width"),
#'   "stroke-opacity" (or "stroke_opacity"), "fill", and "fill-opacity" (or
#'   "fill_opacity"). The fill and stroke color values can be specified as
#'   six-digit hex codes or color names, and the opacity and width values should
#'   be supplied as floating-point numbers. If overlay_style is `NULL`, the
#'   style values can be pulled from columns with the same names in
#'   `overlay_sf`.
#' @param overlay_markers The prepared overlay markers (optional). See the
#'   function [prep_overlay_markers] for more information on how to specify a
#'   marker overlay.
#' @param longitude,latitude The longitude and latitude of the map center. If an
#'   overlay is supplied, the map will default to the extent of the overlay
#'   unless longitude, latitude, and zoom are all specified.
#' @param zoom The map zoom. The map will infer this from the overlay unless
#'   longitude, latitude, and zoom are all specified.
#' @param width,height The map width and height; defaults to `NULL`
#' @param pitch,bearing The map pitch and bearing; defaults to `NULL`. pitch can
#'   range from 0 to 60, and bearing from -360 to 360.
#' @param scale ratio to scale the output image; `scale = 1` will return the
#'   largest possible image. defaults to 0.5
#' @param scaling_factor The scaling factor of the tiles; either `"1x"`
#'   (the default) or `"2x"`
#' @param attribution Controls whether there is attribution on the image.
#'   Defaults to `TRUE`. If `FALSE`, the watermarked attribution is removed from
#'   the image. You still have a legal responsibility to attribute maps that use
#'   OpenStreetMap data, which includes most maps from Mapbox. If you specify
#'   `attribution = FALSE`, you are legally required to include proper
#'   attribution elsewhere on the webpage or document.
#' @param logo Controls whether there is a Mapbox logo on the image. Defaults to
#'   `TRUE`.
#' @param before_layer A character string that specifies where in the hierarchy
#'   of layer elements the overlay should be inserted. The overlay will be
#'   placed just above the specified layer in the given Mapbox styles. List
#'   layer ids for a map style with `get_style(style_id = style_id, username =
#'   username, style_url = style_url, access_token =
#'   access_token)[["layers"]][["id"]]`
#' @param access_token A Mapbox access token; which can be set with
#'   [mb_access_token].
#' @param image If `FALSE`, return the a [httr::response] object from
#'   [httr::GET] using the static image URL; defaults to `TRUE`.
#' @param strip If `TRUE`, drop image comments and metadata when `image = TRUE`;
#'   defaults to `TRUE`.
#' @return A pointer to an image of class `"magick-image"` if `image = TRUE`.
#'   The resulting image can be manipulated further with functions from the
#'   magick package.
#'
#' @examples \dontrun{
#'
#' library(mapboxapi)
#'
#' points_of_interest <- tibble::tibble(
#'   longitude = c(-73.99405, -74.00616, -73.99577, -74.00761),
#'   latitude = c(40.72033, 40.72182, 40.71590, 40.71428)
#' )
#'
#' prepped_pois <- prep_overlay_markers(
#'   data = points_of_interest,
#'   marker_type = "pin-l",
#'   label = 1:4,
#'   color = "fff"
#' )
#'
#' map <- static_mapbox(
#'   style_id = "streets-v11",
#'   username = "mapbox",
#'   overlay_markers = prepped_pois,
#'   width = 1200,
#'   height = 800
#' )
#'
#' map
#' }
#'
#' @export
#' @importFrom httr GET content
#' @importFrom jsonlite fromJSON
#' @importFrom magick image_read
static_mapbox <- function(location = NULL,
                          buffer_dist = 1000,
                          units = "m",
                          style_id,
                          username,
                          style_url = NULL,
                          overlay_sf = NULL,
                          overlay_style = NULL,
                          overlay_markers = NULL,
                          longitude = NULL,
                          latitude = NULL,
                          zoom = NULL,
                          width = NULL,
                          height = NULL,
                          bearing = NULL,
                          pitch = NULL,
                          scale = 0.5,
                          scaling_factor = c("1x", "2x"),
                          attribution = TRUE,
                          logo = TRUE,
                          before_layer = NULL,
                          access_token = NULL,
                          image = TRUE,
                          strip = TRUE) {
  access_token <- get_mb_access_token(access_token)

  base <-
    set_static_map_style(
      style_url = style_url,
      username = username,
      style_id = style_id
    )

  # Next, figure out the overlay
  # Basically, the idea is that you can string together GeoJSON, markers, etc.
  # on a map and put it over a Mapbox style. overlay should accept such
  # components and format them accordingly.
  #
  # `overlay_sf` converts to GeoJSON to make the request.
  # The input GeoJSON should have internal information conforming to
  # simplestyle-spec Eventually, this function could be able to do that
  # internally but not yet

  base <-
    set_static_map_overlay(
      base = base,
      overlay_sf = overlay_sf,
      overlay_style = overlay_style,
      overlay_markers = overlay_markers
    )

  merc_bbox <- location_to_bbox(location, buffer_dist, units, crs = 3857)
  lonlat_bbox <- location_to_bbox(location, buffer_dist, units, crs = 4326)

  focus_args <- c(longitude, latitude, zoom)

  if (all(is.null(focus_args))) {
    if (is.null(location)) {
      focus <- "auto"
    } else {
      focus <- paste0("[", paste0(lonlat_bbox, collapse = ","), "]")
    }
  } else {
    if (!is.null(location) && all(sapply(c(longitude, latitude), is.null))) {
      center <- bbox_to_center(lonlat_bbox, crs = 4326)
      longitude <- center[1]
      latitude <- center[2]
    }

    if (is.null(zoom)) {
      zoom <- 11
    }

    stopifnot(
      "`zoom` must be between 0 and 22" = (zoom >= 0) && (zoom <= 22)
    )

    focus_args <- c(longitude, latitude, zoom)

    stopifnot(
      all(!is.null(focus_args))
    )

    if (is.null(bearing)) {
      bearing <- 0
    } else if ((bearing < 0) && (bearing >= -360)) {
      bearing <- 360 + bearing
    }

    if (is.null(pitch)) {
      pitch <- 0
    }

    stopifnot(
      "`bearing` must be between -360 and 360" = ((bearing >= 0) && (bearing <= 360)),
      "`pitch` must be between 0 and 60" = ((pitch >= 0) && (pitch <= 60))
    )

    focus_args <- c(focus_args, bearing, pitch)

    focus <- paste0(focus_args, collapse = ",")
  }

  base <- paste(base, focus, sep = "/")

  base1 <-
    set_static_map_dims(
      base = base,
      bbox = merc_bbox,
      width = width,
      height = height,
      scale = scale
    )

  scaling_factor <- match.arg(scaling_factor)

  if (scaling_factor == "2x") {
    base1 <- sprintf("%s@2x", base1)
  }

  if (nchar(base1) > 8192) {
    stop("Your request is too large likely due to the size of your overlay
         geometry. Consider simplifying your overlay geometry or adding your
         data to a style in Mapbox Studio to resolve this.", call. = FALSE)
  }

  if (!is.null(before_layer)) {
    before_layer <-
      match.arg(
        before_layer,
        get_style(
          style_id = style_id,
          username = username,
          style_url = style_url,
          access_token = access_token
          )[["layers"]][["id"]]
      )
  }

  request <-
    httr::GET(
      base1,
      query = list(
        access_token = access_token,
        attribution = tolower(attribution),
        logo = tolower(logo),
        before_layer = before_layer
      )
    )

  if (request$status_code != 200) {
    content <- httr::content(request, as = "text")
    stop(print(jsonlite::fromJSON(content)), call. = FALSE)
  }

  if (!image) {
    return(request)
  }

  if (!rlang::is_installed("magick") && rlang::is_interactive()) {
    rlang::check_installed("magick")
  }

  magick::image_read(httr::content(request), strip = strip)
}

#' Add username and style_id to API query for static_mapbox
#'
#' @noRd
#' @importFrom stringi stri_extract
set_static_map_style <- function(style_url = NULL, username, style_id) {
  if (!is.null(style_url)) {
    username <- stringi::stri_extract(style_url,
                                      regex = paste0("(?<=styles/).+(?=/)"))
    style_id <- stringi::stri_extract(style_url,
                                      regex = paste0("(?<=", username, "/).+"))
  }

  # Construct the request URL
  # First, do chunk 1
  base <- sprintf(
    "https://api.mapbox.com/styles/v1/%s/%s/static",
    username, style_id
  )

  return(base)
}

#' Add overlay to API query for static_mapbox
#'
#' @noRd
#' @importFrom sf st_sf
#' @importFrom utils URLencode
#' @importFrom geojsonsf sf_geojson
set_static_map_overlay <- function(base = NULL,
                                   overlay_sf = NULL,
                                   overlay_style = NULL,
                                   overlay_markers = NULL) {
  overlay <- NULL

  if (!is.null(overlay_sf)) {
    if (is_sfc(overlay_sf)) {
      overlay_sf <- sf::st_sf(overlay_sf)
    }

    overlay_sf <-
      set_overlay_style(
        overlay_sf = overlay_sf,
        overlay_style = overlay_style
      )

    if (sf::st_crs(overlay_sf)["input"] != "EPSG:4326") {
      overlay_sf <- sf::st_transform(overlay_sf, 4326)
    }

    overlay_json <- geojsonsf::sf_geojson(overlay_sf)

    overlay <- sprintf("geojson(%s)", overlay_json)
  }

  if (!is.null(overlay_markers)) {
    if (attr(overlay_markers, "mapboxapi") != "marker_spec") {
      stop("Overlay markers should be formatted with `prep_overlay_markers()`
           before using in a static map", call. = FALSE)
    }

    marker_spec <- overlay_markers %>%
      unlist() %>%
      paste0(collapse = ",")

    if (!is.null(overlay)) {
      overlay <- paste(overlay, marker_spec, sep = ",")
    } else {
      overlay <- marker_spec
    }
  }

  if (!is.null(overlay)) {
    return(paste(base, overlay, sep = "/"))
  }

  base
}

#' Set overlay style
#'
#' @noRd
#' @importFrom utils URLencode
set_overlay_style <-
  function(overlay_sf,
           overlay_style = NULL) {
    if (is.null(overlay_style)) {
      overlay_style <- overlay_sf[1, ]
    }

    style_names <- names(overlay_style)

    if ("stroke" %in% style_names) {
      if (!is_hex(overlay_style$stroke)) {
        overlay_style$stroke <- col2hex(overlay_style$stroke)
      }

      overlay_sf$stroke <-
        utils::URLencode(overlay_style$stroke, reserved = TRUE)
    }

    if ("stroke-opacity" %in% style_names) {
      overlay_sf$`stroke-opacity` <-
        utils::URLencode(
          as.character(overlay_style$`stroke-opacity`),
          reserved = TRUE
          )
    }

    if ("stroke_opacity" %in% style_names) {
      overlay_sf$`stroke-opacity` <-
        utils::URLencode(
          as.character(overlay_style$stroke_opacity),
          reserved = TRUE
          )
    }

    if ("stroke-width" %in% style_names) {
      overlay_sf$`stroke-width` <-
        utils::URLencode(
          as.character(overlay_style$`stroke-width`),
          reserved = TRUE
          )
    }

    if ("stroke_width" %in% style_names) {
      overlay_sf$`stroke-width` <-
        utils::URLencode(
          as.character(overlay_style$stroke_width),
          reserved = TRUE
          )
    }

    if ("fill" %in% style_names) {
      if (!is_hex(overlay_style$fill)) {
        overlay_style$fill <- col2hex(overlay_style$fill)
      }

      overlay_sf$fill <-
        utils::URLencode(overlay_style$fill, reserved = TRUE)
    }

    if ("fill_opacity" %in% style_names) {
      overlay_sf$`fill-opacity` <-
        utils::URLencode(
          as.character(overlay_style$fill_opacity),
          reserved = TRUE
          )
    }

    if ("fill-opacity" %in% style_names) {
      overlay_sf$`fill-opacity` <-
        utils::URLencode(
          as.character(overlay_style$`fill-opacity`),
          reserved = TRUE
          )
    }

    overlay_sf
  }

#' Add width/height to API query for static_mapbox
#'
#' @noRd
set_static_map_dims <- function(base = NULL,
                                bbox = NULL,
                                width = NULL,
                                height = NULL,
                                scale = 0.5) {
  has_map_dims <- !all(is.null(c(width, height)))

  if (!has_map_dims && !is.null(bbox)) {
      asp <- as.numeric(abs(bbox$xmax - bbox$xmin) / abs(bbox$ymax - bbox$ymin))
      max_size <- min(1280, round(1280 * scale))
      width <- min(max_size, round(max_size * asp))
      height <- min(max_size, round(max_size / asp))
  } else if (has_map_dims) {
    width <- round(width)
    height <- round(height)
  }

  sprintf("%s/%sx%s", base, width, height)
}


#' Make a static Mapbox ggplot2 layer or tmap basemap
#'
#' These functions wrap [static_mapbox()] and [ggspatial::layer_spatial()] or
#' [tmap::tm_rgb()] to support the use of images from the [Mapbox Static Maps
#' API](https://www.mapbox.com/static-maps) as
#' [{ggplot2}](https://ggplot2.tidyverse.org/) or
#' [{tmap}](https://r-tmap.github.io/tmap/) basemaps.
#'
#' This function uses a different approach than [get_static_tiles()]. Instead,
#' [layer_static_mapbox()] is based largely on `layer_mapbox()` in the snapbox package
#' (available under a [MIT
#' license](https://github.com/anthonynorth/snapbox/blob/master/LICENSE). There
#' are a few key differences between [layer_static_mapbox()] and
#' `layer_mapbox()`. The "scale" parameter is equivalent to the
#' "scale_ratio" parameter for snapbox. Setting `scale_factor = "2x"` is
#' equivalent to setting `retina = TRUE.` Both functions return basemaps that
#' are no larger than a single tile (a maximum of 1280 by 1280 pixels).
#'
#' For [tm_static_mapbox()], [tmap::tm_shape] is called with `projection = 3857` and
#' [tmap::tm_rgb] is called with `max.value = 1`.
#'
#' @rdname layer_static_mapbox
#' @inheritParams static_mapbox
#' @param ... additional parameters passed to [ggspatial::layer_spatial] or [tmap::tm_rgb]
#' @export
#' @author Eli Pousson, \email{eli.pousson@gmail.com}
#' @author Anthony North, \email{anthony.jl.north@gmail.com}
#' @author Miles McBain, \email{miles.mcbain@gmail.com}
#' @importFrom rlang is_installed is_interactive check_installed
layer_static_mapbox <- function(location = NULL,
                                buffer_dist = 1000,
                                units = "m",
                                style_id,
                                username,
                                style_url = NULL,
                                overlay_sf = NULL,
                                overlay_style = NULL,
                                overlay_markers = NULL,
                                width = NULL,
                                height = NULL,
                                scale = 0.5,
                                scaling_factor = c("1x", "2x"),
                                attribution = TRUE,
                                logo = TRUE,
                                before_layer = NULL,
                                access_token = NULL,
                                ...) {
  if (!rlang::is_installed("ggspatial") && rlang::is_interactive()) {
    rlang::check_installed("ggspatial")
  }

  request <-
    static_mapbox(
      location = location,
      buffer_dist = buffer_dist,
      units = units,
      style_id = style_id,
      username = username,
      style_url = style_url,
      overlay_sf = overlay_sf,
      overlay_style = overlay_style,
      overlay_markers = overlay_markers,
      width = width,
      height = height,
      scale = scale,
      scaling_factor = scaling_factor,
      attribution = attribution,
      logo = logo,
      before_layer = before_layer,
      access_token = access_token,
      image = FALSE
    )

  ras <- request_to_raster(request, location, buffer_dist, units)

  ggspatial::layer_spatial(data = ras, ...)
}

#' @name tm_static_mapbox
#' @rdname layer_static_mapbox
#' @inheritParams static_mapbox
#' @importFrom rlang is_installed is_interactive check_installed
#' @export
tm_static_mapbox <- function(location = NULL,
                             buffer_dist = 1000,
                             units = "m",
                             style_id,
                             username,
                             style_url = NULL,
                             overlay_sf = NULL,
                             overlay_style = NULL,
                             overlay_markers = NULL,
                             width = NULL,
                             height = NULL,
                             scale = 0.5,
                             scaling_factor = c("1x", "2x"),
                             attribution = TRUE,
                             logo = TRUE,
                             before_layer = NULL,
                             access_token = NULL,
                             ...) {
  if (!rlang::is_installed("tmap") && rlang::is_interactive()) {
    rlang::check_installed("tmap")
  }

  request <-
    static_mapbox(
      location = location,
      buffer_dist = buffer_dist,
      units = units,
      style_id = style_id,
      username = username,
      style_url = style_url,
      overlay_sf = overlay_sf,
      overlay_style = overlay_style,
      overlay_markers = overlay_markers,
      width = width,
      height = height,
      scale = scale,
      scaling_factor = scaling_factor,
      attribution = attribution,
      logo = logo,
      before_layer = before_layer,
      access_token = access_token,
      image = FALSE
    )

  ras <- request_to_raster(request, location, buffer_dist, units)

  tmap::tm_shape(shp = ras, projection = 3857) +
    tmap::tm_rgb(max.value = 1, ...)
}

#' Convert static_mapbox API request to raster image
#'
#' @noRd
#' @importFrom sf st_crs
#' @importFrom raster brick extent projection
#' @importFrom httr content
request_to_raster <- function(request,
                              location = NULL,
                              buffer_dist = NULL,
                              units = "m") {
  ras <- raster::brick(httr::content(request))

  merc_bbox <- location_to_bbox(location, buffer_dist, units, crs = 3857)

  merc_proj <- sf::st_crs(3857)$proj4string

  raster::extent(ras) <- merc_bbox

  suppressWarnings(raster::projection(ras) <- merc_proj)

  ras
}

#' Prepare overlay markers for use in a Mapbox static map
#'
#' Markers are prepared to match GeoJSON
#' [marker-spec](https://github.com/mapbox/mapbox-gl-markers#geojson-marker-spec)
#' which is a partial implementation of the GeoJSON
#' [simplestyle-spec](https://github.com/mapbox/simplestyle-spec/tree/master/1.1.0)
#' (described as a work-in-progress by Mapbox).
#'
#' @param data An input data frame with longitude and latitude columns (X and Y
#'   or lon and lat as names are also acceptable) or an `sf` object with
#'   geometry type POINT.
#' @param marker_type The marker type; one of `"pin-s"`, for a small pin;
#'   `"pin-l"`, for a large pin; and `"url"`, for an image path. If
#'   marker_type is the same length as the rows in data, a mix of different
#'   marker types are allowed.
#' @param label The marker label (optional). Can be a letter, number (0 through
#'   99), or a valid Maki icon (see <https://labs.mapbox.com/maki-icons/>)
#'   for options).
#' @param color The marker color (optional). `color` can be specified as a color
#'   name or as a three or six-digit hexadecimal code (with or without the
#'   number sign).
#' @param longitude A vector of longitudes; inferred from the input dataset if
#'   `data` is provided.
#' @param latitude A vector of latitudes; inferred from the input dataset if
#'   `data` is provided.
#' @param url The URL of the image to be used for the icon if `marker_type =
#'   "url"`.
#' @name prep_overlay_markers
#' @return A formatted list of marker specifications that can be passed to the
#'   [static_mapbox] function.
#' @export
#' @importFrom sf st_geometry_type st_point_on_surface st_transform
#'   st_coordinates
#' @importFrom purrr map
prep_overlay_markers <- function(data = NULL,
                                 marker_type = c("pin-s", "pin-l", "url"),
                                 label = NA,
                                 color = NA,
                                 longitude = NULL,
                                 latitude = NULL,
                                 url = NA) {



  if (!is.null(data)) {
    if (any(grepl("^sf", class(data)))) {
      if (sf::st_geometry_type(data, by_geometry = FALSE) != "POINT") {
        data <- suppressWarnings(sf::st_point_on_surface(data))
      }

      # Construct the marker data frame
      coords_df <- data %>%
        sf::st_transform(4326) %>%
        sf::st_coordinates() %>%
        as.data.frame()

      coords_df <- cbind(data, coords_df)
    } else {
      coords_df <- data
    }
    # Infer coordinates from column names
    names(coords_df) <- tolower(names(coords_df))

    if (all(c("lon", "lat") %in% names(coords_df))) {
      coords_df$longitude <- coords_df$lon
      coords_df$latitude <- coords_df$lat
    } else if (all(c("x", "y") %in% names(coords_df))) {
      coords_df$longitude <- coords_df$x
      coords_df$latitude <- coords_df$y
    }

    if (!all(c("longitude", "latitude") %in% names(coords_df))) {
      stop("Your input dataset must have longitude/latitude information denoted
           by columns x and y, lon and lat, or longitude and latitude.",
        call. = FALSE
      )
    }

    if ("marker_type" %in% names(coords_df)) {
      marker_type <- coords_df$marker_type
    } else {
      marker_type <-
        match.arg(
          marker_type, c("pin-s", "pin-l", "url"),
          several.ok = length(marker_type) == nrow(coords_df)
        )
      coords_df$marker_type <- marker_type
    }

    if ("label" %in% names(coords_df)) {
      label <- coords_df$label
    } else {
      if (!is.null(label)) {
        coords_df$label <- label
      }
    }

    if ("color" %in% names(coords_df)) {
      color <- coords_df$color
    } else {
      if (!is.null(color)) {
        coords_df$color <- color
      }
    }

    if (!all(is.null(coords_df$color))) {
      if (!is_hex(coords_df$color)) {
        coords_df$color <- col2hex(coords_df$color, TRUE)
      } else {
        coords_df$color <- rmv_hash(coords_df$color)
      }
    }

    if ("url" %in% names(coords_df)) {
      url <- coords_df$url
    } else {
      if (!is.null(url)) {
        coords_df$url <- url
      }
    }
  } else {
    if (!is.null(color)) {
      if (!is_hex(color)) {
        color <- col2hex(color, TRUE)
      } else {
        color <- rmv_hash(color)
      }
    }

    coords_df <-
      data.frame(
        longitude = longitude,
        latitude = latitude,
        marker_type = marker_type,
        label = label,
        color = color,
        url = url
      )
  }

  # Iterate through the coords_df to make a formatted list of markers
  marker_list <- purrr::map(1:nrow(coords_df), ~ {
    format_marker(.x, coords_df = coords_df, url = url)
  })

  attr(marker_list, "mapboxapi") <- "marker_spec"

  marker_list
}

#' Format marker using marker_spec based on n row of a coordinate data frame
#'
#' @noRd
#' @importFrom utils URLencode
format_marker <- function(n, coords_df = NULL, url = NULL) {
  r <- coords_df[n, ]

  if (r$marker_type %in% c("pin-s", "pin-l")) {
    if (!is.na(r$label) && !is.na(r$color)) {
      return(sprintf(
        "%s-%s+%s(%s,%s)",
        r$marker_type,
        tolower(r$label),
        r$color,
        r$longitude,
        r$latitude
      ))
    } else if (is.na(r$label) && !is.na(r$color)) {
      return(sprintf(
        "%s-%s(%s,%s)",
        r$marker_type,
        r$color,
        r$longitude,
        r$latitude
      ))
    } else if (!is.na(r$label) && is.na(r$color)) {
      return(sprintf(
        "%s-%s(%s,%s)",
        r$marker_type,
        tolower(r$label),
        r$longitude,
        r$latitude
      ))
    } else {
      return(sprintf(
        "%s-(%s,%s)",
        r$marker_type,
        r$longitude,
        r$latitude
      ))
    }
  } else if (r$marker_type == "url") {
    if (is.null(url)) {
      stop("A valid URL must be provided.")
    }
    encoded_url <- utils::URLencode(r$url, reserved = TRUE)
    return(sprintf(
      "url-%s(%s,%s)",
      encoded_url,
      r$longitude,
      r$latitude
    ))
  }
}

#' Use a Mapbox style in a Leaflet map
#'
#' See the [Mapbox Static Tiles API
#' documentation](https://docs.mapbox.com/api/maps/static-tiles/) for more
#' information.
#'
#' @param map A map widget object created by [leaflet::leaflet()]
#' @param style_url A Mapbox style URL
#' @param style_id The style ID of a Mapbox style
#' @param username A Mapbox username
#' @param scaling_factor The scaling factor to use when rendering the tiles. A
#'   scaling factor of `"1x"` (the default) returns 512px by 512px tiles. A
#'   factor of `"1x"` returns 256x256 tiles, and a factor of `"2x"` returns
#'   1024x1024 tiles.
#' @param access_token Your Mapbox access token; which can be set with
#'   [mb_access_token()].
#' @param layerId the layer ID
#' @param group The name of the group the Mapbox tile layer should belong to
#'   (for use in Shiny and to modify layers control in a Leaflet workflow)
#' @param options A list of extra options (optional)
#' @param data The data object used to derive argument values; can be provided
#'   to the initial call to [leaflet::leaflet()]
#' @param attribution If `TRUE`, pass a standard attribution to
#'   [leaflet::addTiles()]. If `FALSE`, attribution is `NULL`. attribution can
#'   also be a character string including HTML.
#' @return A pointer to the Mapbox Static Tiles API which will be translated
#'   appropriately by the leaflet R package.
#'
#' @examples \dontrun{
#'
#' library(leaflet)
#' library(mapboxapi)
#'
#' leaflet() %>%
#'   addMapboxTiles(
#'     style_id = "light-v9",
#'     username = "mapbox"
#'   ) %>%
#'   setView(
#'     lng = -74.0051,
#'     lat = 40.7251,
#'     zoom = 13
#'   )
#' }
#'
#' @export
addMapboxTiles <- function(map,
                           style_id,
                           username,
                           style_url = NULL,
                           scaling_factor = c("1x", "0.5x", "2x"),
                           access_token = NULL,
                           layerId = NULL,
                           group = NULL,
                           options = leaflet::tileOptions(),
                           data = leaflet::getMapData(map),
                           attribution = TRUE) {
  access_token <- get_mb_access_token(access_token)

  if (!is.null(style_url)) {
    username <- stringi::stri_extract(style_url, regex = paste0("(?<=styles/).+(?=/)"))
    style_id <- stringi::stri_extract(style_url, regex = paste0("(?<=", username, "/).+"))
  }

  sfactor <- match.arg(scaling_factor)

  url <-
    switch(sfactor,
      "1x" = sprintf("https://api.mapbox.com/styles/v1/%s/%s/tiles/{z}/{x}/{y}?access_token=%s", username, style_id, access_token),
      "0.5x" = sprintf("https://api.mapbox.com/styles/v1/%s/%s/tiles/256/{z}/{x}/{y}?access_token=%s", username, style_id, access_token),
      "2x" = sprintf("https://api.mapbox.com/styles/v1/%s/%s/tiles/{z}/{x}/{y}@2x?access_token=%s", username, style_id, access_token)
    )

  if (!is.character(attribution)) {
    if (attribution) {
      attribution <- '&copy; <a href="https://www.mapbox.com/about/maps/">Mapbox</a> &copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> <strong><a href="https://www.mapbox.com/map-feedback/" target="_blank">Improve this map</a></strong>'
    } else {
      attribution <- NULL
    }
  }

  leaflet::addTiles(
    map = map,
    urlTemplate = url,
    attribution = attribution,
    layerId = layerId,
    group = group,
    options = options,
    data = data
  )
}


#' Get static tiles from a Mapbox style for use as a basemap
#'
#' This function queries the [Mapbox Static Tiles
#' API](https://docs.mapbox.com/api/maps/static-tiles/) and composites the tiles
#' as a raster suitable for use as a basemap in
#' [tmap](https://r-tmap.github.io/tmap/) or
#' [ggplot2](https://ggplot2.tidyverse.org/) (with the
#' [ggspatial::layer_spatial()] function. It returns a raster
#' layer that corresponds either to an input bounding box or a buffered area
#' around an input shape.
#'
#' @param location An input location for which you would like to request tiles.
#'                 Can be a length-4 vector representing a bounding box, or an `sf` object.
#'                 If an input `sf` object is supplied, use the `buffer_dist` argument to
#'                 control how much area you want to capture around the layer.
#'                 While the input `sf` object can be in an arbitrary coordinate reference system,
#'                 if a length-4 bounding box vector is supplied instead it must represent
#'                 WGS84 longitude/latitude coordinates and be in the order
#'                 `c(xmin, ymin, xmax, ymax)`.
#' @param zoom The zoom level for which you'd like to return tiles.
#' @param style_id A Mapbox style ID; retrieve yours from your Mapbox account.
#' @param username A Mapbox username.
#' @param style_url A Mapbox style URL.
#' @param scaling_factor The scaling factor to use; one of `"1x"` or `"2x"`.
#' @param buffer_dist The distance to buffer around an input `sf` object for determining tile extent, specified in units. Defaults to 5000.
#' @param units Units of `buffer_dist`; defaults to "m" (meters). If buffer_dist
#'   is a units class object, the units argument is ignored.
#' @param crop Whether or not to crop the result to the specified bounding box or buffer area.
#'             Defaults to `TRUE`; `FALSE` will return the extent of the overlapping
#'             tiles.
#' @param access_token A Mapbox access token. Supply yours here or set globally with the [mb_access_token()] function.
#'
#' @return A raster layer of tiles from the requested Mapbox style representing the area around the input location. The raster layer is projected in the Web Mercator coordinate reference system.
#'
#' @examples \dontrun{
#'
#' library(mapboxapi)
#' library(tigris)
#' library(tmap)
#' library(ggspatial)
#' library(ggplot2)
#'
#' ny_tracts <- tracts("NY", "New York", cb = TRUE)
#'
#' ny_tiles <- get_static_tiles(
#'   location = ny_tracts,
#'   zoom = 10,
#'   style_id = "light-v9",
#'   username = "mapbox"
#' )
#'
#' # tmap usage:
#' tm_shape(ny_tiles) +
#'   tm_rgb() +
#'   tm_shape(ny_tracts) +
#'   tm_polygons(alpha = 0.5, col = "navy") +
#'   tm_credits("Basemap (c) Mapbox, (c) OpenStreetMap",
#'     position = c("RIGHT", "BOTTOM")
#'   )
#'
#' # ggplot2 usage:
#' ggplot() +
#'   layer_spatial(ny_tiles) +
#'   geom_sf(data = ny_tracts, fill = "navy", alpha = 0.5) +
#'   theme_void() +
#'   labs(caption = "Basemap (c) Mapbox, (c) OpenStreetMap")
#' }
#'
#' @export
get_static_tiles <- function(location,
                             zoom,
                             style_id,
                             username,
                             style_url = NULL,
                             scaling_factor = c("1x", "2x"),
                             buffer_dist = 5000,
                             units = "m",
                             crop = TRUE,
                             access_token = NULL) {
  message("Attribution is required if using Mapbox tiles on a map.\nAdd the text '(c) Mapbox, (c) OpenStreetMap' to your map for proper attribution.")

  access_token <- get_mb_access_token(access_token)

  if (!is.null(style_url)) {
    username <- stringi::stri_extract(style_url, regex = paste0("(?<=styles/).+(?=/)"))
    style_id <- stringi::stri_extract(style_url, regex = paste0("(?<=", username, "/).+"))
  }

  bbox <- location_to_bbox(location, buffer_dist, units)

  tile_grid <- slippymath::bbox_to_tile_grid(bbox = bbox, zoom = zoom)

  tile_ids <- tile_grid$tiles

  sfactor <- match.arg(scaling_factor)

  tile_list <- purrr::map2(tile_ids$x, tile_ids$y, ~ {
    # Build the request to Mapbox
    if (sfactor == "2x") {
      url <- sprintf(
        "https://api.mapbox.com/styles/v1/%s/%s/tiles/%s/%s/%s@2x",
        username, style_id, zoom, .x, .y
      )
    } else {
      url <- sprintf(
        "https://api.mapbox.com/styles/v1/%s/%s/tiles/%s/%s/%s",
        username, style_id, zoom, .x, .y
      )
    }

    box <- slippymath::tile_bbox(.x, .y, zoom)

    tmp <- tempdir()

    loc <- file.path(tmp, sprintf("%s_%s.png", .x, .y))

    request <- httr::GET(url,
                         query = list(access_token = access_token),
                         httr::write_disk(loc, overwrite = TRUE)
    )

    # Only try to read the data if there is data available
    if (request$status_code == 200) {

      # Most styles will be returned as PNG as they are composed entirely
      # of vector layers. However, if a style includes satellite imagery it will
      # be returned as JPEG. Check this at this step.
      my_img <- try(png::readPNG(loc), silent = TRUE)

      if ("try-error" %in% class(my_img)) {
        my_img <- jpeg::readJPEG(loc)
      }

      my_img <- my_img * 255

      merc <- sf::st_crs(3857)$proj4string

      ras <- raster::brick(my_img)
      raster::extent(ras) <- box
      suppressWarnings(raster::projection(ras) <- merc)

      return(ras)
    }
  })

  # Assemble the list of raster tiles
  # Adapted from Michael Sumner's ceramic code: https://github.com/hypertidy/ceramic/blob/master/R/raster.R
  if (length(tile_list) == 1) {
    out_brick <- tile_list[[1]]
  } else {
    target_crs <- suppressWarnings(raster::projection(tile_list[[1]]))

    out_raster <- suppressWarnings(tile_list %>%
                                     purrr::map(function(tile) {
                                       raster::extent(tile)
                                     }) %>%
                                     purrr::reduce(raster::union) %>%
                                     raster::raster(crs = target_crs))

    raster::res(out_raster) <- suppressWarnings(raster::res(tile_list[[1]]))

    raster_cells <- suppressWarnings(tile_list %>%
                                       map(function(tile) {
                                         raster::cellsFromExtent(out_raster, tile)
                                       }) %>%
                                       unlist(use.names = FALSE))

    raster_values <- suppressWarnings(tile_list %>%
                                        map(function(tile) {
                                          raster::values(tile)
                                        }) %>%
                                        reduce(rbind))

    out_brick <- suppressWarnings(raster::brick(out_raster, out_raster, out_raster) %>%
                                    raster::setValues(raster_values[order(raster_cells), ]))
  }

  if (crop) {
    bb_shape <- bbox %>%
      sf::st_as_sfc(crs = 4326) %>%
      sf::st_sf() %>%
      sf::st_transform(sf::st_crs(out_brick))

    cropped_brick <- raster::crop(out_brick, bb_shape)

    return(cropped_brick)
  } else {
    return(out_brick)
  }
}

Try the mapboxapi package in your browser

Any scripts or data that you put into this service are public.

mapboxapi documentation built on Oct. 17, 2023, 1:10 a.m.